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