confidential.go

  1// Copyright (c) Microsoft Corporation.
  2// Licensed under the MIT license.
  3
  4/*
  5Package confidential provides a client for authentication of "confidential" applications.
  6A "confidential" application is defined as an app that run on servers. They are considered
  7difficult to access and for that reason capable of keeping an application secret.
  8Confidential clients can hold configuration-time secrets.
  9*/
 10package confidential
 11
 12import (
 13	"context"
 14	"crypto"
 15	"crypto/rsa"
 16	"crypto/x509"
 17	"encoding/base64"
 18	"encoding/pem"
 19	"errors"
 20	"fmt"
 21
 22	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
 23	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base"
 24	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/exported"
 25	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
 26	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
 27	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
 28	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
 29	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/options"
 30	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
 31)
 32
 33/*
 34Design note:
 35
 36confidential.Client uses base.Client as an embedded type. base.Client statically assigns its attributes
 37during creation. As it doesn't have any pointers in it, anything borrowed from it, such as
 38Base.AuthParams is a copy that is free to be manipulated here.
 39
 40Duplicate Calls shared between public.Client and this package:
 41There is some duplicate call options provided here that are the same as in public.Client . This
 42is a design choices. Go proverb(https://www.youtube.com/watch?v=PAAkCSZUG1c&t=9m28s):
 43"a little copying is better than a little dependency". Yes, we could have another package with
 44shared options (fail).  That divides like 2 options from all others which makes the user look
 45through more docs.  We can have all clients in one package, but I think separate packages
 46here makes for better naming (public.Client vs client.PublicClient).  So I chose a little
 47duplication.
 48
 49.Net People, Take note on X509:
 50This uses x509.Certificates and private keys. x509 does not store private keys. .Net
 51has a x509.Certificate2 abstraction that has private keys, but that just a strange invention.
 52As such I've put a PEM decoder into here.
 53*/
 54
 55// TODO(msal): This should have example code for each method on client using Go's example doc framework.
 56// base usage details should be include in the package documentation.
 57
 58// AuthResult contains the results of one token acquisition operation.
 59// For details see https://aka.ms/msal-net-authenticationresult
 60type AuthResult = base.AuthResult
 61
 62type AuthenticationScheme = authority.AuthenticationScheme
 63
 64type Account = shared.Account
 65
 66// CertFromPEM converts a PEM file (.pem or .key) for use with [NewCredFromCert]. The file
 67// must contain the public certificate and the private key. If a PEM block is encrypted and
 68// password is not an empty string, it attempts to decrypt the PEM blocks using the password.
 69// Multiple certs are due to certificate chaining for use cases like TLS that sign from root to leaf.
 70func CertFromPEM(pemData []byte, password string) ([]*x509.Certificate, crypto.PrivateKey, error) {
 71	var certs []*x509.Certificate
 72	var priv crypto.PrivateKey
 73	for {
 74		block, rest := pem.Decode(pemData)
 75		if block == nil {
 76			break
 77		}
 78
 79		//nolint:staticcheck // x509.IsEncryptedPEMBlock and x509.DecryptPEMBlock are deprecated. They are used here only to support a usecase.
 80		if x509.IsEncryptedPEMBlock(block) {
 81			b, err := x509.DecryptPEMBlock(block, []byte(password))
 82			if err != nil {
 83				return nil, nil, fmt.Errorf("could not decrypt encrypted PEM block: %v", err)
 84			}
 85			block, _ = pem.Decode(b)
 86			if block == nil {
 87				return nil, nil, fmt.Errorf("encounter encrypted PEM block that did not decode")
 88			}
 89		}
 90
 91		switch block.Type {
 92		case "CERTIFICATE":
 93			cert, err := x509.ParseCertificate(block.Bytes)
 94			if err != nil {
 95				return nil, nil, fmt.Errorf("block labelled 'CERTIFICATE' could not be parsed by x509: %v", err)
 96			}
 97			certs = append(certs, cert)
 98		case "PRIVATE KEY":
 99			if priv != nil {
100				return nil, nil, errors.New("found multiple private key blocks")
101			}
102
103			var err error
104			priv, err = x509.ParsePKCS8PrivateKey(block.Bytes)
105			if err != nil {
106				return nil, nil, fmt.Errorf("could not decode private key: %v", err)
107			}
108		case "RSA PRIVATE KEY":
109			if priv != nil {
110				return nil, nil, errors.New("found multiple private key blocks")
111			}
112			var err error
113			priv, err = x509.ParsePKCS1PrivateKey(block.Bytes)
114			if err != nil {
115				return nil, nil, fmt.Errorf("could not decode private key: %v", err)
116			}
117		}
118		pemData = rest
119	}
120
121	if len(certs) == 0 {
122		return nil, nil, fmt.Errorf("no certificates found")
123	}
124
125	if priv == nil {
126		return nil, nil, fmt.Errorf("no private key found")
127	}
128
129	return certs, priv, nil
130}
131
132// AssertionRequestOptions has required information for client assertion claims
133type AssertionRequestOptions = exported.AssertionRequestOptions
134
135// Credential represents the credential used in confidential client flows.
136type Credential struct {
137	secret string
138
139	cert *x509.Certificate
140	key  crypto.PrivateKey
141	x5c  []string
142
143	assertionCallback func(context.Context, AssertionRequestOptions) (string, error)
144
145	tokenProvider func(context.Context, TokenProviderParameters) (TokenProviderResult, error)
146}
147
148// toInternal returns the accesstokens.Credential that is used internally. The current structure of the
149// code requires that client.go, requests.go and confidential.go share a credential type without
150// having import recursion. That requires the type used between is in a shared package. Therefore
151// we have this.
152func (c Credential) toInternal() (*accesstokens.Credential, error) {
153	if c.secret != "" {
154		return &accesstokens.Credential{Secret: c.secret}, nil
155	}
156	if c.cert != nil {
157		if c.key == nil {
158			return nil, errors.New("missing private key for certificate")
159		}
160		return &accesstokens.Credential{Cert: c.cert, Key: c.key, X5c: c.x5c}, nil
161	}
162	if c.key != nil {
163		return nil, errors.New("missing certificate for private key")
164	}
165	if c.assertionCallback != nil {
166		return &accesstokens.Credential{AssertionCallback: c.assertionCallback}, nil
167	}
168	if c.tokenProvider != nil {
169		return &accesstokens.Credential{TokenProvider: c.tokenProvider}, nil
170	}
171	return nil, errors.New("invalid credential")
172}
173
174// NewCredFromSecret creates a Credential from a secret.
175func NewCredFromSecret(secret string) (Credential, error) {
176	if secret == "" {
177		return Credential{}, errors.New("secret can't be empty string")
178	}
179	return Credential{secret: secret}, nil
180}
181
182// NewCredFromAssertionCallback creates a Credential that invokes a callback to get assertions
183// authenticating the application. The callback must be thread safe.
184func NewCredFromAssertionCallback(callback func(context.Context, AssertionRequestOptions) (string, error)) Credential {
185	return Credential{assertionCallback: callback}
186}
187
188// NewCredFromCert creates a Credential from a certificate or chain of certificates and an RSA private key
189// as returned by [CertFromPEM].
190func NewCredFromCert(certs []*x509.Certificate, key crypto.PrivateKey) (Credential, error) {
191	cred := Credential{key: key}
192	k, ok := key.(*rsa.PrivateKey)
193	if !ok {
194		return cred, errors.New("key must be an RSA key")
195	}
196	for _, cert := range certs {
197		if cert == nil {
198			// not returning an error here because certs may still contain a sufficient cert/key pair
199			continue
200		}
201		certKey, ok := cert.PublicKey.(*rsa.PublicKey)
202		if ok && k.E == certKey.E && k.N.Cmp(certKey.N) == 0 {
203			// We know this is the signing cert because its public key matches the given private key.
204			// This cert must be first in x5c.
205			cred.cert = cert
206			cred.x5c = append([]string{base64.StdEncoding.EncodeToString(cert.Raw)}, cred.x5c...)
207		} else {
208			cred.x5c = append(cred.x5c, base64.StdEncoding.EncodeToString(cert.Raw))
209		}
210	}
211	if cred.cert == nil {
212		return cred, errors.New("key doesn't match any certificate")
213	}
214	return cred, nil
215}
216
217// TokenProviderParameters is the authentication parameters passed to token providers
218type TokenProviderParameters = exported.TokenProviderParameters
219
220// TokenProviderResult is the authentication result returned by custom token providers
221type TokenProviderResult = exported.TokenProviderResult
222
223// NewCredFromTokenProvider creates a Credential from a function that provides access tokens. The function
224// must be concurrency safe. This is intended only to allow the Azure SDK to cache MSI tokens. It isn't
225// useful to applications in general because the token provider must implement all authentication logic.
226func NewCredFromTokenProvider(provider func(context.Context, TokenProviderParameters) (TokenProviderResult, error)) Credential {
227	return Credential{tokenProvider: provider}
228}
229
230// AutoDetectRegion instructs MSAL Go to auto detect region for Azure regional token service.
231func AutoDetectRegion() string {
232	return "TryAutoDetect"
233}
234
235// Client is a representation of authentication client for confidential applications as defined in the
236// package doc. A new Client should be created PER SERVICE USER.
237// For more information, visit https://docs.microsoft.com/azure/active-directory/develop/msal-client-applications
238type Client struct {
239	base base.Client
240	cred *accesstokens.Credential
241}
242
243// clientOptions are optional settings for New(). These options are set using various functions
244// returning Option calls.
245type clientOptions struct {
246	accessor                          cache.ExportReplace
247	authority, azureRegion            string
248	capabilities                      []string
249	disableInstanceDiscovery, sendX5C bool
250	httpClient                        ops.HTTPClient
251}
252
253// Option is an optional argument to New().
254type Option func(o *clientOptions)
255
256// WithCache provides an accessor that will read and write authentication data to an externally managed cache.
257func WithCache(accessor cache.ExportReplace) Option {
258	return func(o *clientOptions) {
259		o.accessor = accessor
260	}
261}
262
263// WithClientCapabilities allows configuring one or more client capabilities such as "CP1"
264func WithClientCapabilities(capabilities []string) Option {
265	return func(o *clientOptions) {
266		// there's no danger of sharing the slice's underlying memory with the application because
267		// this slice is simply passed to base.WithClientCapabilities, which copies its data
268		o.capabilities = capabilities
269	}
270}
271
272// WithHTTPClient allows for a custom HTTP client to be set.
273func WithHTTPClient(httpClient ops.HTTPClient) Option {
274	return func(o *clientOptions) {
275		o.httpClient = httpClient
276	}
277}
278
279// WithX5C specifies if x5c claim(public key of the certificate) should be sent to STS to enable Subject Name Issuer Authentication.
280func WithX5C() Option {
281	return func(o *clientOptions) {
282		o.sendX5C = true
283	}
284}
285
286// WithInstanceDiscovery set to false to disable authority validation (to support private cloud scenarios)
287func WithInstanceDiscovery(enabled bool) Option {
288	return func(o *clientOptions) {
289		o.disableInstanceDiscovery = !enabled
290	}
291}
292
293// WithAzureRegion sets the region(preferred) or Confidential.AutoDetectRegion() for auto detecting region.
294// Region names as per https://azure.microsoft.com/en-ca/global-infrastructure/geographies/.
295// See https://aka.ms/region-map for more details on region names.
296// The region value should be short region name for the region where the service is deployed.
297// For example "centralus" is short name for region Central US.
298// Not all auth flows can use the regional token service.
299// Service To Service (client credential flow) tokens can be obtained from the regional service.
300// Requires configuration at the tenant level.
301// Auto-detection works on a limited number of Azure artifacts (VMs, Azure functions).
302// If auto-detection fails, the non-regional endpoint will be used.
303// If an invalid region name is provided, the non-regional endpoint MIGHT be used or the token request MIGHT fail.
304func WithAzureRegion(val string) Option {
305	return func(o *clientOptions) {
306		o.azureRegion = val
307	}
308}
309
310// New is the constructor for Client. authority is the URL of a token authority such as "https://login.microsoftonline.com/<your tenant>".
311// If the Client will connect directly to AD FS, use "adfs" for the tenant. clientID is the application's client ID (also called its
312// "application ID").
313func New(authority, clientID string, cred Credential, options ...Option) (Client, error) {
314	internalCred, err := cred.toInternal()
315	if err != nil {
316		return Client{}, err
317	}
318
319	opts := clientOptions{
320		authority: authority,
321		// if the caller specified a token provider, it will handle all details of authentication, using Client only as a token cache
322		disableInstanceDiscovery: cred.tokenProvider != nil,
323		httpClient:               shared.DefaultClient,
324	}
325	for _, o := range options {
326		o(&opts)
327	}
328	baseOpts := []base.Option{
329		base.WithCacheAccessor(opts.accessor),
330		base.WithClientCapabilities(opts.capabilities),
331		base.WithInstanceDiscovery(!opts.disableInstanceDiscovery),
332		base.WithRegionDetection(opts.azureRegion),
333		base.WithX5C(opts.sendX5C),
334	}
335	base, err := base.New(clientID, opts.authority, oauth.New(opts.httpClient), baseOpts...)
336	if err != nil {
337		return Client{}, err
338	}
339	base.AuthParams.IsConfidentialClient = true
340
341	return Client{base: base, cred: internalCred}, nil
342}
343
344// authCodeURLOptions contains options for AuthCodeURL
345type authCodeURLOptions struct {
346	claims, loginHint, tenantID, domainHint string
347}
348
349// AuthCodeURLOption is implemented by options for AuthCodeURL
350type AuthCodeURLOption interface {
351	authCodeURLOption()
352}
353
354// AuthCodeURL creates a URL used to acquire an authorization code. Users need to call CreateAuthorizationCodeURLParameters and pass it in.
355//
356// Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID]
357func (cca Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, opts ...AuthCodeURLOption) (string, error) {
358	o := authCodeURLOptions{}
359	if err := options.ApplyOptions(&o, opts); err != nil {
360		return "", err
361	}
362	ap, err := cca.base.AuthParams.WithTenant(o.tenantID)
363	if err != nil {
364		return "", err
365	}
366	ap.Claims = o.claims
367	ap.LoginHint = o.loginHint
368	ap.DomainHint = o.domainHint
369	return cca.base.AuthCodeURL(ctx, clientID, redirectURI, scopes, ap)
370}
371
372// WithLoginHint pre-populates the login prompt with a username.
373func WithLoginHint(username string) interface {
374	AuthCodeURLOption
375	options.CallOption
376} {
377	return struct {
378		AuthCodeURLOption
379		options.CallOption
380	}{
381		CallOption: options.NewCallOption(
382			func(a any) error {
383				switch t := a.(type) {
384				case *authCodeURLOptions:
385					t.loginHint = username
386				default:
387					return fmt.Errorf("unexpected options type %T", a)
388				}
389				return nil
390			},
391		),
392	}
393}
394
395// WithDomainHint adds the IdP domain as domain_hint query parameter in the auth url.
396func WithDomainHint(domain string) interface {
397	AuthCodeURLOption
398	options.CallOption
399} {
400	return struct {
401		AuthCodeURLOption
402		options.CallOption
403	}{
404		CallOption: options.NewCallOption(
405			func(a any) error {
406				switch t := a.(type) {
407				case *authCodeURLOptions:
408					t.domainHint = domain
409				default:
410					return fmt.Errorf("unexpected options type %T", a)
411				}
412				return nil
413			},
414		),
415	}
416}
417
418// WithClaims sets additional claims to request for the token, such as those required by conditional access policies.
419// Use this option when Azure AD returned a claims challenge for a prior request. The argument must be decoded.
420// This option is valid for any token acquisition method.
421func WithClaims(claims string) interface {
422	AcquireByAuthCodeOption
423	AcquireByCredentialOption
424	AcquireOnBehalfOfOption
425	AcquireSilentOption
426	AuthCodeURLOption
427	options.CallOption
428} {
429	return struct {
430		AcquireByAuthCodeOption
431		AcquireByCredentialOption
432		AcquireOnBehalfOfOption
433		AcquireSilentOption
434		AuthCodeURLOption
435		options.CallOption
436	}{
437		CallOption: options.NewCallOption(
438			func(a any) error {
439				switch t := a.(type) {
440				case *acquireTokenByAuthCodeOptions:
441					t.claims = claims
442				case *acquireTokenByCredentialOptions:
443					t.claims = claims
444				case *acquireTokenOnBehalfOfOptions:
445					t.claims = claims
446				case *acquireTokenSilentOptions:
447					t.claims = claims
448				case *authCodeURLOptions:
449					t.claims = claims
450				default:
451					return fmt.Errorf("unexpected options type %T", a)
452				}
453				return nil
454			},
455		),
456	}
457}
458
459// WithAuthenticationScheme is an extensibility mechanism designed to be used only by Azure Arc for proof of possession access tokens.
460func WithAuthenticationScheme(authnScheme AuthenticationScheme) interface {
461	AcquireSilentOption
462	AcquireByCredentialOption
463	options.CallOption
464} {
465	return struct {
466		AcquireSilentOption
467		AcquireByCredentialOption
468		options.CallOption
469	}{
470		CallOption: options.NewCallOption(
471			func(a any) error {
472				switch t := a.(type) {
473				case *acquireTokenSilentOptions:
474					t.authnScheme = authnScheme
475				case *acquireTokenByCredentialOptions:
476					t.authnScheme = authnScheme
477				default:
478					return fmt.Errorf("unexpected options type %T", a)
479				}
480				return nil
481			},
482		),
483	}
484}
485
486// WithTenantID specifies a tenant for a single authentication. It may be different than the tenant set in [New].
487// This option is valid for any token acquisition method.
488func WithTenantID(tenantID string) interface {
489	AcquireByAuthCodeOption
490	AcquireByCredentialOption
491	AcquireOnBehalfOfOption
492	AcquireSilentOption
493	AuthCodeURLOption
494	options.CallOption
495} {
496	return struct {
497		AcquireByAuthCodeOption
498		AcquireByCredentialOption
499		AcquireOnBehalfOfOption
500		AcquireSilentOption
501		AuthCodeURLOption
502		options.CallOption
503	}{
504		CallOption: options.NewCallOption(
505			func(a any) error {
506				switch t := a.(type) {
507				case *acquireTokenByAuthCodeOptions:
508					t.tenantID = tenantID
509				case *acquireTokenByCredentialOptions:
510					t.tenantID = tenantID
511				case *acquireTokenOnBehalfOfOptions:
512					t.tenantID = tenantID
513				case *acquireTokenSilentOptions:
514					t.tenantID = tenantID
515				case *authCodeURLOptions:
516					t.tenantID = tenantID
517				default:
518					return fmt.Errorf("unexpected options type %T", a)
519				}
520				return nil
521			},
522		),
523	}
524}
525
526// acquireTokenSilentOptions are all the optional settings to an AcquireTokenSilent() call.
527// These are set by using various AcquireTokenSilentOption functions.
528type acquireTokenSilentOptions struct {
529	account          Account
530	claims, tenantID string
531	authnScheme      AuthenticationScheme
532}
533
534// AcquireSilentOption is implemented by options for AcquireTokenSilent
535type AcquireSilentOption interface {
536	acquireSilentOption()
537}
538
539// WithSilentAccount uses the passed account during an AcquireTokenSilent() call.
540func WithSilentAccount(account Account) interface {
541	AcquireSilentOption
542	options.CallOption
543} {
544	return struct {
545		AcquireSilentOption
546		options.CallOption
547	}{
548		CallOption: options.NewCallOption(
549			func(a any) error {
550				switch t := a.(type) {
551				case *acquireTokenSilentOptions:
552					t.account = account
553				default:
554					return fmt.Errorf("unexpected options type %T", a)
555				}
556				return nil
557			},
558		),
559	}
560}
561
562// AcquireTokenSilent acquires a token from either the cache or using a refresh token.
563//
564// Options: [WithClaims], [WithSilentAccount], [WithTenantID]
565func (cca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts ...AcquireSilentOption) (AuthResult, error) {
566	o := acquireTokenSilentOptions{}
567	if err := options.ApplyOptions(&o, opts); err != nil {
568		return AuthResult{}, err
569	}
570
571	if o.claims != "" {
572		return AuthResult{}, errors.New("call another AcquireToken method to request a new token having these claims")
573	}
574
575	silentParameters := base.AcquireTokenSilentParameters{
576		Scopes:      scopes,
577		Account:     o.account,
578		RequestType: accesstokens.ATConfidential,
579		Credential:  cca.cred,
580		IsAppCache:  o.account.IsZero(),
581		TenantID:    o.tenantID,
582		AuthnScheme: o.authnScheme,
583	}
584
585	return cca.base.AcquireTokenSilent(ctx, silentParameters)
586}
587
588// acquireTokenByAuthCodeOptions contains the optional parameters used to acquire an access token using the authorization code flow.
589type acquireTokenByAuthCodeOptions struct {
590	challenge, claims, tenantID string
591}
592
593// AcquireByAuthCodeOption is implemented by options for AcquireTokenByAuthCode
594type AcquireByAuthCodeOption interface {
595	acquireByAuthCodeOption()
596}
597
598// WithChallenge allows you to provide a challenge for the .AcquireTokenByAuthCode() call.
599func WithChallenge(challenge string) interface {
600	AcquireByAuthCodeOption
601	options.CallOption
602} {
603	return struct {
604		AcquireByAuthCodeOption
605		options.CallOption
606	}{
607		CallOption: options.NewCallOption(
608			func(a any) error {
609				switch t := a.(type) {
610				case *acquireTokenByAuthCodeOptions:
611					t.challenge = challenge
612				default:
613					return fmt.Errorf("unexpected options type %T", a)
614				}
615				return nil
616			},
617		),
618	}
619}
620
621// AcquireTokenByAuthCode is a request to acquire a security token from the authority, using an authorization code.
622// The specified redirect URI must be the same URI that was used when the authorization code was requested.
623//
624// Options: [WithChallenge], [WithClaims], [WithTenantID]
625func (cca Client) AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, opts ...AcquireByAuthCodeOption) (AuthResult, error) {
626	o := acquireTokenByAuthCodeOptions{}
627	if err := options.ApplyOptions(&o, opts); err != nil {
628		return AuthResult{}, err
629	}
630
631	params := base.AcquireTokenAuthCodeParameters{
632		Scopes:      scopes,
633		Code:        code,
634		Challenge:   o.challenge,
635		Claims:      o.claims,
636		AppType:     accesstokens.ATConfidential,
637		Credential:  cca.cred, // This setting differs from public.Client.AcquireTokenByAuthCode
638		RedirectURI: redirectURI,
639		TenantID:    o.tenantID,
640	}
641
642	return cca.base.AcquireTokenByAuthCode(ctx, params)
643}
644
645// acquireTokenByCredentialOptions contains optional configuration for AcquireTokenByCredential
646type acquireTokenByCredentialOptions struct {
647	claims, tenantID string
648	authnScheme      AuthenticationScheme
649}
650
651// AcquireByCredentialOption is implemented by options for AcquireTokenByCredential
652type AcquireByCredentialOption interface {
653	acquireByCredOption()
654}
655
656// AcquireTokenByCredential acquires a security token from the authority, using the client credentials grant.
657//
658// Options: [WithClaims], [WithTenantID]
659func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string, opts ...AcquireByCredentialOption) (AuthResult, error) {
660	o := acquireTokenByCredentialOptions{}
661	err := options.ApplyOptions(&o, opts)
662	if err != nil {
663		return AuthResult{}, err
664	}
665	authParams, err := cca.base.AuthParams.WithTenant(o.tenantID)
666	if err != nil {
667		return AuthResult{}, err
668	}
669	authParams.Scopes = scopes
670	authParams.AuthorizationType = authority.ATClientCredentials
671	authParams.Claims = o.claims
672	if o.authnScheme != nil {
673		authParams.AuthnScheme = o.authnScheme
674	}
675	token, err := cca.base.Token.Credential(ctx, authParams, cca.cred)
676	if err != nil {
677		return AuthResult{}, err
678	}
679	return cca.base.AuthResultFromToken(ctx, authParams, token, true)
680}
681
682// acquireTokenOnBehalfOfOptions contains optional configuration for AcquireTokenOnBehalfOf
683type acquireTokenOnBehalfOfOptions struct {
684	claims, tenantID string
685}
686
687// AcquireOnBehalfOfOption is implemented by options for AcquireTokenOnBehalfOf
688type AcquireOnBehalfOfOption interface {
689	acquireOBOOption()
690}
691
692// AcquireTokenOnBehalfOf acquires a security token for an app using middle tier apps access token.
693// Refer https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow.
694//
695// Options: [WithClaims], [WithTenantID]
696func (cca Client) AcquireTokenOnBehalfOf(ctx context.Context, userAssertion string, scopes []string, opts ...AcquireOnBehalfOfOption) (AuthResult, error) {
697	o := acquireTokenOnBehalfOfOptions{}
698	if err := options.ApplyOptions(&o, opts); err != nil {
699		return AuthResult{}, err
700	}
701	params := base.AcquireTokenOnBehalfOfParameters{
702		Scopes:        scopes,
703		UserAssertion: userAssertion,
704		Claims:        o.claims,
705		Credential:    cca.cred,
706		TenantID:      o.tenantID,
707	}
708	return cca.base.AcquireTokenOnBehalfOf(ctx, params)
709}
710
711// Account gets the account in the token cache with the specified homeAccountID.
712func (cca Client) Account(ctx context.Context, accountID string) (Account, error) {
713	return cca.base.Account(ctx, accountID)
714}
715
716// RemoveAccount signs the account out and forgets account from token cache.
717func (cca Client) RemoveAccount(ctx context.Context, account Account) error {
718	return cca.base.RemoveAccount(ctx, account)
719}