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