// Copyright 2014 The Go 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 internalimport ()// Token represents the credentials used to authorize// the requests to access protected resources on the OAuth 2.0// provider's backend.//// This type is a mirror of [golang.org/x/oauth2.Token] and exists to break// an otherwise-circular dependency. Other internal packages// should convert this Token into an [golang.org/x/oauth2.Token] before use.typeTokenstruct {// AccessToken is the token that authorizes and authenticates // the requests. AccessToken string// TokenType is the type of token. // The Type method returns either this or "Bearer", the default. TokenType string// RefreshToken is a token that's used by the application // (as opposed to the user) to refresh the access token // if it expires. RefreshToken string// Expiry is the optional expiration time of the access token. // // If zero, TokenSource implementations will reuse the same // token forever and RefreshToken or equivalent // mechanisms for that TokenSource will not be used. Expiry time.Time// ExpiresIn is the OAuth2 wire format "expires_in" field, // which specifies how many seconds later the token expires, // relative to an unknown time base approximately around "now". // It is the application's responsibility to populate // `Expiry` from `ExpiresIn` when required. ExpiresIn int64`json:"expires_in,omitempty"`// Raw optionally contains extra metadata from the server // when updating a token. Raw any}// tokenJSON is the struct representing the HTTP response from OAuth2// providers returning a token or error in JSON form.// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1type tokenJSON struct { AccessToken string`json:"access_token"` TokenType string`json:"token_type"` RefreshToken string`json:"refresh_token"` ExpiresIn expirationTime`json:"expires_in"`// at least PayPal returns string, while most return number// error fields // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 ErrorCode string`json:"error"` ErrorDescription string`json:"error_description"` ErrorURI string`json:"error_uri"`}func ( *tokenJSON) () ( time.Time) {if := .ExpiresIn; != 0 {returntime.Now().Add(time.Duration() * time.Second) }return}type expirationTime int32func ( *expirationTime) ( []byte) error {iflen() == 0 || string() == "null" {returnnil }varjson.Number := json.Unmarshal(, &)if != nil {return } , := .Int64()if != nil {return }if > math.MaxInt32 { = math.MaxInt32 } * = expirationTime()returnnil}// AuthStyle is a copy of the golang.org/x/oauth2 package's AuthStyle type.typeAuthStyleintconst (AuthStyleUnknownAuthStyle = 0AuthStyleInParamsAuthStyle = 1AuthStyleInHeaderAuthStyle = 2)// LazyAuthStyleCache is a backwards compatibility compromise to let Configs// have a lazily-initialized AuthStyleCache.//// The two users of this, oauth2.Config and oauth2/clientcredentials.Config,// both would ideally just embed an unexported AuthStyleCache but because both// were historically allowed to be copied by value we can't retroactively add an// uncopyable Mutex to them.//// We could use an atomic.Pointer, but that was added recently enough (in Go// 1.18) that we'd break Go 1.17 users where the tests as of 2023-08-03// still pass. By using an atomic.Value, it supports both Go 1.17 and// copying by value, even if that's not ideal.typeLazyAuthStyleCachestruct { v atomic.Value// of *AuthStyleCache}func ( *LazyAuthStyleCache) () *AuthStyleCache {if , := .v.Load().(*AuthStyleCache); {return } := new(AuthStyleCache)if !.v.CompareAndSwap(nil, ) { = .v.Load().(*AuthStyleCache) }return}type authStyleCacheKey struct { url string clientID string}// AuthStyleCache is the set of tokenURLs we've successfully used via// RetrieveToken and which style auth we ended up using.// It's called a cache, but it doesn't (yet?) shrink. It's expected that// the set of OAuth2 servers a program contacts over time is fixed and// small.typeAuthStyleCachestruct { mu sync.Mutex m map[authStyleCacheKey]AuthStyle}// lookupAuthStyle reports which auth style we last used with tokenURL// when calling RetrieveToken and whether we have ever done so.func ( *AuthStyleCache) (, string) ( AuthStyle, bool) { .mu.Lock()defer .mu.Unlock() , = .m[authStyleCacheKey{, }]return}// setAuthStyle adds an entry to authStyleCache, documented above.func ( *AuthStyleCache) (, string, AuthStyle) { .mu.Lock()defer .mu.Unlock()if .m == nil { .m = make(map[authStyleCacheKey]AuthStyle) } .m[authStyleCacheKey{, }] = }// newTokenRequest returns a new *http.Request to retrieve a new token// from tokenURL using the provided clientID, clientSecret, and POST// body parameters.//// inParams is whether the clientID & clientSecret should be encoded// as the POST body. An 'inParams' value of true means to send it in// the POST body (along with any values in v); false means to send it// in the Authorization header.func newTokenRequest(, , string, url.Values, AuthStyle) (*http.Request, error) {if == AuthStyleInParams { = cloneURLValues()if != "" { .Set("client_id", ) }if != "" { .Set("client_secret", ) } } , := http.NewRequest("POST", , strings.NewReader(.Encode()))if != nil {returnnil, } .Header.Set("Content-Type", "application/x-www-form-urlencoded")if == AuthStyleInHeader { .SetBasicAuth(url.QueryEscape(), url.QueryEscape()) }return , nil}func cloneURLValues( url.Values) url.Values { := make(url.Values, len())for , := range { [] = append([]string(nil), ...) }return}func ( context.Context, , , string, url.Values, AuthStyle, *AuthStyleCache) (*Token, error) { := == AuthStyleUnknownif {if , := .lookupAuthStyle(, ); { = = false } else { = AuthStyleInHeader// the first way we'll try } } , := newTokenRequest(, , , , )if != nil {returnnil, } , := doTokenRoundTrip(, )if != nil && {// If we get an error, assume the server wants the // clientID & clientSecret in a different form. // See https://code.google.com/p/goauth2/issues/detail?id=31 for background. // In summary: // - Reddit only accepts client secret in the Authorization header // - Dropbox accepts either it in URL param or Auth header, but not both. // - Google only accepts URL param (not spec compliant?), not Auth header // - Stripe only accepts client secret in Auth header with Bearer method, not Basic // // We used to maintain a big table in this code of all the sites and which way // they went, but maintaining it didn't scale & got annoying. // So just try both ways. = AuthStyleInParams// the second way we'll try , _ = newTokenRequest(, , , , ) , = doTokenRoundTrip(, ) }if && == nil { .setAuthStyle(, , ) }// Don't overwrite `RefreshToken` with an empty value // if this was a token refreshing request.if != nil && .RefreshToken == "" { .RefreshToken = .Get("refresh_token") }return , }func doTokenRoundTrip( context.Context, *http.Request) (*Token, error) { , := ContextClient().Do(.WithContext())if != nil {returnnil, } , := io.ReadAll(io.LimitReader(.Body, 1<<20)) .Body.Close()if != nil {returnnil, fmt.Errorf("oauth2: cannot fetch token: %v", ) } := .StatusCode < 200 || .StatusCode > 299 := &RetrieveError{Response: ,Body: ,// attempt to populate error detail below }var *Token , , := mime.ParseMediaType(.Header.Get("Content-Type"))switch {case"application/x-www-form-urlencoded", "text/plain":// some endpoints return a query string , := url.ParseQuery(string())if != nil {if {returnnil, }returnnil, fmt.Errorf("oauth2: cannot parse response: %v", ) } .ErrorCode = .Get("error") .ErrorDescription = .Get("error_description") .ErrorURI = .Get("error_uri") = &Token{AccessToken: .Get("access_token"),TokenType: .Get("token_type"),RefreshToken: .Get("refresh_token"),Raw: , } := .Get("expires_in") , := strconv.Atoi()if != 0 { .Expiry = time.Now().Add(time.Duration() * time.Second) }default:vartokenJSONif = json.Unmarshal(, &); != nil {if {returnnil, }returnnil, fmt.Errorf("oauth2: cannot parse json: %v", ) } .ErrorCode = .ErrorCode .ErrorDescription = .ErrorDescription .ErrorURI = .ErrorURI = &Token{AccessToken: .AccessToken,TokenType: .TokenType,RefreshToken: .RefreshToken,Expiry: .expiry(),ExpiresIn: int64(.ExpiresIn),Raw: make(map[string]any), }json.Unmarshal(, &.Raw) // no error checks for optional fields }// according to spec, servers should respond status 400 in error case // https://www.rfc-editor.org/rfc/rfc6749#section-5.2 // but some unorthodox servers respond 200 in error caseif || .ErrorCode != "" {returnnil, }if .AccessToken == "" {returnnil, errors.New("oauth2: server response missing access_token") }return , nil}// mirrors oauth2.RetrieveErrortypeRetrieveErrorstruct { Response *http.Response Body []byte ErrorCode string ErrorDescription string ErrorURI string}func ( *RetrieveError) () string {if .ErrorCode != "" { := fmt.Sprintf("oauth2: %q", .ErrorCode)if .ErrorDescription != "" { += fmt.Sprintf(" %q", .ErrorDescription) }if .ErrorURI != "" { += fmt.Sprintf(" %q", .ErrorURI) }return }returnfmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", .Response.Status, .Body)}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.