MENU

【Golang】Google API 複数scopeのクレデンシャルを取得

先日1年ほど前に書いたGoのソースを眺めていたら、Google APIのクレデンシャル取得のところをどうしてこういう実装にしたのか忘れてしまい、戸惑ったので備忘として残しておきます。

Gmail API Go Quickstart に掲載されている実装の

        // If modifying these scopes, delete your previously saved token.json.
        config, err := google.ConfigFromJSON(b, gmail.GmailReadonlyScope)

の箇所が、ここではGmailのReadOnlyのScopeでconfigを取得していますが、これが複数サービス(たとえはGoogle Calender、Google Tasks)への権限も含めて取得したいとなった場合、このConfigFromJSONは使えず、またscopeを複数配列で渡せるようなメソッドもなかった(見つけられなかっただけかもしれません。。) ので、自分で実装を変更してみた、という内容です。

方法

↑の実装ではConfigFromJSONで取得するはずだった、Credentials構造体のscopeの箇所だけ自分で代入した、というだけです。

type Credentials struct {
    Installed struct {
        ClientId                string   `json:"client_id"`
        ProjectId               string   `json:"project_id"`
        AuthUrl                 string   `json:"auth_url"`
        TokenUrl                string   `json:"token_url"`
        AuthProviderX509CertUrl string   `json:"auth_provider_x509_cert_url"`
        ClientSecret            string   `json:"client_secret"`
        RdirectUris             []string `json:"redirect_uris"`
    } `json:"installed"`
}

func getConfig() *oauth2.Config {
    b, err := ioutil.ReadFile("credentials.json")
    if err != nil {
        log.Fatalf("Unable to read client secret file: %v", err)
    }

    var cred Credentials
    json.Unmarshal(b, &cred)
    // tasks.TasksReadonlyScope → tasks.TasksScope
    scopes := []string{gmail.GmailReadonlyScope, tasks.TasksScope, calendar.CalendarReadonlyScope}
    return &oauth2.Config{
        ClientID:     cred.Installed.ClientId,
        ClientSecret: cred.Installed.ClientSecret,
        Endpoint:     google.Endpoint,
        Scopes:       scopes,
        RedirectURL:  cred.Installed.RdirectUris[0],
    }
}

それ以外のToken取得などの処理はチュートリアルのままでOKです。
全体としては以下のような実装になります。

type Credentials struct {
    Installed struct {
        ClientId                string   `json:"client_id"`
        ProjectId               string   `json:"project_id"`
        AuthUrl                 string   `json:"auth_url"`
        TokenUrl                string   `json:"token_url"`
        AuthProviderX509CertUrl string   `json:"auth_provider_x509_cert_url"`
        ClientSecret            string   `json:"client_secret"`
        RdirectUris             []string `json:"redirect_uris"`
    } `json:"installed"`
}

// Retrieve a token, saves the token, then returns the generated client.
func getClient(config *oauth2.Config) *http.Client {
    // The file token.json stores the user's access and refresh tokens, and is
    // created automatically when the authorization flow completes for the first
    // time.
    tokFile := "token.json"
    tok, err := tokenFromFile(tokFile)
    if err != nil {
        tok = getTokenFromWeb(config)
        saveToken(tokFile, tok)
    }
    return config.Client(context.Background(), tok)
}

// Request a token from the web, then returns the retrieved token.
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
    authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
    fmt.Printf("Go to the following link in your browser then type the "+
        "authorization code: \n%v\n", authURL)

    var authCode string
    if _, err := fmt.Scan(&authCode); err != nil {
        log.Fatalf("Unable to read authorization code: %v", err)
    }

    tok, err := config.Exchange(context.TODO(), authCode)
    if err != nil {
        log.Fatalf("Unable to retrieve token from web: %v", err)
    }
    return tok
}

// Retrieves a token from a local file.
func tokenFromFile(file string) (*oauth2.Token, error) {
    f, err := os.Open(file)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    tok := &oauth2.Token{}
    err = json.NewDecoder(f).Decode(tok)
    return tok, err
}

// Saves a token to a file path.
func saveToken(path string, token *oauth2.Token) {
    fmt.Printf("Saving credential file to: %s\n", path)
    f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
    if err != nil {
        log.Fatalf("Unable to cache oauth token: %v", err)
    }
    defer f.Close()
    json.NewEncoder(f).Encode(token)
}

func getConfig() *oauth2.Config {
    b, err := ioutil.ReadFile("credentials.json")
    if err != nil {
        log.Fatalf("Unable to read client secret file: %v", err)
    }

    var cred Credentials
    json.Unmarshal(b, &cred)
    // tasks.TasksReadonlyScope → tasks.TasksScope
    scopes := []string{gmail.GmailReadonlyScope, tasks.TasksScope, calendar.CalendarReadonlyScope}
    return &oauth2.Config{
        ClientID:     cred.Installed.ClientId,
        ClientSecret: cred.Installed.ClientSecret,
        Endpoint:     google.Endpoint,
        Scopes:       scopes,
        RedirectURL:  cred.Installed.RdirectUris[0],
    }
}

func main() {
    config := getConfig()
    client := getClient(config)
    srv, err := gmail.New(client)
        //
        // gmail取得処理
        //
    srv, err := calendar.New(client)
        //
        // calendar取得処理
        //
    srv, err :=tasks.New(client)
        //
        //task取得処理
        //
}

こんな感じで一つのトークン、Configで複数サービスへの接続ができるようになります。
もちろん一つのクレデンシャルに権限を与えすぎるのもリスクはありますが自分で管理するアプリケーションの中で接続サービスごとにトークンを分けるのも大変なので、自分はこうしてしまっています。