device_code_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
 13	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
 14	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
 15	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
 16)
 17
 18const credNameDeviceCode = "DeviceCodeCredential"
 19
 20// DeviceCodeCredentialOptions contains optional parameters for DeviceCodeCredential.
 21type DeviceCodeCredentialOptions struct {
 22	azcore.ClientOptions
 23
 24	// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire
 25	// tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant.
 26	AdditionallyAllowedTenants []string
 27
 28	// authenticationRecord returned by a call to a credential's Authenticate method. Set this option
 29	// to enable the credential to use data from a previous authentication.
 30	authenticationRecord authenticationRecord
 31
 32	// ClientID is the ID of the application users will authenticate to.
 33	// Defaults to the ID of an Azure development application.
 34	ClientID string
 35
 36	// disableAutomaticAuthentication prevents the credential from automatically prompting the user to authenticate.
 37	// When this option is true, GetToken will return authenticationRequiredError when user interaction is necessary
 38	// to acquire a token.
 39	disableAutomaticAuthentication bool
 40
 41	// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
 42	// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
 43	// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
 44	// the application responsible for ensuring the configured authority is valid and trustworthy.
 45	DisableInstanceDiscovery bool
 46
 47	// TenantID is the Microsoft Entra tenant the credential authenticates in. Defaults to the
 48	// "organizations" tenant, which can authenticate work and school accounts. Required for single-tenant
 49	// applications.
 50	TenantID string
 51
 52	// tokenCachePersistenceOptions enables persistent token caching when not nil.
 53	tokenCachePersistenceOptions *tokenCachePersistenceOptions
 54
 55	// UserPrompt controls how the credential presents authentication instructions. The credential calls
 56	// this function with authentication details when it receives a device code. By default, the credential
 57	// prints these details to stdout.
 58	UserPrompt func(context.Context, DeviceCodeMessage) error
 59}
 60
 61func (o *DeviceCodeCredentialOptions) init() {
 62	if o.TenantID == "" {
 63		o.TenantID = organizationsTenantID
 64	}
 65	if o.ClientID == "" {
 66		o.ClientID = developerSignOnClientID
 67	}
 68	if o.UserPrompt == nil {
 69		o.UserPrompt = func(ctx context.Context, dc DeviceCodeMessage) error {
 70			fmt.Println(dc.Message)
 71			return nil
 72		}
 73	}
 74}
 75
 76// DeviceCodeMessage contains the information a user needs to complete authentication.
 77type DeviceCodeMessage struct {
 78	// UserCode is the user code returned by the service.
 79	UserCode string `json:"user_code"`
 80	// VerificationURL is the URL at which the user must authenticate.
 81	VerificationURL string `json:"verification_uri"`
 82	// Message is user instruction from Microsoft Entra ID.
 83	Message string `json:"message"`
 84}
 85
 86// DeviceCodeCredential acquires tokens for a user via the device code flow, which has the
 87// user browse to a Microsoft Entra URL, enter a code, and authenticate. It's useful
 88// for authenticating a user in an environment without a web browser, such as an SSH session.
 89// If a web browser is available, [InteractiveBrowserCredential] is more convenient because it
 90// automatically opens a browser to the login page.
 91type DeviceCodeCredential struct {
 92	client *publicClient
 93}
 94
 95// NewDeviceCodeCredential creates a DeviceCodeCredential. Pass nil to accept default options.
 96func NewDeviceCodeCredential(options *DeviceCodeCredentialOptions) (*DeviceCodeCredential, error) {
 97	cp := DeviceCodeCredentialOptions{}
 98	if options != nil {
 99		cp = *options
100	}
101	cp.init()
102	msalOpts := publicClientOptions{
103		AdditionallyAllowedTenants:     cp.AdditionallyAllowedTenants,
104		ClientOptions:                  cp.ClientOptions,
105		DeviceCodePrompt:               cp.UserPrompt,
106		DisableAutomaticAuthentication: cp.disableAutomaticAuthentication,
107		DisableInstanceDiscovery:       cp.DisableInstanceDiscovery,
108		Record:                         cp.authenticationRecord,
109		TokenCachePersistenceOptions:   cp.tokenCachePersistenceOptions,
110	}
111	c, err := newPublicClient(cp.TenantID, cp.ClientID, credNameDeviceCode, msalOpts)
112	if err != nil {
113		return nil, err
114	}
115	c.name = credNameDeviceCode
116	return &DeviceCodeCredential{client: c}, nil
117}
118
119// Authenticate a user via the device code flow. Subsequent calls to GetToken will automatically use the returned AuthenticationRecord.
120func (c *DeviceCodeCredential) authenticate(ctx context.Context, opts *policy.TokenRequestOptions) (authenticationRecord, error) {
121	var err error
122	ctx, endSpan := runtime.StartSpan(ctx, credNameDeviceCode+"."+traceOpAuthenticate, c.client.azClient.Tracer(), nil)
123	defer func() { endSpan(err) }()
124	tk, err := c.client.Authenticate(ctx, opts)
125	return tk, err
126}
127
128// GetToken requests an access token from Microsoft Entra ID. It will begin the device code flow and poll until the user completes authentication.
129// This method is called automatically by Azure SDK clients.
130func (c *DeviceCodeCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
131	var err error
132	ctx, endSpan := runtime.StartSpan(ctx, credNameDeviceCode+"."+traceOpGetToken, c.client.azClient.Tracer(), nil)
133	defer func() { endSpan(err) }()
134	tk, err := c.client.GetToken(ctx, opts)
135	return tk, err
136}
137
138var _ azcore.TokenCredential = (*DeviceCodeCredential)(nil)