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