blob: 239ec329620f681cd3cc62e5a93c051bdd41ecea [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001// Copyright 2014 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package oauth2
6
7import (
8 "context"
9 "fmt"
10 "net/http"
11 "net/url"
12 "strconv"
13 "strings"
14 "time"
15
16 "golang.org/x/oauth2/internal"
17)
18
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053019// defaultExpiryDelta determines how earlier a token should be considered
Zack Williamse940c7a2019-08-21 14:25:39 -070020// expired than its actual expiration time. It is used to avoid late
21// expirations due to client-server time mismatches.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053022const defaultExpiryDelta = 10 * time.Second
Zack Williamse940c7a2019-08-21 14:25:39 -070023
24// Token represents the credentials used to authorize
25// the requests to access protected resources on the OAuth 2.0
26// provider's backend.
27//
28// Most users of this package should not access fields of Token
29// directly. They're exported mostly for use by related packages
30// implementing derivative OAuth2 flows.
31type Token struct {
32 // AccessToken is the token that authorizes and authenticates
33 // the requests.
34 AccessToken string `json:"access_token"`
35
36 // TokenType is the type of token.
37 // The Type method returns either this or "Bearer", the default.
38 TokenType string `json:"token_type,omitempty"`
39
40 // RefreshToken is a token that's used by the application
41 // (as opposed to the user) to refresh the access token
42 // if it expires.
43 RefreshToken string `json:"refresh_token,omitempty"`
44
45 // Expiry is the optional expiration time of the access token.
46 //
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053047 // If zero, [TokenSource] implementations will reuse the same
Zack Williamse940c7a2019-08-21 14:25:39 -070048 // token forever and RefreshToken or equivalent
49 // mechanisms for that TokenSource will not be used.
50 Expiry time.Time `json:"expiry,omitempty"`
51
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053052 // ExpiresIn is the OAuth2 wire format "expires_in" field,
53 // which specifies how many seconds later the token expires,
54 // relative to an unknown time base approximately around "now".
55 // It is the application's responsibility to populate
56 // `Expiry` from `ExpiresIn` when required.
57 ExpiresIn int64 `json:"expires_in,omitempty"`
58
Zack Williamse940c7a2019-08-21 14:25:39 -070059 // raw optionally contains extra metadata from the server
60 // when updating a token.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053061 raw any
62
63 // expiryDelta is used to calculate when a token is considered
64 // expired, by subtracting from Expiry. If zero, defaultExpiryDelta
65 // is used.
66 expiryDelta time.Duration
Zack Williamse940c7a2019-08-21 14:25:39 -070067}
68
69// Type returns t.TokenType if non-empty, else "Bearer".
70func (t *Token) Type() string {
71 if strings.EqualFold(t.TokenType, "bearer") {
72 return "Bearer"
73 }
74 if strings.EqualFold(t.TokenType, "mac") {
75 return "MAC"
76 }
77 if strings.EqualFold(t.TokenType, "basic") {
78 return "Basic"
79 }
80 if t.TokenType != "" {
81 return t.TokenType
82 }
83 return "Bearer"
84}
85
86// SetAuthHeader sets the Authorization header to r using the access
87// token in t.
88//
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053089// This method is unnecessary when using [Transport] or an HTTP Client
Zack Williamse940c7a2019-08-21 14:25:39 -070090// returned by this package.
91func (t *Token) SetAuthHeader(r *http.Request) {
92 r.Header.Set("Authorization", t.Type()+" "+t.AccessToken)
93}
94
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053095// WithExtra returns a new [Token] that's a clone of t, but using the
Zack Williamse940c7a2019-08-21 14:25:39 -070096// provided raw extra map. This is only intended for use by packages
97// implementing derivative OAuth2 flows.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +053098func (t *Token) WithExtra(extra any) *Token {
Zack Williamse940c7a2019-08-21 14:25:39 -070099 t2 := new(Token)
100 *t2 = *t
101 t2.raw = extra
102 return t2
103}
104
105// Extra returns an extra field.
106// Extra fields are key-value pairs returned by the server as a
107// part of the token retrieval response.
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530108func (t *Token) Extra(key string) any {
109 if raw, ok := t.raw.(map[string]any); ok {
Zack Williamse940c7a2019-08-21 14:25:39 -0700110 return raw[key]
111 }
112
113 vals, ok := t.raw.(url.Values)
114 if !ok {
115 return nil
116 }
117
118 v := vals.Get(key)
119 switch s := strings.TrimSpace(v); strings.Count(s, ".") {
120 case 0: // Contains no "."; try to parse as int
121 if i, err := strconv.ParseInt(s, 10, 64); err == nil {
122 return i
123 }
124 case 1: // Contains a single "."; try to parse as float
125 if f, err := strconv.ParseFloat(s, 64); err == nil {
126 return f
127 }
128 }
129
130 return v
131}
132
133// timeNow is time.Now but pulled out as a variable for tests.
134var timeNow = time.Now
135
136// expired reports whether the token is expired.
137// t must be non-nil.
138func (t *Token) expired() bool {
139 if t.Expiry.IsZero() {
140 return false
141 }
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530142
143 expiryDelta := defaultExpiryDelta
144 if t.expiryDelta != 0 {
145 expiryDelta = t.expiryDelta
146 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700147 return t.Expiry.Round(0).Add(-expiryDelta).Before(timeNow())
148}
149
150// Valid reports whether t is non-nil, has an AccessToken, and is not expired.
151func (t *Token) Valid() bool {
152 return t != nil && t.AccessToken != "" && !t.expired()
153}
154
155// tokenFromInternal maps an *internal.Token struct into
156// a *Token struct.
157func tokenFromInternal(t *internal.Token) *Token {
158 if t == nil {
159 return nil
160 }
161 return &Token{
162 AccessToken: t.AccessToken,
163 TokenType: t.TokenType,
164 RefreshToken: t.RefreshToken,
165 Expiry: t.Expiry,
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530166 ExpiresIn: t.ExpiresIn,
Zack Williamse940c7a2019-08-21 14:25:39 -0700167 raw: t.Raw,
168 }
169}
170
171// retrieveToken takes a *Config and uses that to retrieve an *internal.Token.
172// This token is then mapped from *internal.Token into an *oauth2.Token which is returned along
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530173// with an error.
Zack Williamse940c7a2019-08-21 14:25:39 -0700174func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530175 tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v, internal.AuthStyle(c.Endpoint.AuthStyle), c.authStyleCache.Get())
Zack Williamse940c7a2019-08-21 14:25:39 -0700176 if err != nil {
177 if rErr, ok := err.(*internal.RetrieveError); ok {
178 return nil, (*RetrieveError)(rErr)
179 }
180 return nil, err
181 }
182 return tokenFromInternal(tk), nil
183}
184
185// RetrieveError is the error returned when the token endpoint returns a
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530186// non-2XX HTTP status code or populates RFC 6749's 'error' parameter.
187// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
Zack Williamse940c7a2019-08-21 14:25:39 -0700188type RetrieveError struct {
189 Response *http.Response
190 // Body is the body that was consumed by reading Response.Body.
191 // It may be truncated.
192 Body []byte
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530193 // ErrorCode is RFC 6749's 'error' parameter.
194 ErrorCode string
195 // ErrorDescription is RFC 6749's 'error_description' parameter.
196 ErrorDescription string
197 // ErrorURI is RFC 6749's 'error_uri' parameter.
198 ErrorURI string
Zack Williamse940c7a2019-08-21 14:25:39 -0700199}
200
201func (r *RetrieveError) Error() string {
balaji.nagarajan8a2a7ee2026-06-19 22:31:13 +0530202 if r.ErrorCode != "" {
203 s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
204 if r.ErrorDescription != "" {
205 s += fmt.Sprintf(" %q", r.ErrorDescription)
206 }
207 if r.ErrorURI != "" {
208 s += fmt.Sprintf(" %q", r.ErrorURI)
209 }
210 return s
211 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700212 return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
213}