2020-02-21 03:58:17 +00:00
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
|
|
// Licensed under the MIT license.
|
|
|
|
|
|
|
|
/*
|
|
|
|
Package public provides a client for authentication of "public" applications. A "public"
|
|
|
|
application is defined as an app that runs on client devices (android, ios, windows, linux, ...).
|
|
|
|
These devices are "untrusted" and access resources via web APIs that must authenticate.
|
|
|
|
*/
|
|
|
|
package public
|
|
|
|
|
|
|
|
/*
|
|
|
|
Design note:
|
|
|
|
|
|
|
|
public.Client uses client.Base as an embedded type. client.Base statically assigns its attributes
|
|
|
|
during creation. As it doesn't have any pointers in it, anything borrowed from it, such as
|
|
|
|
Base.AuthParams is a copy that is free to be manipulated here.
|
|
|
|
*/
|
|
|
|
|
|
|
|
// TODO(msal): This should have example code for each method on client using Go's example doc framework.
|
|
|
|
// base usage details should be includee in the package documentation.
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
|
|
|
|
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
|
|
|
|
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base"
|
|
|
|
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/local"
|
|
|
|
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
|
|
|
|
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
|
|
|
|
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
|
|
|
|
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
|
|
|
|
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/options"
|
|
|
|
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/pkg/browser"
|
|
|
|
)
|
|
|
|
|
|
|
|
// AuthResult contains the results of one token acquisition operation.
|
|
|
|
// For details see https://aka.ms/msal-net-authenticationresult
|
|
|
|
type AuthResult = base.AuthResult
|
|
|
|
|
|
|
|
type Account = shared.Account
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// clientOptions configures the Client's behavior.
|
|
|
|
type clientOptions struct {
|
|
|
|
accessor cache.ExportReplace
|
|
|
|
authority string
|
|
|
|
capabilities []string
|
2020-02-21 03:58:17 +00:00
|
|
|
disableInstanceDiscovery bool
|
2023-05-11 13:23:47 +00:00
|
|
|
httpClient ops.HTTPClient
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
func (p *clientOptions) validate() error {
|
|
|
|
u, err := url.Parse(p.authority)
|
2020-02-21 03:58:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Authority options cannot be URL parsed: %w", err)
|
|
|
|
}
|
|
|
|
if u.Scheme != "https" {
|
|
|
|
return fmt.Errorf("Authority(%s) did not start with https://", u.String())
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Option is an optional argument to the New constructor.
|
2023-05-11 13:23:47 +00:00
|
|
|
type Option func(o *clientOptions)
|
2020-02-21 03:58:17 +00:00
|
|
|
|
|
|
|
// WithAuthority allows for a custom authority to be set. This must be a valid https url.
|
|
|
|
func WithAuthority(authority string) Option {
|
2023-05-11 13:23:47 +00:00
|
|
|
return func(o *clientOptions) {
|
|
|
|
o.authority = authority
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// WithCache provides an accessor that will read and write authentication data to an externally managed cache.
|
2020-02-21 03:58:17 +00:00
|
|
|
func WithCache(accessor cache.ExportReplace) Option {
|
2023-05-11 13:23:47 +00:00
|
|
|
return func(o *clientOptions) {
|
|
|
|
o.accessor = accessor
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithClientCapabilities allows configuring one or more client capabilities such as "CP1"
|
|
|
|
func WithClientCapabilities(capabilities []string) Option {
|
2023-05-11 13:23:47 +00:00
|
|
|
return func(o *clientOptions) {
|
2020-02-21 03:58:17 +00:00
|
|
|
// there's no danger of sharing the slice's underlying memory with the application because
|
|
|
|
// this slice is simply passed to base.WithClientCapabilities, which copies its data
|
|
|
|
o.capabilities = capabilities
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithHTTPClient allows for a custom HTTP client to be set.
|
|
|
|
func WithHTTPClient(httpClient ops.HTTPClient) Option {
|
2023-05-11 13:23:47 +00:00
|
|
|
return func(o *clientOptions) {
|
|
|
|
o.httpClient = httpClient
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithInstanceDiscovery set to false to disable authority validation (to support private cloud scenarios)
|
|
|
|
func WithInstanceDiscovery(enabled bool) Option {
|
2023-05-11 13:23:47 +00:00
|
|
|
return func(o *clientOptions) {
|
2020-02-21 03:58:17 +00:00
|
|
|
o.disableInstanceDiscovery = !enabled
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Client is a representation of authentication client for public applications as defined in the
|
|
|
|
// package doc. For more information, visit https://docs.microsoft.com/azure/active-directory/develop/msal-client-applications.
|
|
|
|
type Client struct {
|
|
|
|
base base.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
// New is the constructor for Client.
|
|
|
|
func New(clientID string, options ...Option) (Client, error) {
|
2023-05-11 13:23:47 +00:00
|
|
|
opts := clientOptions{
|
|
|
|
authority: base.AuthorityPublicCloud,
|
|
|
|
httpClient: shared.DefaultClient,
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, o := range options {
|
|
|
|
o(&opts)
|
|
|
|
}
|
|
|
|
if err := opts.validate(); err != nil {
|
|
|
|
return Client{}, err
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
base, err := base.New(clientID, opts.authority, oauth.New(opts.httpClient), base.WithCacheAccessor(opts.accessor), base.WithClientCapabilities(opts.capabilities), base.WithInstanceDiscovery(!opts.disableInstanceDiscovery))
|
2020-02-21 03:58:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return Client{}, err
|
|
|
|
}
|
|
|
|
return Client{base}, nil
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// authCodeURLOptions contains options for AuthCodeURL
|
|
|
|
type authCodeURLOptions struct {
|
2020-02-21 03:58:17 +00:00
|
|
|
claims, loginHint, tenantID, domainHint string
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// AuthCodeURLOption is implemented by options for AuthCodeURL
|
|
|
|
type AuthCodeURLOption interface {
|
|
|
|
authCodeURLOption()
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// AuthCodeURL creates a URL used to acquire an authorization code.
|
2020-02-21 03:58:17 +00:00
|
|
|
//
|
|
|
|
// Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID]
|
2023-05-11 13:23:47 +00:00
|
|
|
func (pca Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, opts ...AuthCodeURLOption) (string, error) {
|
|
|
|
o := authCodeURLOptions{}
|
2020-02-21 03:58:17 +00:00
|
|
|
if err := options.ApplyOptions(&o, opts); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
ap, err := pca.base.AuthParams.WithTenant(o.tenantID)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
ap.Claims = o.claims
|
|
|
|
ap.LoginHint = o.loginHint
|
|
|
|
ap.DomainHint = o.domainHint
|
|
|
|
return pca.base.AuthCodeURL(ctx, clientID, redirectURI, scopes, ap)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithClaims sets additional claims to request for the token, such as those required by conditional access policies.
|
|
|
|
// Use this option when Azure AD returned a claims challenge for a prior request. The argument must be decoded.
|
|
|
|
// This option is valid for any token acquisition method.
|
|
|
|
func WithClaims(claims string) interface {
|
|
|
|
AcquireByAuthCodeOption
|
|
|
|
AcquireByDeviceCodeOption
|
|
|
|
AcquireByUsernamePasswordOption
|
|
|
|
AcquireInteractiveOption
|
|
|
|
AcquireSilentOption
|
2023-05-11 13:23:47 +00:00
|
|
|
AuthCodeURLOption
|
2020-02-21 03:58:17 +00:00
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AcquireByAuthCodeOption
|
|
|
|
AcquireByDeviceCodeOption
|
|
|
|
AcquireByUsernamePasswordOption
|
|
|
|
AcquireInteractiveOption
|
|
|
|
AcquireSilentOption
|
2023-05-11 13:23:47 +00:00
|
|
|
AuthCodeURLOption
|
2020-02-21 03:58:17 +00:00
|
|
|
options.CallOption
|
|
|
|
}{
|
|
|
|
CallOption: options.NewCallOption(
|
|
|
|
func(a any) error {
|
|
|
|
switch t := a.(type) {
|
2023-05-11 13:23:47 +00:00
|
|
|
case *acquireTokenByAuthCodeOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.claims = claims
|
|
|
|
case *acquireTokenByDeviceCodeOptions:
|
|
|
|
t.claims = claims
|
|
|
|
case *acquireTokenByUsernamePasswordOptions:
|
|
|
|
t.claims = claims
|
2023-05-11 13:23:47 +00:00
|
|
|
case *acquireTokenSilentOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.claims = claims
|
2023-05-11 13:23:47 +00:00
|
|
|
case *authCodeURLOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.claims = claims
|
2023-05-11 13:23:47 +00:00
|
|
|
case *interactiveAuthOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.claims = claims
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unexpected options type %T", a)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithTenantID specifies a tenant for a single authentication. It may be different than the tenant set in [New] by [WithAuthority].
|
|
|
|
// This option is valid for any token acquisition method.
|
|
|
|
func WithTenantID(tenantID string) interface {
|
|
|
|
AcquireByAuthCodeOption
|
|
|
|
AcquireByDeviceCodeOption
|
|
|
|
AcquireByUsernamePasswordOption
|
|
|
|
AcquireInteractiveOption
|
|
|
|
AcquireSilentOption
|
2023-05-11 13:23:47 +00:00
|
|
|
AuthCodeURLOption
|
2020-02-21 03:58:17 +00:00
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AcquireByAuthCodeOption
|
|
|
|
AcquireByDeviceCodeOption
|
|
|
|
AcquireByUsernamePasswordOption
|
|
|
|
AcquireInteractiveOption
|
|
|
|
AcquireSilentOption
|
2023-05-11 13:23:47 +00:00
|
|
|
AuthCodeURLOption
|
2020-02-21 03:58:17 +00:00
|
|
|
options.CallOption
|
|
|
|
}{
|
|
|
|
CallOption: options.NewCallOption(
|
|
|
|
func(a any) error {
|
|
|
|
switch t := a.(type) {
|
2023-05-11 13:23:47 +00:00
|
|
|
case *acquireTokenByAuthCodeOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.tenantID = tenantID
|
|
|
|
case *acquireTokenByDeviceCodeOptions:
|
|
|
|
t.tenantID = tenantID
|
|
|
|
case *acquireTokenByUsernamePasswordOptions:
|
|
|
|
t.tenantID = tenantID
|
2023-05-11 13:23:47 +00:00
|
|
|
case *acquireTokenSilentOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.tenantID = tenantID
|
2023-05-11 13:23:47 +00:00
|
|
|
case *authCodeURLOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.tenantID = tenantID
|
2023-05-11 13:23:47 +00:00
|
|
|
case *interactiveAuthOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.tenantID = tenantID
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unexpected options type %T", a)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// acquireTokenSilentOptions are all the optional settings to an AcquireTokenSilent() call.
|
2020-02-21 03:58:17 +00:00
|
|
|
// These are set by using various AcquireTokenSilentOption functions.
|
2023-05-11 13:23:47 +00:00
|
|
|
type acquireTokenSilentOptions struct {
|
|
|
|
account Account
|
2020-02-21 03:58:17 +00:00
|
|
|
claims, tenantID string
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireSilentOption is implemented by options for AcquireTokenSilent
|
|
|
|
type AcquireSilentOption interface {
|
|
|
|
acquireSilentOption()
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithSilentAccount uses the passed account during an AcquireTokenSilent() call.
|
|
|
|
func WithSilentAccount(account Account) interface {
|
|
|
|
AcquireSilentOption
|
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AcquireSilentOption
|
|
|
|
options.CallOption
|
|
|
|
}{
|
|
|
|
CallOption: options.NewCallOption(
|
|
|
|
func(a any) error {
|
|
|
|
switch t := a.(type) {
|
2023-05-11 13:23:47 +00:00
|
|
|
case *acquireTokenSilentOptions:
|
|
|
|
t.account = account
|
2020-02-21 03:58:17 +00:00
|
|
|
default:
|
|
|
|
return fmt.Errorf("unexpected options type %T", a)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireTokenSilent acquires a token from either the cache or using a refresh token.
|
|
|
|
//
|
|
|
|
// Options: [WithClaims], [WithSilentAccount], [WithTenantID]
|
|
|
|
func (pca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts ...AcquireSilentOption) (AuthResult, error) {
|
2023-05-11 13:23:47 +00:00
|
|
|
o := acquireTokenSilentOptions{}
|
2020-02-21 03:58:17 +00:00
|
|
|
if err := options.ApplyOptions(&o, opts); err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
silentParameters := base.AcquireTokenSilentParameters{
|
|
|
|
Scopes: scopes,
|
2023-05-11 13:23:47 +00:00
|
|
|
Account: o.account,
|
2020-02-21 03:58:17 +00:00
|
|
|
Claims: o.claims,
|
|
|
|
RequestType: accesstokens.ATPublic,
|
|
|
|
IsAppCache: false,
|
|
|
|
TenantID: o.tenantID,
|
|
|
|
}
|
|
|
|
|
|
|
|
return pca.base.AcquireTokenSilent(ctx, silentParameters)
|
|
|
|
}
|
|
|
|
|
|
|
|
// acquireTokenByUsernamePasswordOptions contains optional configuration for AcquireTokenByUsernamePassword
|
|
|
|
type acquireTokenByUsernamePasswordOptions struct {
|
|
|
|
claims, tenantID string
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireByUsernamePasswordOption is implemented by options for AcquireTokenByUsernamePassword
|
|
|
|
type AcquireByUsernamePasswordOption interface {
|
|
|
|
acquireByUsernamePasswordOption()
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireTokenByUsernamePassword acquires a security token from the authority, via Username/Password Authentication.
|
|
|
|
// NOTE: this flow is NOT recommended.
|
|
|
|
//
|
|
|
|
// Options: [WithClaims], [WithTenantID]
|
|
|
|
func (pca Client) AcquireTokenByUsernamePassword(ctx context.Context, scopes []string, username, password string, opts ...AcquireByUsernamePasswordOption) (AuthResult, error) {
|
|
|
|
o := acquireTokenByUsernamePasswordOptions{}
|
|
|
|
if err := options.ApplyOptions(&o, opts); err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
authParams, err := pca.base.AuthParams.WithTenant(o.tenantID)
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
authParams.Scopes = scopes
|
|
|
|
authParams.AuthorizationType = authority.ATUsernamePassword
|
|
|
|
authParams.Claims = o.claims
|
|
|
|
authParams.Username = username
|
|
|
|
authParams.Password = password
|
|
|
|
|
|
|
|
token, err := pca.base.Token.UsernamePassword(ctx, authParams)
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
return pca.base.AuthResultFromToken(ctx, authParams, token, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
type DeviceCodeResult = accesstokens.DeviceCodeResult
|
|
|
|
|
|
|
|
// DeviceCode provides the results of the device code flows first stage (containing the code)
|
|
|
|
// that must be entered on the second device and provides a method to retrieve the AuthenticationResult
|
|
|
|
// once that code has been entered and verified.
|
|
|
|
type DeviceCode struct {
|
|
|
|
// Result holds the information about the device code (such as the code).
|
|
|
|
Result DeviceCodeResult
|
|
|
|
|
|
|
|
authParams authority.AuthParams
|
|
|
|
client Client
|
|
|
|
dc oauth.DeviceCode
|
|
|
|
}
|
|
|
|
|
|
|
|
// AuthenticationResult retreives the AuthenticationResult once the user enters the code
|
|
|
|
// on the second device. Until then it blocks until the .AcquireTokenByDeviceCode() context
|
|
|
|
// is cancelled or the token expires.
|
|
|
|
func (d DeviceCode) AuthenticationResult(ctx context.Context) (AuthResult, error) {
|
|
|
|
token, err := d.dc.Token(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
return d.client.base.AuthResultFromToken(ctx, d.authParams, token, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
// acquireTokenByDeviceCodeOptions contains optional configuration for AcquireTokenByDeviceCode
|
|
|
|
type acquireTokenByDeviceCodeOptions struct {
|
|
|
|
claims, tenantID string
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireByDeviceCodeOption is implemented by options for AcquireTokenByDeviceCode
|
|
|
|
type AcquireByDeviceCodeOption interface {
|
|
|
|
acquireByDeviceCodeOptions()
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireTokenByDeviceCode acquires a security token from the authority, by acquiring a device code and using that to acquire the token.
|
|
|
|
// Users need to create an AcquireTokenDeviceCodeParameters instance and pass it in.
|
|
|
|
//
|
|
|
|
// Options: [WithClaims], [WithTenantID]
|
|
|
|
func (pca Client) AcquireTokenByDeviceCode(ctx context.Context, scopes []string, opts ...AcquireByDeviceCodeOption) (DeviceCode, error) {
|
|
|
|
o := acquireTokenByDeviceCodeOptions{}
|
|
|
|
if err := options.ApplyOptions(&o, opts); err != nil {
|
|
|
|
return DeviceCode{}, err
|
|
|
|
}
|
|
|
|
authParams, err := pca.base.AuthParams.WithTenant(o.tenantID)
|
|
|
|
if err != nil {
|
|
|
|
return DeviceCode{}, err
|
|
|
|
}
|
|
|
|
authParams.Scopes = scopes
|
|
|
|
authParams.AuthorizationType = authority.ATDeviceCode
|
|
|
|
authParams.Claims = o.claims
|
|
|
|
|
|
|
|
dc, err := pca.base.Token.DeviceCode(ctx, authParams)
|
|
|
|
if err != nil {
|
|
|
|
return DeviceCode{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return DeviceCode{Result: dc.Result, authParams: authParams, client: pca, dc: dc}, nil
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// acquireTokenByAuthCodeOptions contains the optional parameters used to acquire an access token using the authorization code flow.
|
|
|
|
type acquireTokenByAuthCodeOptions struct {
|
|
|
|
challenge, claims, tenantID string
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireByAuthCodeOption is implemented by options for AcquireTokenByAuthCode
|
|
|
|
type AcquireByAuthCodeOption interface {
|
|
|
|
acquireByAuthCodeOption()
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithChallenge allows you to provide a code for the .AcquireTokenByAuthCode() call.
|
|
|
|
func WithChallenge(challenge string) interface {
|
|
|
|
AcquireByAuthCodeOption
|
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AcquireByAuthCodeOption
|
|
|
|
options.CallOption
|
|
|
|
}{
|
|
|
|
CallOption: options.NewCallOption(
|
|
|
|
func(a any) error {
|
|
|
|
switch t := a.(type) {
|
2023-05-11 13:23:47 +00:00
|
|
|
case *acquireTokenByAuthCodeOptions:
|
|
|
|
t.challenge = challenge
|
2020-02-21 03:58:17 +00:00
|
|
|
default:
|
|
|
|
return fmt.Errorf("unexpected options type %T", a)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireTokenByAuthCode is a request to acquire a security token from the authority, using an authorization code.
|
|
|
|
// The specified redirect URI must be the same URI that was used when the authorization code was requested.
|
|
|
|
//
|
|
|
|
// Options: [WithChallenge], [WithClaims], [WithTenantID]
|
|
|
|
func (pca Client) AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, opts ...AcquireByAuthCodeOption) (AuthResult, error) {
|
2023-05-11 13:23:47 +00:00
|
|
|
o := acquireTokenByAuthCodeOptions{}
|
2020-02-21 03:58:17 +00:00
|
|
|
if err := options.ApplyOptions(&o, opts); err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
params := base.AcquireTokenAuthCodeParameters{
|
|
|
|
Scopes: scopes,
|
|
|
|
Code: code,
|
2023-05-11 13:23:47 +00:00
|
|
|
Challenge: o.challenge,
|
2020-02-21 03:58:17 +00:00
|
|
|
Claims: o.claims,
|
|
|
|
AppType: accesstokens.ATPublic,
|
|
|
|
RedirectURI: redirectURI,
|
|
|
|
TenantID: o.tenantID,
|
|
|
|
}
|
|
|
|
|
|
|
|
return pca.base.AcquireTokenByAuthCode(ctx, params)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Accounts gets all the accounts in the token cache.
|
|
|
|
// If there are no accounts in the cache the returned slice is empty.
|
2023-05-11 13:23:47 +00:00
|
|
|
func (pca Client) Accounts(ctx context.Context) ([]Account, error) {
|
|
|
|
return pca.base.AllAccounts(ctx)
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveAccount signs the account out and forgets account from token cache.
|
2023-05-11 13:23:47 +00:00
|
|
|
func (pca Client) RemoveAccount(ctx context.Context, account Account) error {
|
|
|
|
return pca.base.RemoveAccount(ctx, account)
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// interactiveAuthOptions contains the optional parameters used to acquire an access token for interactive auth code flow.
|
|
|
|
type interactiveAuthOptions struct {
|
|
|
|
claims, domainHint, loginHint, redirectURI, tenantID string
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireInteractiveOption is implemented by options for AcquireTokenInteractive
|
|
|
|
type AcquireInteractiveOption interface {
|
|
|
|
acquireInteractiveOption()
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithLoginHint pre-populates the login prompt with a username.
|
|
|
|
func WithLoginHint(username string) interface {
|
|
|
|
AcquireInteractiveOption
|
2023-05-11 13:23:47 +00:00
|
|
|
AuthCodeURLOption
|
2020-02-21 03:58:17 +00:00
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AcquireInteractiveOption
|
2023-05-11 13:23:47 +00:00
|
|
|
AuthCodeURLOption
|
2020-02-21 03:58:17 +00:00
|
|
|
options.CallOption
|
|
|
|
}{
|
|
|
|
CallOption: options.NewCallOption(
|
|
|
|
func(a any) error {
|
|
|
|
switch t := a.(type) {
|
2023-05-11 13:23:47 +00:00
|
|
|
case *authCodeURLOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.loginHint = username
|
2023-05-11 13:23:47 +00:00
|
|
|
case *interactiveAuthOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.loginHint = username
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unexpected options type %T", a)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithDomainHint adds the IdP domain as domain_hint query parameter in the auth url.
|
|
|
|
func WithDomainHint(domain string) interface {
|
|
|
|
AcquireInteractiveOption
|
2023-05-11 13:23:47 +00:00
|
|
|
AuthCodeURLOption
|
2020-02-21 03:58:17 +00:00
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AcquireInteractiveOption
|
2023-05-11 13:23:47 +00:00
|
|
|
AuthCodeURLOption
|
2020-02-21 03:58:17 +00:00
|
|
|
options.CallOption
|
|
|
|
}{
|
|
|
|
CallOption: options.NewCallOption(
|
|
|
|
func(a any) error {
|
|
|
|
switch t := a.(type) {
|
2023-05-11 13:23:47 +00:00
|
|
|
case *authCodeURLOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.domainHint = domain
|
2023-05-11 13:23:47 +00:00
|
|
|
case *interactiveAuthOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.domainHint = domain
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unexpected options type %T", a)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// WithRedirectURI sets a port for the local server used in interactive authentication, for
|
|
|
|
// example http://localhost:port. All URI components other than the port are ignored.
|
2020-02-21 03:58:17 +00:00
|
|
|
func WithRedirectURI(redirectURI string) interface {
|
|
|
|
AcquireInteractiveOption
|
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AcquireInteractiveOption
|
|
|
|
options.CallOption
|
|
|
|
}{
|
|
|
|
CallOption: options.NewCallOption(
|
|
|
|
func(a any) error {
|
|
|
|
switch t := a.(type) {
|
2023-05-11 13:23:47 +00:00
|
|
|
case *interactiveAuthOptions:
|
|
|
|
t.redirectURI = redirectURI
|
2020-02-21 03:58:17 +00:00
|
|
|
default:
|
|
|
|
return fmt.Errorf("unexpected options type %T", a)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireTokenInteractive acquires a security token from the authority using the default web browser to select the account.
|
|
|
|
// https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#interactive-and-non-interactive-authentication
|
|
|
|
//
|
|
|
|
// Options: [WithDomainHint], [WithLoginHint], [WithRedirectURI], [WithTenantID]
|
|
|
|
func (pca Client) AcquireTokenInteractive(ctx context.Context, scopes []string, opts ...AcquireInteractiveOption) (AuthResult, error) {
|
2023-05-11 13:23:47 +00:00
|
|
|
o := interactiveAuthOptions{}
|
2020-02-21 03:58:17 +00:00
|
|
|
if err := options.ApplyOptions(&o, opts); err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
// the code verifier is a random 32-byte sequence that's been base-64 encoded without padding.
|
|
|
|
// it's used to prevent MitM attacks during auth code flow, see https://tools.ietf.org/html/rfc7636
|
|
|
|
cv, challenge, err := codeVerifier()
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
var redirectURL *url.URL
|
2023-05-11 13:23:47 +00:00
|
|
|
if o.redirectURI != "" {
|
|
|
|
redirectURL, err = url.Parse(o.redirectURI)
|
2020-02-21 03:58:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
authParams, err := pca.base.AuthParams.WithTenant(o.tenantID)
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
authParams.Scopes = scopes
|
|
|
|
authParams.AuthorizationType = authority.ATInteractive
|
|
|
|
authParams.Claims = o.claims
|
|
|
|
authParams.CodeChallenge = challenge
|
|
|
|
authParams.CodeChallengeMethod = "S256"
|
|
|
|
authParams.LoginHint = o.loginHint
|
|
|
|
authParams.DomainHint = o.domainHint
|
|
|
|
authParams.State = uuid.New().String()
|
|
|
|
authParams.Prompt = "select_account"
|
|
|
|
res, err := pca.browserLogin(ctx, redirectURL, authParams)
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
authParams.Redirecturi = res.redirectURI
|
|
|
|
|
|
|
|
req, err := accesstokens.NewCodeChallengeRequest(authParams, accesstokens.ATPublic, nil, res.authCode, cv)
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
token, err := pca.base.Token.AuthCode(ctx, req)
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return pca.base.AuthResultFromToken(ctx, authParams, token, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
type interactiveAuthResult struct {
|
|
|
|
authCode string
|
|
|
|
redirectURI string
|
|
|
|
}
|
|
|
|
|
|
|
|
// provides a test hook to simulate opening a browser
|
|
|
|
var browserOpenURL = func(authURL string) error {
|
|
|
|
return browser.OpenURL(authURL)
|
|
|
|
}
|
|
|
|
|
|
|
|
// parses the port number from the provided URL.
|
|
|
|
// returns 0 if nil or no port is specified.
|
|
|
|
func parsePort(u *url.URL) (int, error) {
|
|
|
|
if u == nil {
|
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
p := u.Port()
|
|
|
|
if p == "" {
|
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
return strconv.Atoi(p)
|
|
|
|
}
|
|
|
|
|
|
|
|
// browserLogin launches the system browser for interactive login
|
|
|
|
func (pca Client) browserLogin(ctx context.Context, redirectURI *url.URL, params authority.AuthParams) (interactiveAuthResult, error) {
|
|
|
|
// start local redirect server so login can call us back
|
|
|
|
port, err := parsePort(redirectURI)
|
|
|
|
if err != nil {
|
|
|
|
return interactiveAuthResult{}, err
|
|
|
|
}
|
|
|
|
srv, err := local.New(params.State, port)
|
|
|
|
if err != nil {
|
|
|
|
return interactiveAuthResult{}, err
|
|
|
|
}
|
|
|
|
defer srv.Shutdown()
|
|
|
|
params.Scopes = accesstokens.AppendDefaultScopes(params)
|
|
|
|
authURL, err := pca.base.AuthCodeURL(ctx, params.ClientID, srv.Addr, params.Scopes, params)
|
|
|
|
if err != nil {
|
|
|
|
return interactiveAuthResult{}, err
|
|
|
|
}
|
|
|
|
// open browser window so user can select credentials
|
|
|
|
if err := browserOpenURL(authURL); err != nil {
|
|
|
|
return interactiveAuthResult{}, err
|
|
|
|
}
|
|
|
|
// now wait until the logic calls us back
|
|
|
|
res := srv.Result(ctx)
|
|
|
|
if res.Err != nil {
|
|
|
|
return interactiveAuthResult{}, res.Err
|
|
|
|
}
|
|
|
|
return interactiveAuthResult{
|
|
|
|
authCode: res.Code,
|
|
|
|
redirectURI: srv.Addr,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// creates a code verifier string along with its SHA256 hash which
|
|
|
|
// is used as the challenge when requesting an auth code.
|
|
|
|
// used in interactive auth flow for PKCE.
|
|
|
|
func codeVerifier() (codeVerifier string, challenge string, err error) {
|
|
|
|
cvBytes := make([]byte, 32)
|
|
|
|
if _, err = rand.Read(cvBytes); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
codeVerifier = base64.RawURLEncoding.EncodeToString(cvBytes)
|
|
|
|
// for PKCE, create a hash of the code verifier
|
|
|
|
cvh := sha256.Sum256([]byte(codeVerifier))
|
|
|
|
challenge = base64.RawURLEncoding.EncodeToString(cvh[:])
|
|
|
|
return
|
|
|
|
}
|