| package oauth |
| |
| import ( |
| "code.google.com/p/goauth2/oauth" |
| "encoding/json" |
| "fmt" |
| "net/http" |
| "os" |
| ) |
| |
| // googleOAuth implements the OAuthProvider interface with google oauth 2.0. |
| type googleOAuth struct { |
| // client_id and client_secret registered with the Google Developer |
| // Console for API access. |
| clientID, clientSecret string |
| |
| scope, authURL, tokenURL string |
| |
| // URL used to verify google tokens. |
| // (From https://developers.google.com/accounts/docs/OAuth2Login#validatinganidtoken |
| // and https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken) |
| verifyURL string |
| } |
| |
| func NewGoogleOAuth(configFile string) (OAuthProvider, error) { |
| clientID, clientSecret, err := getOAuthClientIDAndSecret(configFile) |
| if err != nil { |
| return nil, err |
| } |
| return &googleOAuth{ |
| clientID: clientID, |
| clientSecret: clientSecret, |
| scope: "email", |
| authURL: "https://accounts.google.com/o/oauth2/auth", |
| tokenURL: "https://accounts.google.com/o/oauth2/token", |
| verifyURL: "https://www.googleapis.com/oauth2/v1/tokeninfo?", |
| }, nil |
| } |
| |
| func (g *googleOAuth) AuthURL(redirectUrl, state string) string { |
| return g.oauthConfig(redirectUrl).AuthCodeURL(state) |
| } |
| |
| // ExchangeAuthCodeForEmail exchanges the authorization code (which must |
| // have been obtained with scope=email) for an OAuth token and then uses Google's |
| // tokeninfo API to extract the email address from that token. |
| func (g *googleOAuth) ExchangeAuthCodeForEmail(authcode string, url string) (string, error) { |
| config := g.oauthConfig(url) |
| t, err := (&oauth.Transport{Config: config}).Exchange(authcode) |
| if err != nil { |
| return "", fmt.Errorf("failed to exchange authorization code for token: %v", err) |
| } |
| // Ideally, would validate the token ourselves without an HTTP roundtrip. |
| // However, for now, as per: |
| // https://developers.google.com/accounts/docs/OAuth2Login#validatinganidtoken |
| // pay an HTTP round-trip to have Google do this. |
| if t.Extra == nil || len(t.Extra["id_token"]) == 0 { |
| return "", fmt.Errorf("no GoogleIDToken found in OAuth token") |
| } |
| // The GoogleIDToken is currently validated by sending an HTTP request to |
| // googleapis.com. This adds a round-trip and service may be denied by |
| // googleapis.com if this handler becomes a breakout success and receives tons |
| // of traffic. If either is a concern, the GoogleIDToken can be validated |
| // without an additional HTTP request. |
| // See: https://developers.google.com/accounts/docs/OAuth2Login#validatinganidtoken |
| tinfo, err := http.Get(g.verifyURL + "id_token=" + t.Extra["id_token"]) |
| if err != nil { |
| return "", fmt.Errorf("failed to talk to GoogleIDToken verifier (%q): %v", g.verifyURL, err) |
| } |
| if tinfo.StatusCode != http.StatusOK { |
| return "", fmt.Errorf("failed to verify GoogleIDToken: %s", tinfo.Status) |
| } |
| var gtoken token |
| if err := json.NewDecoder(tinfo.Body).Decode(>oken); err != nil { |
| return "", fmt.Errorf("invalid JSON response from Google's tokeninfo API: %v", err) |
| } |
| if !gtoken.VerifiedEmail { |
| return "", fmt.Errorf("email not verified: %#v", gtoken) |
| } |
| if gtoken.Issuer != "accounts.google.com" { |
| return "", fmt.Errorf("invalid issuer: %v", gtoken.Issuer) |
| } |
| if gtoken.Audience != config.ClientId { |
| return "", fmt.Errorf("unexpected audience(%v) in GoogleIDToken", gtoken.Audience) |
| } |
| return gtoken.Email, nil |
| } |
| |
| func (g *googleOAuth) oauthConfig(redirectUrl string) *oauth.Config { |
| return &oauth.Config{ |
| ClientId: g.clientID, |
| ClientSecret: g.clientSecret, |
| RedirectURL: redirectUrl, |
| Scope: g.scope, |
| AuthURL: g.authURL, |
| TokenURL: g.tokenURL, |
| } |
| } |
| |
| func getOAuthClientIDAndSecret(configFile string) (clientID, clientSecret string, err error) { |
| f, err := os.Open(configFile) |
| if err != nil { |
| return "", "", fmt.Errorf("failed to open %q: %v", configFile, err) |
| } |
| defer f.Close() |
| clientID, clientSecret, err = ClientIDAndSecretFromJSON(f) |
| if err != nil { |
| return "", "", fmt.Errorf("failed to decode JSON in %q: %v", configFile, err) |
| } |
| return clientID, clientSecret, nil |
| } |
| |
| // IDToken JSON message returned by Google's verification endpoint. |
| // |
| // This differs from the description in: |
| // https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo |
| // because the Google tokeninfo endpoint |
| // (https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=XYZ123) |
| // mentioned in: |
| // https://developers.google.com/accounts/docs/OAuth2Login#validatinganidtoken |
| // seems to return the following JSON message. |
| type token struct { |
| Issuer string `json:"issuer"` |
| IssuedTo string `json:"issued_to"` |
| Audience string `json:"audience"` |
| UserID string `json:"user_id"` |
| ExpiresIn int64 `json:"expires_in"` |
| IssuedAt int64 `json:"issued_at"` |
| Email string `json:"email"` |
| VerifiedEmail bool `json:"verified_email"` |
| } |