public.go raw

   1  // Copyright (c) Microsoft Corporation.
   2  // Licensed under the MIT license.
   3  
   4  /*
   5  Package public provides a client for authentication of "public" applications. A "public"
   6  application is defined as an app that runs on client devices (android, ios, windows, linux, ...).
   7  These devices are "untrusted" and access resources via web APIs that must authenticate.
   8  */
   9  package public
  10  
  11  /*
  12  Design note:
  13  
  14  public.Client uses client.Base as an embedded type. client.Base statically assigns its attributes
  15  during creation. As it doesn't have any pointers in it, anything borrowed from it, such as
  16  Base.AuthParams is a copy that is free to be manipulated here.
  17  */
  18  
  19  // TODO(msal): This should have example code for each method on client using Go's example doc framework.
  20  // base usage details should be includee in the package documentation.
  21  
  22  import (
  23  	"context"
  24  	"crypto/rand"
  25  	"crypto/sha256"
  26  	"encoding/base64"
  27  	"errors"
  28  	"fmt"
  29  	"net/url"
  30  	"reflect"
  31  	"strconv"
  32  
  33  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
  34  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base"
  35  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/local"
  36  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
  37  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
  38  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
  39  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
  40  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/options"
  41  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
  42  	"github.com/google/uuid"
  43  	"github.com/pkg/browser"
  44  )
  45  
  46  // AuthResult contains the results of one token acquisition operation.
  47  // For details see https://aka.ms/msal-net-authenticationresult
  48  type AuthResult = base.AuthResult
  49  
  50  type AuthenticationScheme = authority.AuthenticationScheme
  51  
  52  type Account = shared.Account
  53  
  54  type TokenSource = base.TokenSource
  55  
  56  const (
  57  	TokenSourceIdentityProvider = base.TokenSourceIdentityProvider
  58  	TokenSourceCache            = base.TokenSourceCache
  59  )
  60  
  61  var errNoAccount = errors.New("no account was specified with public.WithSilentAccount(), or the specified account is invalid")
  62  
  63  // clientOptions configures the Client's behavior.
  64  type clientOptions struct {
  65  	accessor                 cache.ExportReplace
  66  	authority                string
  67  	capabilities             []string
  68  	disableInstanceDiscovery bool
  69  	httpClient               ops.HTTPClient
  70  }
  71  
  72  func (p *clientOptions) validate() error {
  73  	u, err := url.Parse(p.authority)
  74  	if err != nil {
  75  		return fmt.Errorf("Authority options cannot be URL parsed: %w", err)
  76  	}
  77  	if u.Scheme != "https" {
  78  		return fmt.Errorf("Authority(%s) did not start with https://", u.String())
  79  	}
  80  	return nil
  81  }
  82  
  83  // Option is an optional argument to the New constructor.
  84  type Option func(o *clientOptions)
  85  
  86  // WithAuthority allows for a custom authority to be set. This must be a valid https url.
  87  func WithAuthority(authority string) Option {
  88  	return func(o *clientOptions) {
  89  		o.authority = authority
  90  	}
  91  }
  92  
  93  // WithCache provides an accessor that will read and write authentication data to an externally managed cache.
  94  func WithCache(accessor cache.ExportReplace) Option {
  95  	return func(o *clientOptions) {
  96  		o.accessor = accessor
  97  	}
  98  }
  99  
 100  // WithClientCapabilities allows configuring one or more client capabilities such as "CP1"
 101  func WithClientCapabilities(capabilities []string) Option {
 102  	return func(o *clientOptions) {
 103  		// there's no danger of sharing the slice's underlying memory with the application because
 104  		// this slice is simply passed to base.WithClientCapabilities, which copies its data
 105  		o.capabilities = capabilities
 106  	}
 107  }
 108  
 109  // WithHTTPClient allows for a custom HTTP client to be set.
 110  func WithHTTPClient(httpClient ops.HTTPClient) Option {
 111  	return func(o *clientOptions) {
 112  		o.httpClient = httpClient
 113  	}
 114  }
 115  
 116  // WithInstanceDiscovery set to false to disable authority validation (to support private cloud scenarios)
 117  func WithInstanceDiscovery(enabled bool) Option {
 118  	return func(o *clientOptions) {
 119  		o.disableInstanceDiscovery = !enabled
 120  	}
 121  }
 122  
 123  // Client is a representation of authentication client for public applications as defined in the
 124  // package doc. For more information, visit https://docs.microsoft.com/azure/active-directory/develop/msal-client-applications.
 125  type Client struct {
 126  	base base.Client
 127  }
 128  
 129  // New is the constructor for Client.
 130  func New(clientID string, options ...Option) (Client, error) {
 131  	opts := clientOptions{
 132  		authority:  base.AuthorityPublicCloud,
 133  		httpClient: shared.DefaultClient,
 134  	}
 135  
 136  	for _, o := range options {
 137  		o(&opts)
 138  	}
 139  	if err := opts.validate(); err != nil {
 140  		return Client{}, err
 141  	}
 142  
 143  	base, err := base.New(clientID, opts.authority, oauth.New(opts.httpClient), base.WithCacheAccessor(opts.accessor), base.WithClientCapabilities(opts.capabilities), base.WithInstanceDiscovery(!opts.disableInstanceDiscovery))
 144  	if err != nil {
 145  		return Client{}, err
 146  	}
 147  	return Client{base}, nil
 148  }
 149  
 150  // authCodeURLOptions contains options for AuthCodeURL
 151  type authCodeURLOptions struct {
 152  	claims, loginHint, tenantID, domainHint string
 153  }
 154  
 155  // AuthCodeURLOption is implemented by options for AuthCodeURL
 156  type AuthCodeURLOption interface {
 157  	authCodeURLOption()
 158  }
 159  
 160  // AuthCodeURL creates a URL used to acquire an authorization code.
 161  //
 162  // Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID]
 163  func (pca Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, opts ...AuthCodeURLOption) (string, error) {
 164  	o := authCodeURLOptions{}
 165  	if err := options.ApplyOptions(&o, opts); err != nil {
 166  		return "", err
 167  	}
 168  	ap, err := pca.base.AuthParams.WithTenant(o.tenantID)
 169  	if err != nil {
 170  		return "", err
 171  	}
 172  	ap.Claims = o.claims
 173  	ap.LoginHint = o.loginHint
 174  	ap.DomainHint = o.domainHint
 175  	return pca.base.AuthCodeURL(ctx, clientID, redirectURI, scopes, ap)
 176  }
 177  
 178  // WithClaims sets additional claims to request for the token, such as those required by conditional access policies.
 179  // Use this option when Azure AD returned a claims challenge for a prior request. The argument must be decoded.
 180  // This option is valid for any token acquisition method.
 181  func WithClaims(claims string) interface {
 182  	AcquireByAuthCodeOption
 183  	AcquireByDeviceCodeOption
 184  	AcquireByUsernamePasswordOption
 185  	AcquireInteractiveOption
 186  	AcquireSilentOption
 187  	AuthCodeURLOption
 188  	options.CallOption
 189  } {
 190  	return struct {
 191  		AcquireByAuthCodeOption
 192  		AcquireByDeviceCodeOption
 193  		AcquireByUsernamePasswordOption
 194  		AcquireInteractiveOption
 195  		AcquireSilentOption
 196  		AuthCodeURLOption
 197  		options.CallOption
 198  	}{
 199  		CallOption: options.NewCallOption(
 200  			func(a any) error {
 201  				switch t := a.(type) {
 202  				case *acquireTokenByAuthCodeOptions:
 203  					t.claims = claims
 204  				case *acquireTokenByDeviceCodeOptions:
 205  					t.claims = claims
 206  				case *acquireTokenByUsernamePasswordOptions:
 207  					t.claims = claims
 208  				case *acquireTokenSilentOptions:
 209  					t.claims = claims
 210  				case *authCodeURLOptions:
 211  					t.claims = claims
 212  				case *interactiveAuthOptions:
 213  					t.claims = claims
 214  				default:
 215  					return fmt.Errorf("unexpected options type %T", a)
 216  				}
 217  				return nil
 218  			},
 219  		),
 220  	}
 221  }
 222  
 223  // WithAuthenticationScheme is an extensibility mechanism designed to be used only by Azure Arc for proof of possession access tokens.
 224  func WithAuthenticationScheme(authnScheme AuthenticationScheme) interface {
 225  	AcquireSilentOption
 226  	AcquireInteractiveOption
 227  	AcquireByUsernamePasswordOption
 228  	options.CallOption
 229  } {
 230  	return struct {
 231  		AcquireSilentOption
 232  		AcquireInteractiveOption
 233  		AcquireByUsernamePasswordOption
 234  		options.CallOption
 235  	}{
 236  		CallOption: options.NewCallOption(
 237  			func(a any) error {
 238  				switch t := a.(type) {
 239  				case *acquireTokenSilentOptions:
 240  					t.authnScheme = authnScheme
 241  				case *interactiveAuthOptions:
 242  					t.authnScheme = authnScheme
 243  				case *acquireTokenByUsernamePasswordOptions:
 244  					t.authnScheme = authnScheme
 245  				default:
 246  					return fmt.Errorf("unexpected options type %T", a)
 247  				}
 248  				return nil
 249  			},
 250  		),
 251  	}
 252  }
 253  
 254  // WithTenantID specifies a tenant for a single authentication. It may be different than the tenant set in [New] by [WithAuthority].
 255  // This option is valid for any token acquisition method.
 256  func WithTenantID(tenantID string) interface {
 257  	AcquireByAuthCodeOption
 258  	AcquireByDeviceCodeOption
 259  	AcquireByUsernamePasswordOption
 260  	AcquireInteractiveOption
 261  	AcquireSilentOption
 262  	AuthCodeURLOption
 263  	options.CallOption
 264  } {
 265  	return struct {
 266  		AcquireByAuthCodeOption
 267  		AcquireByDeviceCodeOption
 268  		AcquireByUsernamePasswordOption
 269  		AcquireInteractiveOption
 270  		AcquireSilentOption
 271  		AuthCodeURLOption
 272  		options.CallOption
 273  	}{
 274  		CallOption: options.NewCallOption(
 275  			func(a any) error {
 276  				switch t := a.(type) {
 277  				case *acquireTokenByAuthCodeOptions:
 278  					t.tenantID = tenantID
 279  				case *acquireTokenByDeviceCodeOptions:
 280  					t.tenantID = tenantID
 281  				case *acquireTokenByUsernamePasswordOptions:
 282  					t.tenantID = tenantID
 283  				case *acquireTokenSilentOptions:
 284  					t.tenantID = tenantID
 285  				case *authCodeURLOptions:
 286  					t.tenantID = tenantID
 287  				case *interactiveAuthOptions:
 288  					t.tenantID = tenantID
 289  				default:
 290  					return fmt.Errorf("unexpected options type %T", a)
 291  				}
 292  				return nil
 293  			},
 294  		),
 295  	}
 296  }
 297  
 298  // acquireTokenSilentOptions are all the optional settings to an AcquireTokenSilent() call.
 299  // These are set by using various AcquireTokenSilentOption functions.
 300  type acquireTokenSilentOptions struct {
 301  	account          Account
 302  	claims, tenantID string
 303  	authnScheme      AuthenticationScheme
 304  }
 305  
 306  // AcquireSilentOption is implemented by options for AcquireTokenSilent
 307  type AcquireSilentOption interface {
 308  	acquireSilentOption()
 309  }
 310  
 311  // WithSilentAccount uses the passed account during an AcquireTokenSilent() call.
 312  func WithSilentAccount(account Account) interface {
 313  	AcquireSilentOption
 314  	options.CallOption
 315  } {
 316  	return struct {
 317  		AcquireSilentOption
 318  		options.CallOption
 319  	}{
 320  		CallOption: options.NewCallOption(
 321  			func(a any) error {
 322  				switch t := a.(type) {
 323  				case *acquireTokenSilentOptions:
 324  					t.account = account
 325  				default:
 326  					return fmt.Errorf("unexpected options type %T", a)
 327  				}
 328  				return nil
 329  			},
 330  		),
 331  	}
 332  }
 333  
 334  // AcquireTokenSilent acquires a token from either the cache or using a refresh token.
 335  //
 336  // Options: [WithClaims], [WithSilentAccount], [WithTenantID]
 337  func (pca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts ...AcquireSilentOption) (AuthResult, error) {
 338  	o := acquireTokenSilentOptions{}
 339  	if err := options.ApplyOptions(&o, opts); err != nil {
 340  		return AuthResult{}, err
 341  	}
 342  	// an account is required to find user tokens in the cache
 343  	if reflect.ValueOf(o.account).IsZero() {
 344  		return AuthResult{}, errNoAccount
 345  	}
 346  
 347  	silentParameters := base.AcquireTokenSilentParameters{
 348  		Scopes:      scopes,
 349  		Account:     o.account,
 350  		Claims:      o.claims,
 351  		RequestType: accesstokens.ATPublic,
 352  		IsAppCache:  false,
 353  		TenantID:    o.tenantID,
 354  		AuthnScheme: o.authnScheme,
 355  	}
 356  
 357  	return pca.base.AcquireTokenSilent(ctx, silentParameters)
 358  }
 359  
 360  // acquireTokenByUsernamePasswordOptions contains optional configuration for AcquireTokenByUsernamePassword
 361  type acquireTokenByUsernamePasswordOptions struct {
 362  	claims, tenantID string
 363  	authnScheme      AuthenticationScheme
 364  }
 365  
 366  // AcquireByUsernamePasswordOption is implemented by options for AcquireTokenByUsernamePassword
 367  type AcquireByUsernamePasswordOption interface {
 368  	acquireByUsernamePasswordOption()
 369  }
 370  
 371  // Deprecated: This API will be removed in a future release. Use a more secure flow instead. Follow this migration guide: https://aka.ms/msal-ropc-migration
 372  //
 373  // AcquireTokenByUsernamePassword acquires a security token from the authority, via Username/Password Authentication.
 374  // Options: [WithClaims], [WithTenantID]
 375  func (pca Client) AcquireTokenByUsernamePassword(ctx context.Context, scopes []string, username, password string, opts ...AcquireByUsernamePasswordOption) (AuthResult, error) {
 376  	o := acquireTokenByUsernamePasswordOptions{}
 377  	if err := options.ApplyOptions(&o, opts); err != nil {
 378  		return AuthResult{}, err
 379  	}
 380  	authParams, err := pca.base.AuthParams.WithTenant(o.tenantID)
 381  	if err != nil {
 382  		return AuthResult{}, err
 383  	}
 384  	authParams.Scopes = scopes
 385  	authParams.AuthorizationType = authority.ATUsernamePassword
 386  	authParams.Claims = o.claims
 387  	authParams.Username = username
 388  	authParams.Password = password
 389  	if o.authnScheme != nil {
 390  		authParams.AuthnScheme = o.authnScheme
 391  	}
 392  
 393  	token, err := pca.base.Token.UsernamePassword(ctx, authParams)
 394  	if err != nil {
 395  		return AuthResult{}, err
 396  	}
 397  	return pca.base.AuthResultFromToken(ctx, authParams, token)
 398  }
 399  
 400  type DeviceCodeResult = accesstokens.DeviceCodeResult
 401  
 402  // DeviceCode provides the results of the device code flows first stage (containing the code)
 403  // that must be entered on the second device and provides a method to retrieve the AuthenticationResult
 404  // once that code has been entered and verified.
 405  type DeviceCode struct {
 406  	// Result holds the information about the device code (such as the code).
 407  	Result DeviceCodeResult
 408  
 409  	authParams authority.AuthParams
 410  	client     Client
 411  	dc         oauth.DeviceCode
 412  }
 413  
 414  // AuthenticationResult retreives the AuthenticationResult once the user enters the code
 415  // on the second device. Until then it blocks until the .AcquireTokenByDeviceCode() context
 416  // is cancelled or the token expires.
 417  func (d DeviceCode) AuthenticationResult(ctx context.Context) (AuthResult, error) {
 418  	token, err := d.dc.Token(ctx)
 419  	if err != nil {
 420  		return AuthResult{}, err
 421  	}
 422  	return d.client.base.AuthResultFromToken(ctx, d.authParams, token)
 423  }
 424  
 425  // acquireTokenByDeviceCodeOptions contains optional configuration for AcquireTokenByDeviceCode
 426  type acquireTokenByDeviceCodeOptions struct {
 427  	claims, tenantID string
 428  }
 429  
 430  // AcquireByDeviceCodeOption is implemented by options for AcquireTokenByDeviceCode
 431  type AcquireByDeviceCodeOption interface {
 432  	acquireByDeviceCodeOptions()
 433  }
 434  
 435  // AcquireTokenByDeviceCode acquires a security token from the authority, by acquiring a device code and using that to acquire the token.
 436  // Users need to create an AcquireTokenDeviceCodeParameters instance and pass it in.
 437  //
 438  // Options: [WithClaims], [WithTenantID]
 439  func (pca Client) AcquireTokenByDeviceCode(ctx context.Context, scopes []string, opts ...AcquireByDeviceCodeOption) (DeviceCode, error) {
 440  	o := acquireTokenByDeviceCodeOptions{}
 441  	if err := options.ApplyOptions(&o, opts); err != nil {
 442  		return DeviceCode{}, err
 443  	}
 444  	authParams, err := pca.base.AuthParams.WithTenant(o.tenantID)
 445  	if err != nil {
 446  		return DeviceCode{}, err
 447  	}
 448  	authParams.Scopes = scopes
 449  	authParams.AuthorizationType = authority.ATDeviceCode
 450  	authParams.Claims = o.claims
 451  
 452  	dc, err := pca.base.Token.DeviceCode(ctx, authParams)
 453  	if err != nil {
 454  		return DeviceCode{}, err
 455  	}
 456  
 457  	return DeviceCode{Result: dc.Result, authParams: authParams, client: pca, dc: dc}, nil
 458  }
 459  
 460  // acquireTokenByAuthCodeOptions contains the optional parameters used to acquire an access token using the authorization code flow.
 461  type acquireTokenByAuthCodeOptions struct {
 462  	challenge, claims, tenantID string
 463  }
 464  
 465  // AcquireByAuthCodeOption is implemented by options for AcquireTokenByAuthCode
 466  type AcquireByAuthCodeOption interface {
 467  	acquireByAuthCodeOption()
 468  }
 469  
 470  // WithChallenge allows you to provide a code for the .AcquireTokenByAuthCode() call.
 471  func WithChallenge(challenge string) interface {
 472  	AcquireByAuthCodeOption
 473  	options.CallOption
 474  } {
 475  	return struct {
 476  		AcquireByAuthCodeOption
 477  		options.CallOption
 478  	}{
 479  		CallOption: options.NewCallOption(
 480  			func(a any) error {
 481  				switch t := a.(type) {
 482  				case *acquireTokenByAuthCodeOptions:
 483  					t.challenge = challenge
 484  				default:
 485  					return fmt.Errorf("unexpected options type %T", a)
 486  				}
 487  				return nil
 488  			},
 489  		),
 490  	}
 491  }
 492  
 493  // AcquireTokenByAuthCode is a request to acquire a security token from the authority, using an authorization code.
 494  // The specified redirect URI must be the same URI that was used when the authorization code was requested.
 495  //
 496  // Options: [WithChallenge], [WithClaims], [WithTenantID]
 497  func (pca Client) AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, opts ...AcquireByAuthCodeOption) (AuthResult, error) {
 498  	o := acquireTokenByAuthCodeOptions{}
 499  	if err := options.ApplyOptions(&o, opts); err != nil {
 500  		return AuthResult{}, err
 501  	}
 502  
 503  	params := base.AcquireTokenAuthCodeParameters{
 504  		Scopes:      scopes,
 505  		Code:        code,
 506  		Challenge:   o.challenge,
 507  		Claims:      o.claims,
 508  		AppType:     accesstokens.ATPublic,
 509  		RedirectURI: redirectURI,
 510  		TenantID:    o.tenantID,
 511  	}
 512  
 513  	return pca.base.AcquireTokenByAuthCode(ctx, params)
 514  }
 515  
 516  // Accounts gets all the accounts in the token cache.
 517  // If there are no accounts in the cache the returned slice is empty.
 518  func (pca Client) Accounts(ctx context.Context) ([]Account, error) {
 519  	return pca.base.AllAccounts(ctx)
 520  }
 521  
 522  // RemoveAccount signs the account out and forgets account from token cache.
 523  func (pca Client) RemoveAccount(ctx context.Context, account Account) error {
 524  	return pca.base.RemoveAccount(ctx, account)
 525  }
 526  
 527  // interactiveAuthOptions contains the optional parameters used to acquire an access token for interactive auth code flow.
 528  type interactiveAuthOptions struct {
 529  	claims, domainHint, loginHint, redirectURI, tenantID string
 530  	openURL                                              func(url string) error
 531  	authnScheme                                          AuthenticationScheme
 532  }
 533  
 534  // AcquireInteractiveOption is implemented by options for AcquireTokenInteractive
 535  type AcquireInteractiveOption interface {
 536  	acquireInteractiveOption()
 537  }
 538  
 539  // WithLoginHint pre-populates the login prompt with a username.
 540  func WithLoginHint(username string) interface {
 541  	AcquireInteractiveOption
 542  	AuthCodeURLOption
 543  	options.CallOption
 544  } {
 545  	return struct {
 546  		AcquireInteractiveOption
 547  		AuthCodeURLOption
 548  		options.CallOption
 549  	}{
 550  		CallOption: options.NewCallOption(
 551  			func(a any) error {
 552  				switch t := a.(type) {
 553  				case *authCodeURLOptions:
 554  					t.loginHint = username
 555  				case *interactiveAuthOptions:
 556  					t.loginHint = username
 557  				default:
 558  					return fmt.Errorf("unexpected options type %T", a)
 559  				}
 560  				return nil
 561  			},
 562  		),
 563  	}
 564  }
 565  
 566  // WithDomainHint adds the IdP domain as domain_hint query parameter in the auth url.
 567  func WithDomainHint(domain string) interface {
 568  	AcquireInteractiveOption
 569  	AuthCodeURLOption
 570  	options.CallOption
 571  } {
 572  	return struct {
 573  		AcquireInteractiveOption
 574  		AuthCodeURLOption
 575  		options.CallOption
 576  	}{
 577  		CallOption: options.NewCallOption(
 578  			func(a any) error {
 579  				switch t := a.(type) {
 580  				case *authCodeURLOptions:
 581  					t.domainHint = domain
 582  				case *interactiveAuthOptions:
 583  					t.domainHint = domain
 584  				default:
 585  					return fmt.Errorf("unexpected options type %T", a)
 586  				}
 587  				return nil
 588  			},
 589  		),
 590  	}
 591  }
 592  
 593  // WithRedirectURI sets a port for the local server used in interactive authentication, for
 594  // example http://localhost:port. All URI components other than the port are ignored.
 595  func WithRedirectURI(redirectURI string) interface {
 596  	AcquireInteractiveOption
 597  	options.CallOption
 598  } {
 599  	return struct {
 600  		AcquireInteractiveOption
 601  		options.CallOption
 602  	}{
 603  		CallOption: options.NewCallOption(
 604  			func(a any) error {
 605  				switch t := a.(type) {
 606  				case *interactiveAuthOptions:
 607  					t.redirectURI = redirectURI
 608  				default:
 609  					return fmt.Errorf("unexpected options type %T", a)
 610  				}
 611  				return nil
 612  			},
 613  		),
 614  	}
 615  }
 616  
 617  // WithOpenURL allows you to provide a function to open the browser to complete the interactive login, instead of launching the system default browser.
 618  func WithOpenURL(openURL func(url string) error) interface {
 619  	AcquireInteractiveOption
 620  	options.CallOption
 621  } {
 622  	return struct {
 623  		AcquireInteractiveOption
 624  		options.CallOption
 625  	}{
 626  		CallOption: options.NewCallOption(
 627  			func(a any) error {
 628  				switch t := a.(type) {
 629  				case *interactiveAuthOptions:
 630  					t.openURL = openURL
 631  				default:
 632  					return fmt.Errorf("unexpected options type %T", a)
 633  				}
 634  				return nil
 635  			},
 636  		),
 637  	}
 638  }
 639  
 640  // AcquireTokenInteractive acquires a security token from the authority using the default web browser to select the account.
 641  // https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#interactive-and-non-interactive-authentication
 642  //
 643  // Options: [WithDomainHint], [WithLoginHint], [WithOpenURL], [WithRedirectURI], [WithTenantID]
 644  func (pca Client) AcquireTokenInteractive(ctx context.Context, scopes []string, opts ...AcquireInteractiveOption) (AuthResult, error) {
 645  	o := interactiveAuthOptions{}
 646  	if err := options.ApplyOptions(&o, opts); err != nil {
 647  		return AuthResult{}, err
 648  	}
 649  	// the code verifier is a random 32-byte sequence that's been base-64 encoded without padding.
 650  	// it's used to prevent MitM attacks during auth code flow, see https://tools.ietf.org/html/rfc7636
 651  	cv, challenge, err := codeVerifier()
 652  	if err != nil {
 653  		return AuthResult{}, err
 654  	}
 655  	var redirectURL *url.URL
 656  	if o.redirectURI != "" {
 657  		redirectURL, err = url.Parse(o.redirectURI)
 658  		if err != nil {
 659  			return AuthResult{}, err
 660  		}
 661  	}
 662  	if o.openURL == nil {
 663  		o.openURL = browser.OpenURL
 664  	}
 665  	authParams, err := pca.base.AuthParams.WithTenant(o.tenantID)
 666  	if err != nil {
 667  		return AuthResult{}, err
 668  	}
 669  	authParams.Scopes = scopes
 670  	authParams.AuthorizationType = authority.ATInteractive
 671  	authParams.Claims = o.claims
 672  	authParams.CodeChallenge = challenge
 673  	authParams.CodeChallengeMethod = "S256"
 674  	authParams.LoginHint = o.loginHint
 675  	authParams.DomainHint = o.domainHint
 676  	authParams.State = uuid.New().String()
 677  	authParams.Prompt = "select_account"
 678  	if o.authnScheme != nil {
 679  		authParams.AuthnScheme = o.authnScheme
 680  	}
 681  	res, err := pca.browserLogin(ctx, redirectURL, authParams, o.openURL)
 682  	if err != nil {
 683  		return AuthResult{}, err
 684  	}
 685  	authParams.Redirecturi = res.redirectURI
 686  
 687  	req, err := accesstokens.NewCodeChallengeRequest(authParams, accesstokens.ATPublic, nil, res.authCode, cv)
 688  	if err != nil {
 689  		return AuthResult{}, err
 690  	}
 691  
 692  	token, err := pca.base.Token.AuthCode(ctx, req)
 693  	if err != nil {
 694  		return AuthResult{}, err
 695  	}
 696  
 697  	return pca.base.AuthResultFromToken(ctx, authParams, token)
 698  }
 699  
 700  type interactiveAuthResult struct {
 701  	authCode    string
 702  	redirectURI string
 703  }
 704  
 705  // parses the port number from the provided URL.
 706  // returns 0 if nil or no port is specified.
 707  func parsePort(u *url.URL) (int, error) {
 708  	if u == nil {
 709  		return 0, nil
 710  	}
 711  	p := u.Port()
 712  	if p == "" {
 713  		return 0, nil
 714  	}
 715  	return strconv.Atoi(p)
 716  }
 717  
 718  // browserLogin calls openURL and waits for a user to log in
 719  func (pca Client) browserLogin(ctx context.Context, redirectURI *url.URL, params authority.AuthParams, openURL func(string) error) (interactiveAuthResult, error) {
 720  	// start local redirect server so login can call us back
 721  	port, err := parsePort(redirectURI)
 722  	if err != nil {
 723  		return interactiveAuthResult{}, err
 724  	}
 725  	srv, err := local.New(params.State, port)
 726  	if err != nil {
 727  		return interactiveAuthResult{}, err
 728  	}
 729  	defer srv.Shutdown()
 730  	params.Scopes = accesstokens.AppendDefaultScopes(params)
 731  	authURL, err := pca.base.AuthCodeURL(ctx, params.ClientID, srv.Addr, params.Scopes, params)
 732  	if err != nil {
 733  		return interactiveAuthResult{}, err
 734  	}
 735  	// open browser window so user can select credentials
 736  	if err := openURL(authURL); err != nil {
 737  		return interactiveAuthResult{}, err
 738  	}
 739  	// now wait until the logic calls us back
 740  	res := srv.Result(ctx)
 741  	if res.Err != nil {
 742  		return interactiveAuthResult{}, res.Err
 743  	}
 744  	return interactiveAuthResult{
 745  		authCode:    res.Code,
 746  		redirectURI: srv.Addr,
 747  	}, nil
 748  }
 749  
 750  // creates a code verifier string along with its SHA256 hash which
 751  // is used as the challenge when requesting an auth code.
 752  // used in interactive auth flow for PKCE.
 753  func codeVerifier() (codeVerifier string, challenge string, err error) {
 754  	cvBytes := make([]byte, 32)
 755  	if _, err = rand.Read(cvBytes); err != nil {
 756  		return
 757  	}
 758  	codeVerifier = base64.RawURLEncoding.EncodeToString(cvBytes)
 759  	// for PKCE, create a hash of the code verifier
 760  	cvh := sha256.Sum256([]byte(codeVerifier))
 761  	challenge = base64.RawURLEncoding.EncodeToString(cvh[:])
 762  	return
 763  }
 764