distribution/vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/public/public.go
Kirat Singh ba4a6bbe02 Update Azure SDK and support additional authentication schemes
Microsoft has updated the golang Azure SDK significantly.  Update the
azure storage driver to use the new SDK.  Add support for client
secret and MSI authentication schemes in addition to shared key
authentication.

Implement rootDirectory support for the azure storage driver to mirror
the S3 driver.

Signed-off-by: Kirat Singh <kirat.singh@beacon.io>

Co-authored-by: Cory Snider <corhere@gmail.com>
2023-04-25 17:23:20 +00:00

716 lines
22 KiB
Go

// 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
// Options configures the Client's behavior.
type Options struct {
// Accessor controls cache persistence. By default there is no cache persistence.
// This can be set with the WithCache() option.
Accessor cache.ExportReplace
// The host of the Azure Active Directory authority. The default is https://login.microsoftonline.com/common.
// This can be changed with the WithAuthority() option.
Authority string
// The HTTP client used for making requests.
// It defaults to a shared http.Client.
HTTPClient ops.HTTPClient
capabilities []string
disableInstanceDiscovery bool
}
func (p *Options) validate() error {
u, err := url.Parse(p.Authority)
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.
type Option func(o *Options)
// WithAuthority allows for a custom authority to be set. This must be a valid https url.
func WithAuthority(authority string) Option {
return func(o *Options) {
o.Authority = authority
}
}
// WithCache allows you to set some type of cache for storing authentication tokens.
func WithCache(accessor cache.ExportReplace) Option {
return func(o *Options) {
o.Accessor = accessor
}
}
// WithClientCapabilities allows configuring one or more client capabilities such as "CP1"
func WithClientCapabilities(capabilities []string) Option {
return func(o *Options) {
// 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 {
return func(o *Options) {
o.HTTPClient = httpClient
}
}
// WithInstanceDiscovery set to false to disable authority validation (to support private cloud scenarios)
func WithInstanceDiscovery(enabled bool) Option {
return func(o *Options) {
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) {
opts := Options{
Authority: base.AuthorityPublicCloud,
HTTPClient: shared.DefaultClient,
}
for _, o := range options {
o(&opts)
}
if err := opts.validate(); err != nil {
return Client{}, err
}
base, err := base.New(clientID, opts.Authority, oauth.New(opts.HTTPClient), base.WithCacheAccessor(opts.Accessor), base.WithClientCapabilities(opts.capabilities), base.WithInstanceDiscovery(!opts.disableInstanceDiscovery))
if err != nil {
return Client{}, err
}
return Client{base}, nil
}
// createAuthCodeURLOptions contains options for CreateAuthCodeURL
type createAuthCodeURLOptions struct {
claims, loginHint, tenantID, domainHint string
}
// CreateAuthCodeURLOption is implemented by options for CreateAuthCodeURL
type CreateAuthCodeURLOption interface {
createAuthCodeURLOption()
}
// CreateAuthCodeURL creates a URL used to acquire an authorization code.
//
// Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID]
func (pca Client) CreateAuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, opts ...CreateAuthCodeURLOption) (string, error) {
o := createAuthCodeURLOptions{}
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
CreateAuthCodeURLOption
options.CallOption
} {
return struct {
AcquireByAuthCodeOption
AcquireByDeviceCodeOption
AcquireByUsernamePasswordOption
AcquireInteractiveOption
AcquireSilentOption
CreateAuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *AcquireTokenByAuthCodeOptions:
t.claims = claims
case *acquireTokenByDeviceCodeOptions:
t.claims = claims
case *acquireTokenByUsernamePasswordOptions:
t.claims = claims
case *AcquireTokenSilentOptions:
t.claims = claims
case *createAuthCodeURLOptions:
t.claims = claims
case *InteractiveAuthOptions:
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
CreateAuthCodeURLOption
options.CallOption
} {
return struct {
AcquireByAuthCodeOption
AcquireByDeviceCodeOption
AcquireByUsernamePasswordOption
AcquireInteractiveOption
AcquireSilentOption
CreateAuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *AcquireTokenByAuthCodeOptions:
t.tenantID = tenantID
case *acquireTokenByDeviceCodeOptions:
t.tenantID = tenantID
case *acquireTokenByUsernamePasswordOptions:
t.tenantID = tenantID
case *AcquireTokenSilentOptions:
t.tenantID = tenantID
case *createAuthCodeURLOptions:
t.tenantID = tenantID
case *InteractiveAuthOptions:
t.tenantID = tenantID
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// AcquireTokenSilentOptions are all the optional settings to an AcquireTokenSilent() call.
// These are set by using various AcquireTokenSilentOption functions.
type AcquireTokenSilentOptions struct {
// Account represents the account to use. To set, use the WithSilentAccount() option.
Account Account
claims, tenantID string
}
// AcquireSilentOption is implemented by options for AcquireTokenSilent
type AcquireSilentOption interface {
acquireSilentOption()
}
// AcquireTokenSilentOption changes options inside AcquireTokenSilentOptions used in .AcquireTokenSilent().
type AcquireTokenSilentOption func(a *AcquireTokenSilentOptions)
func (AcquireTokenSilentOption) 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) {
case *AcquireTokenSilentOptions:
t.Account = account
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) {
o := AcquireTokenSilentOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return AuthResult{}, err
}
silentParameters := base.AcquireTokenSilentParameters{
Scopes: scopes,
Account: o.Account,
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
}
// AcquireTokenByAuthCodeOptions contains the optional parameters used to acquire an access token using the authorization code flow.
type AcquireTokenByAuthCodeOptions struct {
Challenge string
claims, tenantID string
}
// AcquireByAuthCodeOption is implemented by options for AcquireTokenByAuthCode
type AcquireByAuthCodeOption interface {
acquireByAuthCodeOption()
}
// AcquireTokenByAuthCodeOption changes options inside AcquireTokenByAuthCodeOptions used in .AcquireTokenByAuthCode().
type AcquireTokenByAuthCodeOption func(a *AcquireTokenByAuthCodeOptions)
func (AcquireTokenByAuthCodeOption) 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) {
case *AcquireTokenByAuthCodeOptions:
t.Challenge = challenge
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) {
o := AcquireTokenByAuthCodeOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return AuthResult{}, err
}
params := base.AcquireTokenAuthCodeParameters{
Scopes: scopes,
Code: code,
Challenge: o.Challenge,
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.
func (pca Client) Accounts() []Account {
return pca.base.AllAccounts()
}
// RemoveAccount signs the account out and forgets account from token cache.
func (pca Client) RemoveAccount(account Account) error {
pca.base.RemoveAccount(account)
return nil
}
// InteractiveAuthOptions contains the optional parameters used to acquire an access token for interactive auth code flow.
type InteractiveAuthOptions struct {
// Used to specify a custom port for the local server. http://localhost:portnumber
// All other URI components are ignored.
RedirectURI string
claims, loginHint, tenantID, domainHint string
}
// AcquireInteractiveOption is implemented by options for AcquireTokenInteractive
type AcquireInteractiveOption interface {
acquireInteractiveOption()
}
// InteractiveAuthOption changes options inside InteractiveAuthOptions used in .AcquireTokenInteractive().
type InteractiveAuthOption func(*InteractiveAuthOptions)
func (InteractiveAuthOption) acquireInteractiveOption() {}
// WithLoginHint pre-populates the login prompt with a username.
func WithLoginHint(username string) interface {
AcquireInteractiveOption
CreateAuthCodeURLOption
options.CallOption
} {
return struct {
AcquireInteractiveOption
CreateAuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *createAuthCodeURLOptions:
t.loginHint = username
case *InteractiveAuthOptions:
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
CreateAuthCodeURLOption
options.CallOption
} {
return struct {
AcquireInteractiveOption
CreateAuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *createAuthCodeURLOptions:
t.domainHint = domain
case *InteractiveAuthOptions:
t.domainHint = domain
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// WithRedirectURI uses the specified redirect URI for interactive auth.
func WithRedirectURI(redirectURI string) interface {
AcquireInteractiveOption
options.CallOption
} {
return struct {
AcquireInteractiveOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *InteractiveAuthOptions:
t.RedirectURI = redirectURI
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) {
o := InteractiveAuthOptions{}
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
if o.RedirectURI != "" {
redirectURL, err = url.Parse(o.RedirectURI)
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
}