2020-02-21 03:58:17 +00:00
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
|
|
// Licensed under the MIT license.
|
|
|
|
|
|
|
|
/*
|
|
|
|
Package confidential provides a client for authentication of "confidential" applications.
|
|
|
|
A "confidential" application is defined as an app that run on servers. They are considered
|
|
|
|
difficult to access and for that reason capable of keeping an application secret.
|
|
|
|
Confidential clients can hold configuration-time secrets.
|
|
|
|
*/
|
|
|
|
package confidential
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto"
|
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/pem"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
"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/exported"
|
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
|
|
|
/*
|
|
|
|
Design note:
|
|
|
|
|
|
|
|
confidential.Client uses base.Client as an embedded type. base.Client 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.
|
|
|
|
|
|
|
|
Duplicate Calls shared between public.Client and this package:
|
|
|
|
There is some duplicate call options provided here that are the same as in public.Client . This
|
|
|
|
is a design choices. Go proverb(https://www.youtube.com/watch?v=PAAkCSZUG1c&t=9m28s):
|
|
|
|
"a little copying is better than a little dependency". Yes, we could have another package with
|
|
|
|
shared options (fail). That divides like 2 options from all others which makes the user look
|
|
|
|
through more docs. We can have all clients in one package, but I think separate packages
|
|
|
|
here makes for better naming (public.Client vs client.PublicClient). So I chose a little
|
|
|
|
duplication.
|
|
|
|
|
|
|
|
.Net People, Take note on X509:
|
|
|
|
This uses x509.Certificates and private keys. x509 does not store private keys. .Net
|
|
|
|
has some x509.Certificate2 thing that has private keys, but that is just some bullcrap that .Net
|
2023-05-11 13:23:47 +00:00
|
|
|
added, it doesn't exist in real life. As such I've put a PEM decoder into here.
|
2020-02-21 03:58:17 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
// TODO(msal): This should have example code for each method on client using Go's example doc framework.
|
|
|
|
// base usage details should be include in the package documentation.
|
|
|
|
|
|
|
|
// 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
|
|
|
// CertFromPEM converts a PEM file (.pem or .key) for use with [NewCredFromCert]. The file
|
2020-02-21 03:58:17 +00:00
|
|
|
// must contain the public certificate and the private key. If a PEM block is encrypted and
|
|
|
|
// password is not an empty string, it attempts to decrypt the PEM blocks using the password.
|
|
|
|
// Multiple certs are due to certificate chaining for use cases like TLS that sign from root to leaf.
|
|
|
|
func CertFromPEM(pemData []byte, password string) ([]*x509.Certificate, crypto.PrivateKey, error) {
|
|
|
|
var certs []*x509.Certificate
|
|
|
|
var priv crypto.PrivateKey
|
|
|
|
for {
|
|
|
|
block, rest := pem.Decode(pemData)
|
|
|
|
if block == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
//nolint:staticcheck // x509.IsEncryptedPEMBlock and x509.DecryptPEMBlock are deprecated. They are used here only to support a usecase.
|
|
|
|
if x509.IsEncryptedPEMBlock(block) {
|
|
|
|
b, err := x509.DecryptPEMBlock(block, []byte(password))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("could not decrypt encrypted PEM block: %v", err)
|
|
|
|
}
|
|
|
|
block, _ = pem.Decode(b)
|
|
|
|
if block == nil {
|
|
|
|
return nil, nil, fmt.Errorf("encounter encrypted PEM block that did not decode")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch block.Type {
|
|
|
|
case "CERTIFICATE":
|
|
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("block labelled 'CERTIFICATE' could not be parsed by x509: %v", err)
|
|
|
|
}
|
|
|
|
certs = append(certs, cert)
|
|
|
|
case "PRIVATE KEY":
|
|
|
|
if priv != nil {
|
|
|
|
return nil, nil, errors.New("found multiple private key blocks")
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
priv, err = x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("could not decode private key: %v", err)
|
|
|
|
}
|
|
|
|
case "RSA PRIVATE KEY":
|
|
|
|
if priv != nil {
|
|
|
|
return nil, nil, errors.New("found multiple private key blocks")
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
priv, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("could not decode private key: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
pemData = rest
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(certs) == 0 {
|
|
|
|
return nil, nil, fmt.Errorf("no certificates found")
|
|
|
|
}
|
|
|
|
|
|
|
|
if priv == nil {
|
|
|
|
return nil, nil, fmt.Errorf("no private key found")
|
|
|
|
}
|
|
|
|
|
|
|
|
return certs, priv, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// AssertionRequestOptions has required information for client assertion claims
|
|
|
|
type AssertionRequestOptions = exported.AssertionRequestOptions
|
|
|
|
|
|
|
|
// Credential represents the credential used in confidential client flows.
|
|
|
|
type Credential struct {
|
|
|
|
secret string
|
|
|
|
|
|
|
|
cert *x509.Certificate
|
|
|
|
key crypto.PrivateKey
|
|
|
|
x5c []string
|
|
|
|
|
|
|
|
assertionCallback func(context.Context, AssertionRequestOptions) (string, error)
|
|
|
|
|
|
|
|
tokenProvider func(context.Context, TokenProviderParameters) (TokenProviderResult, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// toInternal returns the accesstokens.Credential that is used internally. The current structure of the
|
|
|
|
// code requires that client.go, requests.go and confidential.go share a credential type without
|
|
|
|
// having import recursion. That requires the type used between is in a shared package. Therefore
|
|
|
|
// we have this.
|
|
|
|
func (c Credential) toInternal() (*accesstokens.Credential, error) {
|
|
|
|
if c.secret != "" {
|
|
|
|
return &accesstokens.Credential{Secret: c.secret}, nil
|
|
|
|
}
|
|
|
|
if c.cert != nil {
|
|
|
|
if c.key == nil {
|
|
|
|
return nil, errors.New("missing private key for certificate")
|
|
|
|
}
|
|
|
|
return &accesstokens.Credential{Cert: c.cert, Key: c.key, X5c: c.x5c}, nil
|
|
|
|
}
|
|
|
|
if c.key != nil {
|
|
|
|
return nil, errors.New("missing certificate for private key")
|
|
|
|
}
|
|
|
|
if c.assertionCallback != nil {
|
|
|
|
return &accesstokens.Credential{AssertionCallback: c.assertionCallback}, nil
|
|
|
|
}
|
|
|
|
if c.tokenProvider != nil {
|
|
|
|
return &accesstokens.Credential{TokenProvider: c.tokenProvider}, nil
|
|
|
|
}
|
|
|
|
return nil, errors.New("invalid credential")
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewCredFromSecret creates a Credential from a secret.
|
|
|
|
func NewCredFromSecret(secret string) (Credential, error) {
|
|
|
|
if secret == "" {
|
|
|
|
return Credential{}, errors.New("secret can't be empty string")
|
|
|
|
}
|
|
|
|
return Credential{secret: secret}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewCredFromAssertionCallback creates a Credential that invokes a callback to get assertions
|
|
|
|
// authenticating the application. The callback must be thread safe.
|
|
|
|
func NewCredFromAssertionCallback(callback func(context.Context, AssertionRequestOptions) (string, error)) Credential {
|
|
|
|
return Credential{assertionCallback: callback}
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// NewCredFromCert creates a Credential from a certificate or chain of certificates and an RSA private key
|
|
|
|
// as returned by [CertFromPEM].
|
|
|
|
func NewCredFromCert(certs []*x509.Certificate, key crypto.PrivateKey) (Credential, error) {
|
2020-02-21 03:58:17 +00:00
|
|
|
cred := Credential{key: key}
|
|
|
|
k, ok := key.(*rsa.PrivateKey)
|
|
|
|
if !ok {
|
|
|
|
return cred, errors.New("key must be an RSA key")
|
|
|
|
}
|
|
|
|
for _, cert := range certs {
|
|
|
|
if cert == nil {
|
|
|
|
// not returning an error here because certs may still contain a sufficient cert/key pair
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
certKey, ok := cert.PublicKey.(*rsa.PublicKey)
|
|
|
|
if ok && k.E == certKey.E && k.N.Cmp(certKey.N) == 0 {
|
|
|
|
// We know this is the signing cert because its public key matches the given private key.
|
|
|
|
// This cert must be first in x5c.
|
|
|
|
cred.cert = cert
|
|
|
|
cred.x5c = append([]string{base64.StdEncoding.EncodeToString(cert.Raw)}, cred.x5c...)
|
|
|
|
} else {
|
|
|
|
cred.x5c = append(cred.x5c, base64.StdEncoding.EncodeToString(cert.Raw))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if cred.cert == nil {
|
|
|
|
return cred, errors.New("key doesn't match any certificate")
|
|
|
|
}
|
|
|
|
return cred, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TokenProviderParameters is the authentication parameters passed to token providers
|
|
|
|
type TokenProviderParameters = exported.TokenProviderParameters
|
|
|
|
|
|
|
|
// TokenProviderResult is the authentication result returned by custom token providers
|
|
|
|
type TokenProviderResult = exported.TokenProviderResult
|
|
|
|
|
|
|
|
// NewCredFromTokenProvider creates a Credential from a function that provides access tokens. The function
|
|
|
|
// must be concurrency safe. This is intended only to allow the Azure SDK to cache MSI tokens. It isn't
|
|
|
|
// useful to applications in general because the token provider must implement all authentication logic.
|
|
|
|
func NewCredFromTokenProvider(provider func(context.Context, TokenProviderParameters) (TokenProviderResult, error)) Credential {
|
|
|
|
return Credential{tokenProvider: provider}
|
|
|
|
}
|
|
|
|
|
|
|
|
// AutoDetectRegion instructs MSAL Go to auto detect region for Azure regional token service.
|
|
|
|
func AutoDetectRegion() string {
|
|
|
|
return "TryAutoDetect"
|
|
|
|
}
|
|
|
|
|
|
|
|
// Client is a representation of authentication client for confidential applications as defined in the
|
|
|
|
// package doc. A new Client should be created PER SERVICE USER.
|
|
|
|
// For more information, visit https://docs.microsoft.com/azure/active-directory/develop/msal-client-applications
|
|
|
|
type Client struct {
|
|
|
|
base base.Client
|
|
|
|
cred *accesstokens.Credential
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// clientOptions are optional settings for New(). These options are set using various functions
|
2020-02-21 03:58:17 +00:00
|
|
|
// returning Option calls.
|
2023-05-11 13:23:47 +00:00
|
|
|
type clientOptions struct {
|
|
|
|
accessor cache.ExportReplace
|
|
|
|
authority, azureRegion string
|
|
|
|
capabilities []string
|
|
|
|
disableInstanceDiscovery, sendX5C bool
|
|
|
|
httpClient ops.HTTPClient
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Option is an optional argument to New().
|
2023-05-11 13:23:47 +00:00
|
|
|
type Option func(o *clientOptions)
|
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.
|
|
|
|
func WithCache(accessor cache.ExportReplace) Option {
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithX5C specifies if x5c claim(public key of the certificate) should be sent to STS to enable Subject Name Issuer Authentication.
|
|
|
|
func WithX5C() Option {
|
2023-05-11 13:23:47 +00:00
|
|
|
return func(o *clientOptions) {
|
|
|
|
o.sendX5C = true
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithAzureRegion sets the region(preferred) or Confidential.AutoDetectRegion() for auto detecting region.
|
|
|
|
// Region names as per https://azure.microsoft.com/en-ca/global-infrastructure/geographies/.
|
|
|
|
// See https://aka.ms/region-map for more details on region names.
|
|
|
|
// The region value should be short region name for the region where the service is deployed.
|
|
|
|
// For example "centralus" is short name for region Central US.
|
|
|
|
// Not all auth flows can use the regional token service.
|
|
|
|
// Service To Service (client credential flow) tokens can be obtained from the regional service.
|
|
|
|
// Requires configuration at the tenant level.
|
|
|
|
// Auto-detection works on a limited number of Azure artifacts (VMs, Azure functions).
|
|
|
|
// If auto-detection fails, the non-regional endpoint will be used.
|
|
|
|
// If an invalid region name is provided, the non-regional endpoint MIGHT be used or the token request MIGHT fail.
|
|
|
|
func WithAzureRegion(val string) Option {
|
2023-05-11 13:23:47 +00:00
|
|
|
return func(o *clientOptions) {
|
|
|
|
o.azureRegion = val
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// New is the constructor for Client. authority is the URL of a token authority such as "https://login.microsoftonline.com/<your tenant>".
|
|
|
|
// If the Client will connect directly to AD FS, use "adfs" for the tenant. clientID is the application's client ID (also called its
|
|
|
|
// "application ID").
|
|
|
|
func New(authority, clientID string, cred Credential, options ...Option) (Client, error) {
|
2020-02-21 03:58:17 +00:00
|
|
|
internalCred, err := cred.toInternal()
|
|
|
|
if err != nil {
|
|
|
|
return Client{}, err
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
opts := clientOptions{
|
|
|
|
authority: authority,
|
|
|
|
// if the caller specified a token provider, it will handle all details of authentication, using Client only as a token cache
|
|
|
|
disableInstanceDiscovery: cred.tokenProvider != nil,
|
|
|
|
httpClient: shared.DefaultClient,
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
|
|
|
for _, o := range options {
|
|
|
|
o(&opts)
|
|
|
|
}
|
|
|
|
baseOpts := []base.Option{
|
2023-05-11 13:23:47 +00:00
|
|
|
base.WithCacheAccessor(opts.accessor),
|
2020-02-21 03:58:17 +00:00
|
|
|
base.WithClientCapabilities(opts.capabilities),
|
|
|
|
base.WithInstanceDiscovery(!opts.disableInstanceDiscovery),
|
2023-05-11 13:23:47 +00:00
|
|
|
base.WithRegionDetection(opts.azureRegion),
|
|
|
|
base.WithX5C(opts.sendX5C),
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|
2023-05-11 13:23:47 +00:00
|
|
|
base, err := base.New(clientID, opts.authority, oauth.New(opts.httpClient), baseOpts...)
|
2020-02-21 03:58:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return Client{}, err
|
|
|
|
}
|
|
|
|
base.AuthParams.IsConfidentialClient = true
|
|
|
|
|
|
|
|
return Client{base: base, cred: internalCred}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// authCodeURLOptions contains options for AuthCodeURL
|
|
|
|
type authCodeURLOptions struct {
|
|
|
|
claims, loginHint, tenantID, domainHint string
|
|
|
|
}
|
|
|
|
|
|
|
|
// AuthCodeURLOption is implemented by options for AuthCodeURL
|
|
|
|
type AuthCodeURLOption interface {
|
|
|
|
authCodeURLOption()
|
|
|
|
}
|
|
|
|
|
|
|
|
// AuthCodeURL creates a URL used to acquire an authorization code. Users need to call CreateAuthorizationCodeURLParameters and pass it in.
|
|
|
|
//
|
|
|
|
// Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID]
|
|
|
|
func (cca Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, opts ...AuthCodeURLOption) (string, error) {
|
|
|
|
o := authCodeURLOptions{}
|
|
|
|
if err := options.ApplyOptions(&o, opts); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
ap, err := cca.base.AuthParams.WithTenant(o.tenantID)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
ap.Claims = o.claims
|
|
|
|
ap.LoginHint = o.loginHint
|
|
|
|
ap.DomainHint = o.domainHint
|
|
|
|
return cca.base.AuthCodeURL(ctx, clientID, redirectURI, scopes, ap)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithLoginHint pre-populates the login prompt with a username.
|
|
|
|
func WithLoginHint(username string) interface {
|
|
|
|
AuthCodeURLOption
|
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AuthCodeURLOption
|
|
|
|
options.CallOption
|
|
|
|
}{
|
|
|
|
CallOption: options.NewCallOption(
|
|
|
|
func(a any) error {
|
|
|
|
switch t := a.(type) {
|
|
|
|
case *authCodeURLOptions:
|
|
|
|
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 {
|
|
|
|
AuthCodeURLOption
|
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AuthCodeURLOption
|
|
|
|
options.CallOption
|
|
|
|
}{
|
|
|
|
CallOption: options.NewCallOption(
|
|
|
|
func(a any) error {
|
|
|
|
switch t := a.(type) {
|
|
|
|
case *authCodeURLOptions:
|
|
|
|
t.domainHint = domain
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unexpected options type %T", a)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
AcquireByCredentialOption
|
|
|
|
AcquireOnBehalfOfOption
|
|
|
|
AcquireSilentOption
|
|
|
|
AuthCodeURLOption
|
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AcquireByAuthCodeOption
|
|
|
|
AcquireByCredentialOption
|
|
|
|
AcquireOnBehalfOfOption
|
|
|
|
AcquireSilentOption
|
|
|
|
AuthCodeURLOption
|
|
|
|
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 *acquireTokenByCredentialOptions:
|
|
|
|
t.claims = claims
|
|
|
|
case *acquireTokenOnBehalfOfOptions:
|
|
|
|
t.claims = claims
|
2023-05-11 13:23:47 +00:00
|
|
|
case *acquireTokenSilentOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.claims = claims
|
|
|
|
case *authCodeURLOptions:
|
|
|
|
t.claims = claims
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unexpected options type %T", a)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-11 13:23:47 +00:00
|
|
|
// WithTenantID specifies a tenant for a single authentication. It may be different than the tenant set in [New].
|
2020-02-21 03:58:17 +00:00
|
|
|
// This option is valid for any token acquisition method.
|
|
|
|
func WithTenantID(tenantID string) interface {
|
|
|
|
AcquireByAuthCodeOption
|
|
|
|
AcquireByCredentialOption
|
|
|
|
AcquireOnBehalfOfOption
|
|
|
|
AcquireSilentOption
|
|
|
|
AuthCodeURLOption
|
|
|
|
options.CallOption
|
|
|
|
} {
|
|
|
|
return struct {
|
|
|
|
AcquireByAuthCodeOption
|
|
|
|
AcquireByCredentialOption
|
|
|
|
AcquireOnBehalfOfOption
|
|
|
|
AcquireSilentOption
|
|
|
|
AuthCodeURLOption
|
|
|
|
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 *acquireTokenByCredentialOptions:
|
|
|
|
t.tenantID = tenantID
|
|
|
|
case *acquireTokenOnBehalfOfOptions:
|
|
|
|
t.tenantID = tenantID
|
2023-05-11 13:23:47 +00:00
|
|
|
case *acquireTokenSilentOptions:
|
2020-02-21 03:58:17 +00:00
|
|
|
t.tenantID = tenantID
|
|
|
|
case *authCodeURLOptions:
|
|
|
|
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 (cca 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
|
|
|
|
}
|
|
|
|
|
|
|
|
if o.claims != "" {
|
|
|
|
return AuthResult{}, errors.New("call another AcquireToken method to request a new token having these claims")
|
|
|
|
}
|
|
|
|
|
|
|
|
silentParameters := base.AcquireTokenSilentParameters{
|
|
|
|
Scopes: scopes,
|
2023-05-11 13:23:47 +00:00
|
|
|
Account: o.account,
|
2020-02-21 03:58:17 +00:00
|
|
|
RequestType: accesstokens.ATConfidential,
|
|
|
|
Credential: cca.cred,
|
2023-05-11 13:23:47 +00:00
|
|
|
IsAppCache: o.account.IsZero(),
|
2020-02-21 03:58:17 +00:00
|
|
|
TenantID: o.tenantID,
|
|
|
|
}
|
|
|
|
|
|
|
|
return cca.base.AcquireTokenSilent(ctx, silentParameters)
|
|
|
|
}
|
|
|
|
|
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 challenge 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 (cca 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.ATConfidential,
|
|
|
|
Credential: cca.cred, // This setting differs from public.Client.AcquireTokenByAuthCode
|
|
|
|
RedirectURI: redirectURI,
|
|
|
|
TenantID: o.tenantID,
|
|
|
|
}
|
|
|
|
|
|
|
|
return cca.base.AcquireTokenByAuthCode(ctx, params)
|
|
|
|
}
|
|
|
|
|
|
|
|
// acquireTokenByCredentialOptions contains optional configuration for AcquireTokenByCredential
|
|
|
|
type acquireTokenByCredentialOptions struct {
|
|
|
|
claims, tenantID string
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireByCredentialOption is implemented by options for AcquireTokenByCredential
|
|
|
|
type AcquireByCredentialOption interface {
|
|
|
|
acquireByCredOption()
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireTokenByCredential acquires a security token from the authority, using the client credentials grant.
|
|
|
|
//
|
|
|
|
// Options: [WithClaims], [WithTenantID]
|
|
|
|
func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string, opts ...AcquireByCredentialOption) (AuthResult, error) {
|
|
|
|
o := acquireTokenByCredentialOptions{}
|
|
|
|
err := options.ApplyOptions(&o, opts)
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
authParams, err := cca.base.AuthParams.WithTenant(o.tenantID)
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
authParams.Scopes = scopes
|
|
|
|
authParams.AuthorizationType = authority.ATClientCredentials
|
|
|
|
authParams.Claims = o.claims
|
|
|
|
|
|
|
|
token, err := cca.base.Token.Credential(ctx, authParams, cca.cred)
|
|
|
|
if err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
return cca.base.AuthResultFromToken(ctx, authParams, token, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
// acquireTokenOnBehalfOfOptions contains optional configuration for AcquireTokenOnBehalfOf
|
|
|
|
type acquireTokenOnBehalfOfOptions struct {
|
|
|
|
claims, tenantID string
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireOnBehalfOfOption is implemented by options for AcquireTokenOnBehalfOf
|
|
|
|
type AcquireOnBehalfOfOption interface {
|
|
|
|
acquireOBOOption()
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcquireTokenOnBehalfOf acquires a security token for an app using middle tier apps access token.
|
|
|
|
// Refer https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow.
|
|
|
|
//
|
|
|
|
// Options: [WithClaims], [WithTenantID]
|
|
|
|
func (cca Client) AcquireTokenOnBehalfOf(ctx context.Context, userAssertion string, scopes []string, opts ...AcquireOnBehalfOfOption) (AuthResult, error) {
|
|
|
|
o := acquireTokenOnBehalfOfOptions{}
|
|
|
|
if err := options.ApplyOptions(&o, opts); err != nil {
|
|
|
|
return AuthResult{}, err
|
|
|
|
}
|
|
|
|
params := base.AcquireTokenOnBehalfOfParameters{
|
|
|
|
Scopes: scopes,
|
|
|
|
UserAssertion: userAssertion,
|
|
|
|
Claims: o.claims,
|
|
|
|
Credential: cca.cred,
|
|
|
|
TenantID: o.tenantID,
|
|
|
|
}
|
|
|
|
return cca.base.AcquireTokenOnBehalfOf(ctx, params)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Account gets the account in the token cache with the specified homeAccountID.
|
2023-05-11 13:23:47 +00:00
|
|
|
func (cca Client) Account(ctx context.Context, accountID string) (Account, error) {
|
|
|
|
return cca.base.Account(ctx, accountID)
|
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 (cca Client) RemoveAccount(ctx context.Context, account Account) error {
|
|
|
|
return cca.base.RemoveAccount(ctx, account)
|
2020-02-21 03:58:17 +00:00
|
|
|
}
|