base.go raw
1 // Package base contains a "Base" client that is used by the external public.Client and confidential.Client.
2 // Base holds shared attributes that must be available to both clients and methods that act as
3 // shared calls.
4 package base
5
6 import (
7 "context"
8 "fmt"
9 "net/url"
10 "reflect"
11 "strings"
12 "sync"
13 "sync/atomic"
14 "time"
15
16 "github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
17 "github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
18 "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage"
19 "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
20 "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
21 "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
22 "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
23 )
24
25 const (
26 // AuthorityPublicCloud is the default AAD authority host
27 AuthorityPublicCloud = "https://login.microsoftonline.com/common"
28 scopeSeparator = " "
29 )
30
31 // manager provides an internal cache. It is defined to allow faking the cache in tests.
32 // In production it's a *storage.Manager or *storage.PartitionedManager.
33 type manager interface {
34 cache.Serializer
35 Read(context.Context, authority.AuthParams) (storage.TokenResponse, error)
36 Write(authority.AuthParams, accesstokens.TokenResponse) (shared.Account, error)
37 }
38
39 // accountManager is a manager that also caches accounts. In production it's a *storage.Manager.
40 type accountManager interface {
41 manager
42 AllAccounts() []shared.Account
43 Account(homeAccountID string) shared.Account
44 RemoveAccount(account shared.Account, clientID string)
45 }
46
47 // AcquireTokenSilentParameters contains the parameters to acquire a token silently (from cache).
48 type AcquireTokenSilentParameters struct {
49 Scopes []string
50 Account shared.Account
51 RequestType accesstokens.AppType
52 Credential *accesstokens.Credential
53 IsAppCache bool
54 TenantID string
55 UserAssertion string
56 AuthorizationType authority.AuthorizeType
57 Claims string
58 AuthnScheme authority.AuthenticationScheme
59 ExtraBodyParameters map[string]string
60 CacheKeyComponents map[string]string
61 }
62
63 // AcquireTokenAuthCodeParameters contains the parameters required to acquire an access token using the auth code flow.
64 // To use PKCE, set the CodeChallengeParameter.
65 // Code challenges are used to secure authorization code grants; for more information, visit
66 // https://tools.ietf.org/html/rfc7636.
67 type AcquireTokenAuthCodeParameters struct {
68 Scopes []string
69 Code string
70 Challenge string
71 Claims string
72 RedirectURI string
73 AppType accesstokens.AppType
74 Credential *accesstokens.Credential
75 TenantID string
76 }
77
78 type AcquireTokenOnBehalfOfParameters struct {
79 Scopes []string
80 Claims string
81 Credential *accesstokens.Credential
82 TenantID string
83 UserAssertion string
84 }
85
86 // AuthResult contains the results of one token acquisition operation in PublicClientApplication
87 // or ConfidentialClientApplication. For details see https://aka.ms/msal-net-authenticationresult
88 type AuthResult struct {
89 Account shared.Account
90 IDToken accesstokens.IDToken
91 AccessToken string
92 ExpiresOn time.Time
93 GrantedScopes []string
94 DeclinedScopes []string
95 Metadata AuthResultMetadata
96 }
97
98 // AuthResultMetadata which contains meta data for the AuthResult
99 type AuthResultMetadata struct {
100 RefreshOn time.Time
101 TokenSource TokenSource
102 }
103
104 type TokenSource int
105
106 // These are all the types of token flows.
107 const (
108 TokenSourceIdentityProvider TokenSource = 0
109 TokenSourceCache TokenSource = 1
110 )
111
112 // AuthResultFromStorage creates an AuthResult from a storage token response (which is generated from the cache).
113 func AuthResultFromStorage(storageTokenResponse storage.TokenResponse) (AuthResult, error) {
114 if err := storageTokenResponse.AccessToken.Validate(); err != nil {
115 return AuthResult{}, fmt.Errorf("problem with access token in StorageTokenResponse: %w", err)
116 }
117 account := storageTokenResponse.Account
118 accessToken := storageTokenResponse.AccessToken.Secret
119 grantedScopes := strings.Split(storageTokenResponse.AccessToken.Scopes, scopeSeparator)
120
121 // Checking if there was an ID token in the cache; this will throw an error in the case of confidential client applications.
122 var idToken accesstokens.IDToken
123 if !storageTokenResponse.IDToken.IsZero() {
124 err := idToken.UnmarshalJSON([]byte(storageTokenResponse.IDToken.Secret))
125 if err != nil {
126 return AuthResult{}, fmt.Errorf("problem decoding JWT token: %w", err)
127 }
128 }
129 return AuthResult{
130 Account: account,
131 IDToken: idToken,
132 AccessToken: accessToken,
133 ExpiresOn: storageTokenResponse.AccessToken.ExpiresOn.T,
134 GrantedScopes: grantedScopes,
135 DeclinedScopes: nil,
136 Metadata: AuthResultMetadata{
137 TokenSource: TokenSourceCache,
138 RefreshOn: storageTokenResponse.AccessToken.RefreshOn.T,
139 },
140 }, nil
141 }
142
143 // NewAuthResult creates an AuthResult.
144 func NewAuthResult(tokenResponse accesstokens.TokenResponse, account shared.Account) (AuthResult, error) {
145 if len(tokenResponse.DeclinedScopes) > 0 {
146 return AuthResult{}, fmt.Errorf("token response failed because declined scopes are present: %s", strings.Join(tokenResponse.DeclinedScopes, ","))
147 }
148 return AuthResult{
149 Account: account,
150 IDToken: tokenResponse.IDToken,
151 AccessToken: tokenResponse.AccessToken,
152 ExpiresOn: tokenResponse.ExpiresOn,
153 GrantedScopes: tokenResponse.GrantedScopes.Slice,
154 Metadata: AuthResultMetadata{
155 TokenSource: TokenSourceIdentityProvider,
156 RefreshOn: tokenResponse.RefreshOn.T,
157 },
158 }, nil
159 }
160
161 // Client is a base client that provides access to common methods and primatives that
162 // can be used by multiple clients.
163 type Client struct {
164 Token *oauth.Client
165 manager accountManager // *storage.Manager or fakeManager in tests
166 // pmanager is a partitioned cache for OBO authentication. *storage.PartitionedManager or fakeManager in tests
167 pmanager manager
168
169 AuthParams authority.AuthParams // DO NOT EVER MAKE THIS A POINTER! See "Note" in New().
170 cacheAccessor cache.ExportReplace
171 cacheAccessorMu *sync.RWMutex
172 canRefresh map[string]*atomic.Value
173 canRefreshMu *sync.Mutex
174 }
175
176 // Option is an optional argument to the New constructor.
177 type Option func(c *Client) error
178
179 // WithCacheAccessor allows you to set some type of cache for storing authentication tokens.
180 func WithCacheAccessor(ca cache.ExportReplace) Option {
181 return func(c *Client) error {
182 if ca != nil {
183 c.cacheAccessor = ca
184 }
185 return nil
186 }
187 }
188
189 // WithClientCapabilities allows configuring one or more client capabilities such as "CP1"
190 func WithClientCapabilities(capabilities []string) Option {
191 return func(c *Client) error {
192 var err error
193 if len(capabilities) > 0 {
194 cc, err := authority.NewClientCapabilities(capabilities)
195 if err == nil {
196 c.AuthParams.Capabilities = cc
197 }
198 }
199 return err
200 }
201 }
202
203 // WithKnownAuthorityHosts specifies hosts Client shouldn't validate or request metadata for because they're known to the user
204 func WithKnownAuthorityHosts(hosts []string) Option {
205 return func(c *Client) error {
206 cp := make([]string, len(hosts))
207 copy(cp, hosts)
208 c.AuthParams.KnownAuthorityHosts = cp
209 return nil
210 }
211 }
212
213 // WithX5C specifies if x5c claim(public key of the certificate) should be sent to STS to enable Subject Name Issuer Authentication.
214 func WithX5C(sendX5C bool) Option {
215 return func(c *Client) error {
216 c.AuthParams.SendX5C = sendX5C
217 return nil
218 }
219 }
220
221 func WithRegionDetection(region string) Option {
222 return func(c *Client) error {
223 c.AuthParams.AuthorityInfo.Region = region
224 return nil
225 }
226 }
227
228 func WithInstanceDiscovery(instanceDiscoveryEnabled bool) Option {
229 return func(c *Client) error {
230 c.AuthParams.AuthorityInfo.ValidateAuthority = instanceDiscoveryEnabled
231 c.AuthParams.AuthorityInfo.InstanceDiscoveryDisabled = !instanceDiscoveryEnabled
232 return nil
233 }
234 }
235
236 // New is the constructor for Base.
237 func New(clientID string, authorityURI string, token *oauth.Client, options ...Option) (Client, error) {
238 //By default, validateAuthority is set to true and instanceDiscoveryDisabled is set to false
239 authInfo, err := authority.NewInfoFromAuthorityURI(authorityURI, true, false)
240 if err != nil {
241 return Client{}, err
242 }
243 authParams := authority.NewAuthParams(clientID, authInfo)
244 client := Client{ // Note: Hey, don't even THINK about making Base into *Base. See "design notes" in public.go and confidential.go
245 Token: token,
246 AuthParams: authParams,
247 cacheAccessorMu: &sync.RWMutex{},
248 manager: storage.New(token),
249 pmanager: storage.NewPartitionedManager(token),
250 canRefresh: make(map[string]*atomic.Value),
251 canRefreshMu: &sync.Mutex{},
252 }
253 for _, o := range options {
254 if err = o(&client); err != nil {
255 break
256 }
257 }
258 return client, err
259
260 }
261
262 // AuthCodeURL creates a URL used to acquire an authorization code.
263 func (b Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, authParams authority.AuthParams) (string, error) {
264 endpoints, err := b.Token.ResolveEndpoints(ctx, authParams.AuthorityInfo, "")
265 if err != nil {
266 return "", err
267 }
268
269 baseURL, err := url.Parse(endpoints.AuthorizationEndpoint)
270 if err != nil {
271 return "", err
272 }
273
274 claims, err := authParams.MergeCapabilitiesAndClaims()
275 if err != nil {
276 return "", err
277 }
278
279 v := url.Values{}
280 v.Add("client_id", clientID)
281 v.Add("response_type", "code")
282 v.Add("redirect_uri", redirectURI)
283 v.Add("scope", strings.Join(scopes, scopeSeparator))
284 if authParams.State != "" {
285 v.Add("state", authParams.State)
286 }
287 if claims != "" {
288 v.Add("claims", claims)
289 }
290 if authParams.CodeChallenge != "" {
291 v.Add("code_challenge", authParams.CodeChallenge)
292 }
293 if authParams.CodeChallengeMethod != "" {
294 v.Add("code_challenge_method", authParams.CodeChallengeMethod)
295 }
296 if authParams.LoginHint != "" {
297 v.Add("login_hint", authParams.LoginHint)
298 }
299 if authParams.Prompt != "" {
300 v.Add("prompt", authParams.Prompt)
301 }
302 if authParams.DomainHint != "" {
303 v.Add("domain_hint", authParams.DomainHint)
304 }
305 // There were left over from an implementation that didn't use any of these. We may
306 // need to add them later, but as of now aren't needed.
307 /*
308 if p.ResponseMode != "" {
309 urlParams.Add("response_mode", p.ResponseMode)
310 }
311 */
312 baseURL.RawQuery = v.Encode()
313 return baseURL.String(), nil
314 }
315
316 func (b Client) AcquireTokenSilent(ctx context.Context, silent AcquireTokenSilentParameters) (AuthResult, error) {
317 ar := AuthResult{}
318 // when tenant == "", the caller didn't specify a tenant and WithTenant will choose the client's configured tenant
319 tenant := silent.TenantID
320 authParams, err := b.AuthParams.WithTenant(tenant)
321 if err != nil {
322 return ar, err
323 }
324 authParams.Scopes = silent.Scopes
325 authParams.HomeAccountID = silent.Account.HomeAccountID
326 authParams.AuthorizationType = silent.AuthorizationType
327 authParams.Claims = silent.Claims
328 authParams.UserAssertion = silent.UserAssertion
329 if silent.AuthnScheme != nil {
330 authParams.AuthnScheme = silent.AuthnScheme
331 }
332 if silent.CacheKeyComponents != nil {
333 authParams.CacheKeyComponents = silent.CacheKeyComponents
334 }
335 if silent.ExtraBodyParameters != nil {
336 authParams.ExtraBodyParameters = silent.ExtraBodyParameters
337 }
338 m := b.pmanager
339 if authParams.AuthorizationType != authority.ATOnBehalfOf {
340 authParams.AuthorizationType = authority.ATRefreshToken
341 m = b.manager
342 }
343 if b.cacheAccessor != nil {
344 key := authParams.CacheKey(silent.IsAppCache)
345 b.cacheAccessorMu.RLock()
346 err = b.cacheAccessor.Replace(ctx, m, cache.ReplaceHints{PartitionKey: key})
347 b.cacheAccessorMu.RUnlock()
348 }
349 if err != nil {
350 return ar, err
351 }
352 storageTokenResponse, err := m.Read(ctx, authParams)
353 if err != nil {
354 return ar, err
355 }
356
357 // ignore cached access tokens when given claims
358 if silent.Claims == "" {
359 ar, err = AuthResultFromStorage(storageTokenResponse)
360 if err == nil {
361 if rt := storageTokenResponse.AccessToken.RefreshOn.T; !rt.IsZero() && Now().After(rt) {
362 b.canRefreshMu.Lock()
363 refreshValue, ok := b.canRefresh[tenant]
364 if !ok {
365 refreshValue = &atomic.Value{}
366 refreshValue.Store(false)
367 b.canRefresh[tenant] = refreshValue
368 }
369 b.canRefreshMu.Unlock()
370 if refreshValue.CompareAndSwap(false, true) {
371 defer refreshValue.Store(false)
372 // Added a check to see if the token is still same because there is a chance
373 // that the token is already refreshed by another thread.
374 // If the token is not same, we don't need to refresh it.
375 // Which means it refreshed.
376 if str, err := m.Read(ctx, authParams); err == nil && str.AccessToken.Secret == ar.AccessToken {
377 switch silent.RequestType {
378 case accesstokens.ATConfidential:
379 if tr, er := b.Token.Credential(ctx, authParams, silent.Credential); er == nil {
380 return b.AuthResultFromToken(ctx, authParams, tr)
381 }
382 case accesstokens.ATPublic:
383 token, err := b.Token.Refresh(ctx, silent.RequestType, authParams, silent.Credential, storageTokenResponse.RefreshToken)
384 if err != nil {
385 return ar, err
386 }
387 return b.AuthResultFromToken(ctx, authParams, token)
388 case accesstokens.ATUnknown:
389 return ar, errors.New("silent request type cannot be ATUnknown")
390 }
391 }
392 }
393 }
394 ar.AccessToken, err = authParams.AuthnScheme.FormatAccessToken(ar.AccessToken)
395 return ar, err
396 }
397 }
398
399 // redeem a cached refresh token, if available
400 if reflect.ValueOf(storageTokenResponse.RefreshToken).IsZero() {
401 return ar, errors.New("no token found")
402 }
403 var cc *accesstokens.Credential
404 if silent.RequestType == accesstokens.ATConfidential {
405 cc = silent.Credential
406 }
407 token, err := b.Token.Refresh(ctx, silent.RequestType, authParams, cc, storageTokenResponse.RefreshToken)
408 if err != nil {
409 return ar, err
410 }
411 return b.AuthResultFromToken(ctx, authParams, token)
412 }
413
414 func (b Client) AcquireTokenByAuthCode(ctx context.Context, authCodeParams AcquireTokenAuthCodeParameters) (AuthResult, error) {
415 authParams, err := b.AuthParams.WithTenant(authCodeParams.TenantID)
416 if err != nil {
417 return AuthResult{}, err
418 }
419 authParams.Claims = authCodeParams.Claims
420 authParams.Scopes = authCodeParams.Scopes
421 authParams.Redirecturi = authCodeParams.RedirectURI
422 authParams.AuthorizationType = authority.ATAuthCode
423
424 var cc *accesstokens.Credential
425 if authCodeParams.AppType == accesstokens.ATConfidential {
426 cc = authCodeParams.Credential
427 authParams.IsConfidentialClient = true
428 }
429
430 req, err := accesstokens.NewCodeChallengeRequest(authParams, authCodeParams.AppType, cc, authCodeParams.Code, authCodeParams.Challenge)
431 if err != nil {
432 return AuthResult{}, err
433 }
434
435 token, err := b.Token.AuthCode(ctx, req)
436 if err != nil {
437 return AuthResult{}, err
438 }
439
440 return b.AuthResultFromToken(ctx, authParams, token)
441 }
442
443 // AcquireTokenOnBehalfOf acquires a security token for an app using middle tier apps access token.
444 func (b Client) AcquireTokenOnBehalfOf(ctx context.Context, onBehalfOfParams AcquireTokenOnBehalfOfParameters) (AuthResult, error) {
445 var ar AuthResult
446 silentParameters := AcquireTokenSilentParameters{
447 Scopes: onBehalfOfParams.Scopes,
448 RequestType: accesstokens.ATConfidential,
449 Credential: onBehalfOfParams.Credential,
450 UserAssertion: onBehalfOfParams.UserAssertion,
451 AuthorizationType: authority.ATOnBehalfOf,
452 TenantID: onBehalfOfParams.TenantID,
453 Claims: onBehalfOfParams.Claims,
454 }
455 ar, err := b.AcquireTokenSilent(ctx, silentParameters)
456 if err == nil {
457 return ar, err
458 }
459 authParams, err := b.AuthParams.WithTenant(onBehalfOfParams.TenantID)
460 if err != nil {
461 return AuthResult{}, err
462 }
463 authParams.AuthorizationType = authority.ATOnBehalfOf
464 authParams.Claims = onBehalfOfParams.Claims
465 authParams.Scopes = onBehalfOfParams.Scopes
466 authParams.UserAssertion = onBehalfOfParams.UserAssertion
467 if authParams.ExtraBodyParameters != nil {
468 authParams.ExtraBodyParameters = silentParameters.ExtraBodyParameters
469 }
470 token, err := b.Token.OnBehalfOf(ctx, authParams, onBehalfOfParams.Credential)
471 if err == nil {
472 ar, err = b.AuthResultFromToken(ctx, authParams, token)
473 }
474 return ar, err
475 }
476
477 func (b Client) AuthResultFromToken(ctx context.Context, authParams authority.AuthParams, token accesstokens.TokenResponse) (AuthResult, error) {
478 var m manager = b.manager
479 if authParams.AuthorizationType == authority.ATOnBehalfOf {
480 m = b.pmanager
481 }
482 key := token.CacheKey(authParams)
483 if b.cacheAccessor != nil {
484 b.cacheAccessorMu.Lock()
485 defer b.cacheAccessorMu.Unlock()
486 err := b.cacheAccessor.Replace(ctx, m, cache.ReplaceHints{PartitionKey: key})
487 if err != nil {
488 return AuthResult{}, err
489 }
490 }
491 account, err := m.Write(authParams, token)
492 if err != nil {
493 return AuthResult{}, err
494 }
495 ar, err := NewAuthResult(token, account)
496 if err == nil && b.cacheAccessor != nil {
497 err = b.cacheAccessor.Export(ctx, b.manager, cache.ExportHints{PartitionKey: key})
498 }
499 if err != nil {
500 return AuthResult{}, err
501 }
502
503 ar.AccessToken, err = authParams.AuthnScheme.FormatAccessToken(ar.AccessToken)
504 return ar, err
505 }
506
507 // This function wraps time.Now() and is used for refreshing the application
508 // was created to test the function against refreshin
509 var Now = time.Now
510
511 func (b Client) AllAccounts(ctx context.Context) ([]shared.Account, error) {
512 if b.cacheAccessor != nil {
513 b.cacheAccessorMu.RLock()
514 defer b.cacheAccessorMu.RUnlock()
515 key := b.AuthParams.CacheKey(false)
516 err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
517 if err != nil {
518 return nil, err
519 }
520 }
521 return b.manager.AllAccounts(), nil
522 }
523
524 func (b Client) Account(ctx context.Context, homeAccountID string) (shared.Account, error) {
525 if b.cacheAccessor != nil {
526 b.cacheAccessorMu.RLock()
527 defer b.cacheAccessorMu.RUnlock()
528 authParams := b.AuthParams // This is a copy, as we don't have a pointer receiver and .AuthParams is not a pointer.
529 authParams.AuthorizationType = authority.AccountByID
530 authParams.HomeAccountID = homeAccountID
531 key := b.AuthParams.CacheKey(false)
532 err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
533 if err != nil {
534 return shared.Account{}, err
535 }
536 }
537 return b.manager.Account(homeAccountID), nil
538 }
539
540 // RemoveAccount removes all the ATs, RTs and IDTs from the cache associated with this account.
541 func (b Client) RemoveAccount(ctx context.Context, account shared.Account) error {
542 if b.cacheAccessor == nil {
543 b.manager.RemoveAccount(account, b.AuthParams.ClientID)
544 return nil
545 }
546 b.cacheAccessorMu.Lock()
547 defer b.cacheAccessorMu.Unlock()
548 key := b.AuthParams.CacheKey(false)
549 err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
550 if err != nil {
551 return err
552 }
553 b.manager.RemoveAccount(account, b.AuthParams.ClientID)
554 return b.cacheAccessor.Export(ctx, b.manager, cache.ExportHints{PartitionKey: key})
555 }
556