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}