managed_identity_credential.go

  1//go:build go1.18
  2// +build go1.18
  3
  4// Copyright (c) Microsoft Corporation. All rights reserved.
  5// Licensed under the MIT License.
  6
  7package azidentity
  8
  9import (
 10	"context"
 11	"fmt"
 12	"strings"
 13
 14	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
 15	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
 16	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
 17	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
 18)
 19
 20const credNameManagedIdentity = "ManagedIdentityCredential"
 21
 22type managedIdentityIDKind int
 23
 24const (
 25	miClientID   managedIdentityIDKind = 0
 26	miResourceID managedIdentityIDKind = 1
 27)
 28
 29// ManagedIDKind identifies the ID of a managed identity as either a client or resource ID
 30type ManagedIDKind interface {
 31	fmt.Stringer
 32	idKind() managedIdentityIDKind
 33}
 34
 35// ClientID is the client ID of a user-assigned managed identity.
 36type ClientID string
 37
 38func (ClientID) idKind() managedIdentityIDKind {
 39	return miClientID
 40}
 41
 42// String returns the string value of the ID.
 43func (c ClientID) String() string {
 44	return string(c)
 45}
 46
 47// ResourceID is the resource ID of a user-assigned managed identity.
 48type ResourceID string
 49
 50func (ResourceID) idKind() managedIdentityIDKind {
 51	return miResourceID
 52}
 53
 54// String returns the string value of the ID.
 55func (r ResourceID) String() string {
 56	return string(r)
 57}
 58
 59// ManagedIdentityCredentialOptions contains optional parameters for ManagedIdentityCredential.
 60type ManagedIdentityCredentialOptions struct {
 61	azcore.ClientOptions
 62
 63	// ID is the ID of a managed identity the credential should authenticate. Set this field to use a specific identity
 64	// instead of the hosting environment's default. The value may be the identity's client ID or resource ID, but note that
 65	// some platforms don't accept resource IDs.
 66	ID ManagedIDKind
 67
 68	// dac indicates whether the credential is part of DefaultAzureCredential. When true, and the environment doesn't have
 69	// configuration for a specific managed identity API, the credential tries to determine whether IMDS is available before
 70	// sending its first token request. It does this by sending a malformed request with a short timeout. Any response to that
 71	// request is taken to mean IMDS is available, in which case the credential will send ordinary token requests thereafter
 72	// with no special timeout. The purpose of this behavior is to prevent a very long timeout when IMDS isn't available.
 73	dac bool
 74}
 75
 76// ManagedIdentityCredential authenticates an Azure managed identity in any hosting environment supporting managed identities.
 77// This credential authenticates a system-assigned identity by default. Use ManagedIdentityCredentialOptions.ID to specify a
 78// user-assigned identity. See Microsoft Entra ID documentation for more information about managed identities:
 79// https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview
 80type ManagedIdentityCredential struct {
 81	client *confidentialClient
 82	mic    *managedIdentityClient
 83}
 84
 85// NewManagedIdentityCredential creates a ManagedIdentityCredential. Pass nil to accept default options.
 86func NewManagedIdentityCredential(options *ManagedIdentityCredentialOptions) (*ManagedIdentityCredential, error) {
 87	if options == nil {
 88		options = &ManagedIdentityCredentialOptions{}
 89	}
 90	mic, err := newManagedIdentityClient(options)
 91	if err != nil {
 92		return nil, err
 93	}
 94	cred := confidential.NewCredFromTokenProvider(mic.provideToken)
 95
 96	// It's okay to give MSAL an invalid client ID because MSAL will use it only as part of a cache key.
 97	// ManagedIdentityClient handles all the details of authentication and won't receive this value from MSAL.
 98	clientID := "SYSTEM-ASSIGNED-MANAGED-IDENTITY"
 99	if options.ID != nil {
100		clientID = options.ID.String()
101	}
102	// similarly, it's okay to give MSAL an incorrect tenant because MSAL won't use the value
103	c, err := newConfidentialClient("common", clientID, credNameManagedIdentity, cred, confidentialClientOptions{
104		ClientOptions: options.ClientOptions,
105	})
106	if err != nil {
107		return nil, err
108	}
109	return &ManagedIdentityCredential{client: c, mic: mic}, nil
110}
111
112// GetToken requests an access token from the hosting environment. This method is called automatically by Azure SDK clients.
113func (c *ManagedIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
114	var err error
115	ctx, endSpan := runtime.StartSpan(ctx, credNameManagedIdentity+"."+traceOpGetToken, c.client.azClient.Tracer(), nil)
116	defer func() { endSpan(err) }()
117
118	if len(opts.Scopes) != 1 {
119		err = fmt.Errorf("%s.GetToken() requires exactly one scope", credNameManagedIdentity)
120		return azcore.AccessToken{}, err
121	}
122	// managed identity endpoints require a Microsoft Entra ID v1 resource (i.e. token audience), not a v2 scope, so we remove "/.default" here
123	opts.Scopes = []string{strings.TrimSuffix(opts.Scopes[0], defaultSuffix)}
124	tk, err := c.client.GetToken(ctx, opts)
125	return tk, err
126}
127
128var _ azcore.TokenCredential = (*ManagedIdentityCredential)(nil)