forked from AzureAD/microsoft-authentication-library-for-go
/
accesstokens.go
423 lines (362 loc) · 14.1 KB
/
accesstokens.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
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
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/*
Package accesstokens exposes a REST client for querying backend systems to get various types of
access tokens (oauth) for use in authentication.
These calls are of type "application/x-www-form-urlencoded". This means we use url.Values to
represent arguments and then encode them into the POST body message. We receive JSON in
return for the requests. The request definition is defined in https://tools.ietf.org/html/rfc7521#section-4.2 .
*/
package accesstokens
import (
"context"
"crypto"
/* #nosec */
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"fmt"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/doruk-gercel/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/doruk-gercel/microsoft-authentication-library-for-go/apps/internal/oauth/ops/internal/grant"
"github.com/doruk-gercel/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust"
"github.com/golang-jwt/jwt"
"github.com/google/uuid"
)
const (
grantType = "grant_type"
deviceCode = "device_code"
clientID = "client_id"
clientInfo = "client_info"
clientInfoVal = "1"
username = "username"
password = "password"
)
// assertionLifetime allows tests to control the expiration time of JWT assertions created by Credential.
var assertionLifetime = 10 * time.Minute
//go:generate stringer -type=AppType
// AppType is whether the authorization code flow is for a public or confidential client.
type AppType int8
const (
// ATUnknown is the zero value when the type hasn't been set.
ATUnknown AppType = iota
// ATPublic indicates this if for the Public.Client.
ATPublic
// ATConfidential indicates this if for the Confidential.Client.
ATConfidential
)
type urlFormCaller interface {
URLFormCall(ctx context.Context, endpoint string, qv url.Values, resp interface{}) error
}
// DeviceCodeResponse represents the HTTP response received from the device code endpoint
type DeviceCodeResponse struct {
authority.OAuthResponseBase
UserCode string `json:"user_code"`
DeviceCode string `json:"device_code"`
VerificationURL string `json:"verification_url"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
Message string `json:"message"`
AdditionalFields map[string]interface{}
}
// Convert converts the DeviceCodeResponse to a DeviceCodeResult
func (dcr DeviceCodeResponse) Convert(clientID string, scopes []string) DeviceCodeResult {
expiresOn := time.Now().UTC().Add(time.Duration(dcr.ExpiresIn) * time.Second)
return NewDeviceCodeResult(dcr.UserCode, dcr.DeviceCode, dcr.VerificationURL, expiresOn, dcr.Interval, dcr.Message, clientID, scopes)
}
// Credential represents the credential used in confidential client flows. This can be either
// a Secret or Cert/Key.
type Credential struct {
// Secret contains the credential secret if we are doing auth by secret.
Secret string
// Cert is the public x509 certificate if we are doing any auth other than secret.
Cert *x509.Certificate
// Key is the private key for signing if we are doing any auth other than secret.
Key crypto.PrivateKey
// mu protects everything below.
mu sync.Mutex
// Assertion is the signed JWT assertion if we have retrieved it or if it was passed.
Assertion string
// Expires is when the Assertion expires. Public to allow faking in tests.
// Any use outside msal is not supported by a compatibility promise.
Expires time.Time
}
// JWT gets the jwt assertion when the credential is not using a secret.
func (c *Credential) JWT(authParams authority.AuthParams) (string, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Expires.After(time.Now()) {
return c.Assertion, nil
} else if c.Cert == nil || c.Key == nil {
// The assertion has expired and this Credential can't generate a new one. The assertion
// was presumably provided by the application via confidential.NewCredFromAssertion(). We
// return it despite its expiration to maintain the behavior of previous versions, and
// because there's no API enabling the application to replace the assertion
// (see https://github.com/AzureAD/microsoft-authentication-library-for-go/issues/292).
return c.Assertion, nil
}
expires := time.Now().Add(assertionLifetime)
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"aud": authParams.Endpoints.TokenEndpoint,
"exp": strconv.FormatInt(expires.Unix(), 10),
"iss": authParams.ClientID,
"jti": uuid.New().String(),
"nbf": strconv.FormatInt(time.Now().Unix(), 10),
"sub": authParams.ClientID,
})
token.Header = map[string]interface{}{
"alg": "RS256",
"typ": "JWT",
"x5t": base64.StdEncoding.EncodeToString(thumbprint(c.Cert)),
}
if authParams.SendX5C {
token.Header["x5c"] = []string{base64.StdEncoding.EncodeToString(c.Cert.Raw)}
}
var err error
c.Assertion, err = token.SignedString(c.Key)
if err != nil {
return "", fmt.Errorf("unable to sign a JWT token using private key: %w", err)
}
c.Expires = expires
return c.Assertion, nil
}
// thumbprint runs the asn1.Der bytes through sha1 for use in the x5t parameter of JWT.
// https://tools.ietf.org/html/rfc7517#section-4.8
func thumbprint(cert *x509.Certificate) []byte {
/* #nosec */
a := sha1.Sum(cert.Raw)
return a[:]
}
// Client represents the REST calls to get tokens from token generator backends.
type Client struct {
// Comm provides the HTTP transport client.
Comm urlFormCaller
testing bool
}
// FromUsernamePassword uses a username and password to get an access token.
func (c Client) FromUsernamePassword(ctx context.Context, authParameters authority.AuthParams) (TokenResponse, error) {
qv := url.Values{}
qv.Set(grantType, grant.Password)
qv.Set(username, authParameters.Username)
qv.Set(password, authParameters.Password)
qv.Set(clientID, authParameters.ClientID)
qv.Set(clientInfo, clientInfoVal)
addScopeQueryParam(qv, authParameters)
return c.doTokenResp(ctx, authParameters, qv)
}
// AuthCodeRequest stores the values required to request a token from the authority using an authorization code
type AuthCodeRequest struct {
AuthParams authority.AuthParams
Code string
CodeChallenge string
Credential *Credential
AppType AppType
}
// NewCodeChallengeRequest returns an AuthCodeRequest that uses a code challenge..
func NewCodeChallengeRequest(params authority.AuthParams, appType AppType, cc *Credential, code, challenge string) (AuthCodeRequest, error) {
if appType == ATUnknown {
return AuthCodeRequest{}, fmt.Errorf("bug: NewCodeChallengeRequest() called with AppType == ATUnknown")
}
return AuthCodeRequest{
AuthParams: params,
AppType: appType,
Code: code,
CodeChallenge: challenge,
Credential: cc,
}, nil
}
// FromAuthCode uses an authorization code to retrieve an access token.
func (c Client) FromAuthCode(ctx context.Context, req AuthCodeRequest) (TokenResponse, error) {
var qv url.Values
switch req.AppType {
case ATUnknown:
return TokenResponse{}, fmt.Errorf("bug: Token.AuthCode() received request with AppType == ATUnknown")
case ATConfidential:
var err error
if req.Credential == nil {
return TokenResponse{}, fmt.Errorf("AuthCodeRequest had nil Credential for Confidential app")
}
qv, err = prepURLVals(req.Credential, req.AuthParams)
if err != nil {
return TokenResponse{}, err
}
case ATPublic:
qv = url.Values{}
default:
return TokenResponse{}, fmt.Errorf("bug: Token.AuthCode() received request with AppType == %v, which we do not recongnize", req.AppType)
}
qv.Set(grantType, grant.AuthCode)
qv.Set("code", req.Code)
qv.Set("code_verifier", req.CodeChallenge)
qv.Set("redirect_uri", req.AuthParams.Redirecturi)
qv.Set(clientID, req.AuthParams.ClientID)
qv.Set(clientInfo, clientInfoVal)
addScopeQueryParam(qv, req.AuthParams)
return c.doTokenResp(ctx, req.AuthParams, qv)
}
// FromRefreshToken uses a refresh token (for refreshing credentials) to get a new access token.
func (c Client) FromRefreshToken(ctx context.Context, appType AppType, authParams authority.AuthParams, cc *Credential, refreshToken string) (TokenResponse, error) {
qv := url.Values{}
if appType == ATConfidential {
var err error
qv, err = prepURLVals(cc, authParams)
if err != nil {
return TokenResponse{}, err
}
}
qv.Set(grantType, grant.RefreshToken)
qv.Set(clientID, authParams.ClientID)
qv.Set(clientInfo, clientInfoVal)
qv.Set("refresh_token", refreshToken)
addScopeQueryParam(qv, authParams)
return c.doTokenResp(ctx, authParams, qv)
}
// FromClientSecret uses a client's secret (aka password) to get a new token.
func (c Client) FromClientSecret(ctx context.Context, authParameters authority.AuthParams, clientSecret string) (TokenResponse, error) {
qv := url.Values{}
qv.Set(grantType, grant.ClientCredential)
qv.Set("client_secret", clientSecret)
qv.Set(clientID, authParameters.ClientID)
addScopeQueryParam(qv, authParameters)
token, err := c.doTokenResp(ctx, authParameters, qv)
if err != nil {
return token, fmt.Errorf("FromClientSecret(): %w", err)
}
return token, nil
}
func (c Client) FromAssertion(ctx context.Context, authParameters authority.AuthParams, assertion string) (TokenResponse, error) {
qv := url.Values{}
qv.Set(grantType, grant.ClientCredential)
qv.Set("client_assertion_type", grant.ClientAssertion)
qv.Set("client_assertion", assertion)
qv.Set(clientID, authParameters.ClientID)
qv.Set(clientInfo, clientInfoVal)
addScopeQueryParam(qv, authParameters)
token, err := c.doTokenResp(ctx, authParameters, qv)
if err != nil {
return token, fmt.Errorf("FromAssertion(): %w", err)
}
return token, nil
}
func (c Client) FromUserAssertionClientSecret(ctx context.Context, authParameters authority.AuthParams, userAssertion string, clientSecret string) (TokenResponse, error) {
qv := url.Values{}
qv.Set(grantType, grant.JWT)
qv.Set(clientID, authParameters.ClientID)
qv.Set("client_secret", clientSecret)
qv.Set("assertion", userAssertion)
qv.Set(clientInfo, clientInfoVal)
qv.Set("requested_token_use", "on_behalf_of")
addScopeQueryParam(qv, authParameters)
return c.doTokenResp(ctx, authParameters, qv)
}
func (c Client) FromUserAssertionClientCertificate(ctx context.Context, authParameters authority.AuthParams, userAssertion string, assertion string) (TokenResponse, error) {
qv := url.Values{}
qv.Set(grantType, grant.JWT)
qv.Set("client_assertion_type", grant.ClientAssertion)
qv.Set("client_assertion", assertion)
qv.Set(clientID, authParameters.ClientID)
qv.Set("assertion", userAssertion)
qv.Set(clientInfo, clientInfoVal)
qv.Set("requested_token_use", "on_behalf_of")
addScopeQueryParam(qv, authParameters)
return c.doTokenResp(ctx, authParameters, qv)
}
func (c Client) DeviceCodeResult(ctx context.Context, authParameters authority.AuthParams) (DeviceCodeResult, error) {
qv := url.Values{}
qv.Set(clientID, authParameters.ClientID)
addScopeQueryParam(qv, authParameters)
endpoint := strings.Replace(authParameters.Endpoints.TokenEndpoint, "token", "devicecode", -1)
resp := DeviceCodeResponse{}
err := c.Comm.URLFormCall(ctx, endpoint, qv, &resp)
if err != nil {
return DeviceCodeResult{}, err
}
return resp.Convert(authParameters.ClientID, authParameters.Scopes), nil
}
func (c Client) FromDeviceCodeResult(ctx context.Context, authParameters authority.AuthParams, deviceCodeResult DeviceCodeResult) (TokenResponse, error) {
qv := url.Values{}
qv.Set(grantType, grant.DeviceCode)
qv.Set(deviceCode, deviceCodeResult.DeviceCode)
qv.Set(clientID, authParameters.ClientID)
qv.Set(clientInfo, clientInfoVal)
addScopeQueryParam(qv, authParameters)
return c.doTokenResp(ctx, authParameters, qv)
}
func (c Client) FromSamlGrant(ctx context.Context, authParameters authority.AuthParams, samlGrant wstrust.SamlTokenInfo) (TokenResponse, error) {
qv := url.Values{}
qv.Set(username, authParameters.Username)
qv.Set(password, authParameters.Password)
qv.Set(clientID, authParameters.ClientID)
qv.Set(clientInfo, clientInfoVal)
qv.Set("assertion", base64.StdEncoding.WithPadding(base64.StdPadding).EncodeToString([]byte(samlGrant.Assertion)))
addScopeQueryParam(qv, authParameters)
switch samlGrant.AssertionType {
case grant.SAMLV1:
qv.Set(grantType, grant.SAMLV1)
case grant.SAMLV2:
qv.Set(grantType, grant.SAMLV2)
default:
return TokenResponse{}, fmt.Errorf("GetAccessTokenFromSamlGrant returned unknown SAML assertion type: %q", samlGrant.AssertionType)
}
return c.doTokenResp(ctx, authParameters, qv)
}
func (c Client) doTokenResp(ctx context.Context, authParams authority.AuthParams, qv url.Values) (TokenResponse, error) {
resp := TokenResponse{}
err := c.Comm.URLFormCall(ctx, authParams.Endpoints.TokenEndpoint, qv, &resp)
if err != nil {
return resp, err
}
resp.ComputeScope(authParams)
if c.testing {
return resp, nil
}
return resp, resp.Validate()
}
// prepURLVals returns an url.Values that sets various key/values if we are doing secrets
// or JWT assertions.
func prepURLVals(cc *Credential, authParams authority.AuthParams) (url.Values, error) {
params := url.Values{}
if cc.Secret != "" {
params.Set("client_secret", cc.Secret)
return params, nil
}
jwt, err := cc.JWT(authParams)
if err != nil {
return nil, err
}
params.Set("client_assertion", jwt)
params.Set("client_assertion_type", grant.ClientAssertion)
return params, nil
}
// openid required to get an id token
// offline_access required to get a refresh token
// profile required to get the client_info field back
var detectDefaultScopes = map[string]bool{
"openid": true,
"offline_access": true,
"profile": true,
}
var defaultScopes = []string{"openid", "offline_access", "profile"}
func AppendDefaultScopes(authParameters authority.AuthParams) []string {
scopes := make([]string, 0, len(authParameters.Scopes)+len(defaultScopes))
for _, scope := range authParameters.Scopes {
s := strings.TrimSpace(scope)
if s == "" {
continue
}
if detectDefaultScopes[scope] {
continue
}
scopes = append(scopes, scope)
}
scopes = append(scopes, defaultScopes...)
return scopes
}
func addScopeQueryParam(queryParams url.Values, authParameters authority.AuthParams) {
scopes := AppendDefaultScopes(authParameters)
queryParams.Set("scope", strings.Join(scopes, " "))
}