-
Notifications
You must be signed in to change notification settings - Fork 28
/
googleoauth.go
190 lines (176 loc) · 7.01 KB
/
googleoauth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Copyright 2015 The Vanadium 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 oauth
import (
"encoding/json"
"fmt"
"net/http"
"os"
"golang.org/x/oauth2"
"v.io/v23/context"
)
// 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
ctx *context.T
}
func NewGoogleOAuth(ctx *context.T, 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?",
ctx: ctx,
}, nil
}
func (g *googleOAuth) AuthURL(redirectUrl, state string, approval AuthURLApproval) string {
var opts []oauth2.AuthCodeOption
if approval == ExplicitApproval {
opts = append(opts, oauth2.ApprovalForce)
}
return g.oauthConfig(redirectUrl).AuthCodeURL(state, opts...)
}
// 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 := config.Exchange(oauth2.NoContext, authcode)
if err != nil {
return "", fmt.Errorf("failed to exchange authorization code for token: %v", err)
}
if !t.Valid() {
return "", fmt.Errorf("oauth2 token invalid")
}
// 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.
idToken, ok := t.Extra("id_token").(string)
if !ok {
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=" + idToken)
if err != nil {
return "", fmt.Errorf("failed to talk to GoogleIDToken verifier (%q): %v", g.verifyURL, err)
}
defer tinfo.Body.Close()
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)
}
// We check both "verified_email" and "email_verified" here because the token response sometimes
// contains one and sometimes contains the other.
if !gtoken.VerifiedEmail && !gtoken.EmailVerified {
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
}
// GetEmailAndClientID uses Google's tokeninfo API to determine the email and clientID
// associated with the token.
func (g *googleOAuth) GetEmailAndClientID(accessToken string) (string, string, error) {
// As per https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken
// we obtain the 'info' for the token via an HTTP roundtrip to Google.
tokeninfo, err := http.Get(g.verifyURL + "access_token=" + accessToken)
if err != nil {
return "", "", fmt.Errorf("unable to use token: %v", err)
}
defer tokeninfo.Body.Close()
if tokeninfo.StatusCode != http.StatusOK {
return "", "", fmt.Errorf("unable to verify access token, OAuth2 TokenInfo endpoint responded with StatusCode: %v", tokeninfo.StatusCode)
}
// tokeninfo contains a JSON-encoded struct
var token struct {
IssuedTo string `json:"issued_to"`
Audience string `json:"audience"`
UserID string `json:"user_id"`
Scope string `json:"scope"`
ExpiresIn int64 `json:"expires_in"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
EmailVerified bool `json:"email_verified"`
AccessType string `json:"access_type"`
}
if err := json.NewDecoder(tokeninfo.Body).Decode(&token); err != nil {
return "", "", fmt.Errorf("invalid JSON response from Google's tokeninfo API: %v", err)
}
// We check both "verified_email" and "email_verified" here because the token response sometimes
// contains one and sometimes contains the other.
if !token.VerifiedEmail && !token.EmailVerified {
return "", "", fmt.Errorf("email not verified")
}
return token.Email, token.Audience, nil
}
func (g *googleOAuth) oauthConfig(redirectUrl string) *oauth2.Config {
return &oauth2.Config{
ClientID: g.clientID,
ClientSecret: g.clientSecret,
RedirectURL: redirectUrl,
Scopes: []string{g.scope},
Endpoint: oauth2.Endpoint{
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"`
EmailVerified bool `json:"email_verified"`
}