| // Copyright 2013 The goauth2 Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package serviceaccount provides support for making OAuth2-authorized |
| // HTTP requests from Google Compute Engine instances using service accounts. |
| // |
| // See: https://developers.google.com/compute/docs/authentication |
| // |
| // Example usage: |
| // |
| // client, err := serviceaccount.NewClient(&serviceaccount.Options{}) |
| // if err != nil { |
| // c.Errorf("failed to create service account client: %q", err) |
| // return err |
| // } |
| // client.Post("https://www.googleapis.com/compute/...", ...) |
| // client.Post("https://www.googleapis.com/bigquery/...", ...) |
| // |
| package serviceaccount |
| |
| import ( |
| "encoding/json" |
| "net/http" |
| "net/url" |
| "path" |
| "sync" |
| "time" |
| |
| "code.google.com/p/goauth2/oauth" |
| ) |
| |
| const ( |
| metadataServer = "metadata" |
| serviceAccountPath = "/computeMetadata/v1/instance/service-accounts" |
| ) |
| |
| // Options configures a service account Client. |
| type Options struct { |
| // Underlying transport of service account Client. |
| // If nil, http.DefaultTransport is used. |
| Transport http.RoundTripper |
| |
| // Service account name. |
| // If empty, "default" is used. |
| Account string |
| } |
| |
| // NewClient returns an *http.Client authorized with the service account |
| // configured in the Google Compute Engine instance. |
| func NewClient(opt *Options) (*http.Client, error) { |
| tr := http.DefaultTransport |
| account := "default" |
| if opt != nil { |
| if opt.Transport != nil { |
| tr = opt.Transport |
| } |
| if opt.Account != "" { |
| account = opt.Account |
| } |
| } |
| t := &transport{ |
| Transport: tr, |
| Account: account, |
| } |
| // Get the initial access token. |
| if _, err := fetchToken(t); err != nil { |
| return nil, err |
| } |
| return &http.Client{ |
| Transport: t, |
| }, nil |
| } |
| |
| type tokenData struct { |
| AccessToken string `json:"access_token"` |
| ExpiresIn float64 `json:"expires_in"` |
| TokenType string `json:"token_type"` |
| } |
| |
| // transport is an oauth.Transport with a custom Refresh and RoundTrip implementation. |
| type transport struct { |
| Transport http.RoundTripper |
| Account string |
| |
| mu sync.Mutex |
| *oauth.Token |
| } |
| |
| // Refresh renews the transport's AccessToken. |
| // t.mu sould be held when this is called. |
| func (t *transport) refresh() error { |
| // https://developers.google.com/compute/docs/metadata#transitioning |
| // v1 requires "Metadata-Flavor: Google" header. |
| tokenURL := &url.URL{ |
| Scheme: "http", |
| Host: metadataServer, |
| Path: path.Join(serviceAccountPath, t.Account, "token"), |
| } |
| req, err := http.NewRequest("GET", tokenURL.String(), nil) |
| if err != nil { |
| return err |
| } |
| req.Header.Add("Metadata-Flavor", "Google") |
| resp, err := http.DefaultClient.Do(req) |
| if err != nil { |
| return err |
| } |
| defer resp.Body.Close() |
| d := json.NewDecoder(resp.Body) |
| var token tokenData |
| err = d.Decode(&token) |
| if err != nil { |
| return err |
| } |
| t.Token = &oauth.Token{ |
| AccessToken: token.AccessToken, |
| Expiry: time.Now().Add(time.Duration(token.ExpiresIn) * time.Second), |
| } |
| return nil |
| } |
| |
| // Refresh renews the transport's AccessToken. |
| func (t *transport) Refresh() error { |
| t.mu.Lock() |
| defer t.mu.Unlock() |
| return t.refresh() |
| } |
| |
| // Fetch token from cache or generate a new one if cache miss or expired. |
| func fetchToken(t *transport) (*oauth.Token, error) { |
| // Get a new token using Refresh in case of a cache miss of if it has expired. |
| t.mu.Lock() |
| defer t.mu.Unlock() |
| if t.Token == nil || t.Expired() { |
| if err := t.refresh(); err != nil { |
| return nil, err |
| } |
| } |
| return t.Token, nil |
| } |
| |
| // cloneRequest returns a clone of the provided *http.Request. |
| // The clone is a shallow copy of the struct and its Header map. |
| func cloneRequest(r *http.Request) *http.Request { |
| // shallow copy of the struct |
| r2 := new(http.Request) |
| *r2 = *r |
| // deep copy of the Header |
| r2.Header = make(http.Header) |
| for k, s := range r.Header { |
| r2.Header[k] = s |
| } |
| return r2 |
| } |
| |
| // RoundTrip issues an authorized HTTP request and returns its response. |
| func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { |
| token, err := fetchToken(t) |
| if err != nil { |
| return nil, err |
| } |
| |
| // To set the Authorization header, we must make a copy of the Request |
| // so that we don't modify the Request we were given. |
| // This is required by the specification of http.RoundTripper. |
| newReq := cloneRequest(req) |
| newReq.Header.Set("Authorization", "Bearer "+token.AccessToken) |
| |
| // Make the HTTP request. |
| return t.Transport.RoundTrip(newReq) |
| } |