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}