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