-
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
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
440
-
441
-
442
-
443
-
444
-
445
-
446
-
447
-
448
-
449
-
450
-
451
-
452
-
453
-
454
-
455
-
456
-
457
-
458
-
459
-
460
-
461
-
462
-
463
-
464
-
465
-
466
-
467
-
468
-
469
-
470
-
471
-
472
-
473
-
474
package openidConnect
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
const (
// Standard Claims http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
// fixed, cannot be changed
subjectClaim = "sub"
expiryClaim = "exp"
audienceClaim = "aud"
issuerClaim = "iss"
PreferredUsernameClaim = "preferred_username"
EmailClaim = "email"
NameClaim = "name"
NicknameClaim = "nickname"
PictureClaim = "picture"
GivenNameClaim = "given_name"
FamilyNameClaim = "family_name"
AddressClaim = "address"
// Unused but available to set in Provider claims
MiddleNameClaim = "middle_name"
ProfileClaim = "profile"
WebsiteClaim = "website"
EmailVerifiedClaim = "email_verified"
GenderClaim = "gender"
BirthdateClaim = "birthdate"
ZoneinfoClaim = "zoneinfo"
LocaleClaim = "locale"
PhoneNumberClaim = "phone_number"
PhoneNumberVerifiedClaim = "phone_number_verified"
UpdatedAtClaim = "updated_at"
clockSkew = 10 * time.Second
)
// Provider is the implementation of `goth.Provider` for accessing OpenID Connect provider
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
OpenIDConfig *OpenIDConfig
config *oauth2.Config
providerName string
UserIdClaims []string
NameClaims []string
NickNameClaims []string
EmailClaims []string
AvatarURLClaims []string
FirstNameClaims []string
LastNameClaims []string
LocationClaims []string
SkipUserInfoRequest bool
}
type OpenIDConfig struct {
AuthEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
// If OpenID discovery is enabled, the end_session_endpoint field can optionally be provided
// in the discovery endpoint response according to OpenID spec. See:
// https://openid.net/specs/openid-connect-session-1_0-17.html#OPMetadata
EndSessionEndpoint string `json:"end_session_endpoint, omitempty"`
Issuer string `json:"issuer"`
}
type RefreshTokenResponse struct {
AccessToken string `json:"access_token"`
// The OpenID spec defines the ID token as an optional response field in the
// refresh token flow. As a result, a new ID token may not be returned in a successful
// response.
// See more: https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
IdToken string `json:"id_token, omitempty"`
// The OAuth spec defines the refresh token as an optional response field in the
// refresh token flow. As a result, a new refresh token may not be returned in a successful
// response.
//See more: https://www.oauth.com/oauth2-servers/making-authenticated-requests/refreshing-an-access-token/
RefreshToken string `json:"refresh_token,omitempty"`
}
// New creates a new OpenID Connect provider, and sets up important connection details.
// You should always call `openidConnect.New` to get a new Provider. Never try to create
// one manually.
// New returns an implementation of an OpenID Connect Authorization Code Flow
// See http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
// ID Token decryption is not (yet) supported
// UserInfo decryption is not (yet) supported
func New(clientKey, secret, callbackURL, openIDAutoDiscoveryURL string, scopes ...string) (*Provider, error) {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
UserIdClaims: []string{subjectClaim},
NameClaims: []string{NameClaim},
NickNameClaims: []string{NicknameClaim, PreferredUsernameClaim},
EmailClaims: []string{EmailClaim},
AvatarURLClaims: []string{PictureClaim},
FirstNameClaims: []string{GivenNameClaim},
LastNameClaims: []string{FamilyNameClaim},
LocationClaims: []string{AddressClaim},
providerName: "openid-connect",
}
openIDConfig, err := getOpenIDConfig(p, openIDAutoDiscoveryURL)
if err != nil {
return nil, err
}
p.OpenIDConfig = openIDConfig
p.config = newConfig(p, scopes, openIDConfig)
return p, nil
}
// Name is the name used to retrieve this provider later.
func (p *Provider) Name() string {
return p.providerName
}
// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
func (p *Provider) SetName(name string) {
p.providerName = name
}
func (p *Provider) Client() *http.Client {
return goth.HTTPClientWithFallBack(p.HTTPClient)
}
// Debug is a no-op for the openidConnect package.
func (p *Provider) Debug(debug bool) {}
// BeginAuth asks the OpenID Connect provider for an authentication end-point.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
url := p.config.AuthCodeURL(state)
session := &Session{
AuthURL: url,
}
return session, nil
}
// FetchUser will use the the id_token and access requested information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
sess := session.(*Session)
expiresAt := sess.ExpiresAt
if sess.IDToken == "" {
return goth.User{}, fmt.Errorf("%s cannot get user information without id_token", p.providerName)
}
// decode returned id token to get expiry
claims, err := decodeJWT(sess.IDToken)
if err != nil {
return goth.User{}, fmt.Errorf("oauth2: error decoding JWT token: %v", err)
}
expiry, err := p.validateClaims(claims)
if err != nil {
return goth.User{}, fmt.Errorf("oauth2: error validating JWT token: %v", err)
}
if expiry.Before(expiresAt) {
expiresAt = expiry
}
if err := p.getUserInfo(sess.AccessToken, claims); err != nil {
return goth.User{}, err
}
user := goth.User{
AccessToken: sess.AccessToken,
Provider: p.Name(),
RefreshToken: sess.RefreshToken,
ExpiresAt: expiresAt,
RawData: claims,
IDToken: sess.IDToken,
}
p.userFromClaims(claims, &user)
return user, err
}
//RefreshTokenAvailable refresh token is provided by auth provider or not
func (p *Provider) RefreshTokenAvailable() bool {
return true
}
//RefreshToken get new access token based on the refresh token
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
token := &oauth2.Token{RefreshToken: refreshToken}
ts := p.config.TokenSource(oauth2.NoContext, token)
newToken, err := ts.Token()
if err != nil {
return nil, err
}
return newToken, err
}
// The ID token is a fundamental part of the OpenID connect refresh token flow but is not part of the OAuth flow.
// The existing RefreshToken function leverages the OAuth library's refresh token mechanism, ignoring the refreshed
// ID token. As a result, a new function needs to be exposed (rather than changing the existing function, for backwards
// compatibility purposes) that also returns the id_token in the OpenID refresh token flow API response
// Learn more about ID tokens: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
func (p *Provider) RefreshTokenWithIDToken(refreshToken string) (*RefreshTokenResponse, error) {
urlValues := url.Values{
"grant_type": {"refresh_token"},
"refresh_token": {refreshToken},
"client_id": {p.ClientKey},
"client_secret": {p.Secret},
}
req, err := http.NewRequest("POST", p.OpenIDConfig.TokenEndpoint, strings.NewReader(urlValues.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := p.Client().Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Non-200 response from RefreshToken: %d, WWW-Authenticate=%s", resp.StatusCode, resp.Header.Get("WWW-Authenticate"))
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
resp.Body.Close()
refreshTokenResponse := &RefreshTokenResponse{}
err = json.Unmarshal(body, refreshTokenResponse)
if err != nil {
return nil, err
}
return refreshTokenResponse, nil
}
// validate according to standard, returns expiry
// http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (p *Provider) validateClaims(claims map[string]interface{}) (time.Time, error) {
audience := getClaimValue(claims, []string{audienceClaim})
if audience != p.ClientKey {
found := false
audiences := getClaimValues(claims, []string{audienceClaim})
for _, aud := range audiences {
if aud == p.ClientKey {
found = true
break
}
}
if !found {
return time.Time{}, errors.New("audience in token does not match client key")
}
}
issuer := getClaimValue(claims, []string{issuerClaim})
if issuer != p.OpenIDConfig.Issuer {
return time.Time{}, errors.New("issuer in token does not match issuer in OpenIDConfig discovery")
}
// expiry is required for JWT, not for UserInfoResponse
// is actually a int64, so force it in to that type
expiryClaim := int64(claims[expiryClaim].(float64))
expiry := time.Unix(expiryClaim, 0)
if expiry.Add(clockSkew).Before(time.Now()) {
return time.Time{}, errors.New("user info JWT token is expired")
}
return expiry, nil
}
func (p *Provider) userFromClaims(claims map[string]interface{}, user *goth.User) {
// required
user.UserID = getClaimValue(claims, p.UserIdClaims)
user.Name = getClaimValue(claims, p.NameClaims)
user.NickName = getClaimValue(claims, p.NickNameClaims)
user.Email = getClaimValue(claims, p.EmailClaims)
user.AvatarURL = getClaimValue(claims, p.AvatarURLClaims)
user.FirstName = getClaimValue(claims, p.FirstNameClaims)
user.LastName = getClaimValue(claims, p.LastNameClaims)
user.Location = getClaimValue(claims, p.LocationClaims)
}
func (p *Provider) getUserInfo(accessToken string, claims map[string]interface{}) error {
// skip if there is no UserInfoEndpoint or is explicitly disabled
if p.OpenIDConfig.UserInfoEndpoint == "" || p.SkipUserInfoRequest {
return nil
}
userInfoClaims, err := p.fetchUserInfo(p.OpenIDConfig.UserInfoEndpoint, accessToken)
if err != nil {
return err
}
// The sub (subject) Claim MUST always be returned in the UserInfo Response.
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
userInfoSubject := getClaimValue(userInfoClaims, []string{subjectClaim})
if userInfoSubject == "" {
return fmt.Errorf("userinfo response did not contain a 'sub' claim: %#v", userInfoClaims)
}
// The sub Claim in the UserInfo Response MUST be verified to exactly match the sub Claim in the ID Token;
// if they do not match, the UserInfo Response values MUST NOT be used.
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
subject := getClaimValue(claims, []string{subjectClaim})
if userInfoSubject != subject {
return fmt.Errorf("userinfo 'sub' claim (%s) did not match id_token 'sub' claim (%s)", userInfoSubject, subject)
}
// Merge in userinfo claims in case id_token claims contained some that userinfo did not
for k, v := range userInfoClaims {
claims[k] = v
}
return nil
}
// fetch and decode JSON from the given UserInfo URL
func (p *Provider) fetchUserInfo(url, accessToken string) (map[string]interface{}, error) {
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
resp, err := p.Client().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Non-200 response from UserInfo: %d, WWW-Authenticate=%s", resp.StatusCode, resp.Header.Get("WWW-Authenticate"))
}
// The UserInfo Claims MUST be returned as the members of a JSON object
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return unMarshal(data)
}
func getOpenIDConfig(p *Provider, openIDAutoDiscoveryURL string) (*OpenIDConfig, error) {
res, err := p.Client().Get(openIDAutoDiscoveryURL)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
openIDConfig := &OpenIDConfig{}
err = json.Unmarshal(body, openIDConfig)
if err != nil {
return nil, err
}
return openIDConfig, nil
}
func newConfig(provider *Provider, scopes []string, openIDConfig *OpenIDConfig) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.ClientKey,
ClientSecret: provider.Secret,
RedirectURL: provider.CallbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: openIDConfig.AuthEndpoint,
TokenURL: openIDConfig.TokenEndpoint,
},
Scopes: []string{},
}
if len(scopes) > 0 {
foundOpenIDScope := false
for _, scope := range scopes {
if scope == "openid" {
foundOpenIDScope = true
}
c.Scopes = append(c.Scopes, scope)
}
if !foundOpenIDScope {
c.Scopes = append(c.Scopes, "openid")
}
} else {
c.Scopes = []string{"openid"}
}
return c
}
func getClaimValue(data map[string]interface{}, claims []string) string {
for _, claim := range claims {
if value, ok := data[claim]; ok {
if stringValue, ok := value.(string); ok && len(stringValue) > 0 {
return stringValue
}
}
}
return ""
}
func getClaimValues(data map[string]interface{}, claims []string) []string {
var result []string
for _, claim := range claims {
if value, ok := data[claim]; ok {
if stringValues, ok := value.([]interface{}); ok {
for _, stringValue := range stringValues {
if s, ok := stringValue.(string); ok && len(s) > 0 {
result = append(result, s)
}
}
}
}
}
return result
}
// decodeJWT decodes a JSON Web Token into a simple map
// http://openid.net/specs/draft-jones-json-web-token-07.html
func decodeJWT(jwt string) (map[string]interface{}, error) {
jwtParts := strings.Split(jwt, ".")
if len(jwtParts) != 3 {
return nil, errors.New("jws: invalid token received, not all parts available")
}
decodedPayload, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[1])
if err != nil {
return nil, err
}
return unMarshal(decodedPayload)
}
func unMarshal(payload []byte) (map[string]interface{}, error) {
data := make(map[string]interface{})
return data, json.NewDecoder(bytes.NewBuffer(payload)).Decode(&data)
}