1// Copyright (c) Microsoft Corporation. All rights reserved. 2// Licensed under the MIT License. 3 4package azidentity 5 6import ( 7 "context" 8 "errors" 9 "fmt" 10 "time" 11 12 "github.com/Azure/azure-sdk-for-go/sdk/azcore" 13) 14 15const ( 16 deviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code" 17) 18 19// DeviceCodeCredentialOptions provide options that can configure DeviceCodeCredential instead of using the default values. 20// All zero-value fields will be initialized with their default values. Please note, that both the TenantID or ClientID fields should 21// changed together if default values are not desired. 22type DeviceCodeCredentialOptions struct { 23 // Gets the Azure Active Directory tenant (directory) ID of the service principal 24 // The default value is "organizations". If this value is changed, then also change ClientID to the corresponding value. 25 TenantID string 26 // Gets the client (application) ID of the service principal 27 // The default value is the developer sign on ID for the corresponding "organizations" TenantID. 28 ClientID string 29 // The callback function used to send the login message back to the user 30 // The default will print device code log in information to stdout. 31 UserPrompt func(DeviceCodeMessage) 32 // The host of the Azure Active Directory authority. The default is AzurePublicCloud. 33 // Leave empty to allow overriding the value from the AZURE_AUTHORITY_HOST environment variable. 34 AuthorityHost string 35 // HTTPClient sets the transport for making HTTP requests 36 // Leave this as nil to use the default HTTP transport 37 HTTPClient azcore.Transport 38 // Retry configures the built-in retry policy behavior 39 Retry azcore.RetryOptions 40 // Telemetry configures the built-in telemetry policy behavior 41 Telemetry azcore.TelemetryOptions 42 // Logging configures the built-in logging policy behavior. 43 Logging azcore.LogOptions 44} 45 46// init provides the default settings for DeviceCodeCredential. 47// It will set the following default values: 48// TenantID set to "organizations". 49// ClientID set to the default developer sign on client ID "04b07795-8ddb-461a-bbee-02f9e1bf7b46". 50// UserPrompt set to output login information for the user to stdout. 51func (o *DeviceCodeCredentialOptions) init() { 52 if o.TenantID == "" { 53 o.TenantID = organizationsTenantID 54 } 55 if o.ClientID == "" { 56 o.ClientID = developerSignOnClientID 57 } 58 if o.UserPrompt == nil { 59 o.UserPrompt = func(dc DeviceCodeMessage) { 60 fmt.Println(dc.Message) 61 } 62 } 63} 64 65// DeviceCodeMessage is used to store device code related information to help the user login and allow the device code flow to continue 66// to request a token to authenticate a user. 67type DeviceCodeMessage struct { 68 // User code returned by the service. 69 UserCode string `json:"user_code"` 70 // Verification URL where the user must navigate to authenticate using the device code and credentials. 71 VerificationURL string `json:"verification_uri"` 72 // User friendly text response that can be used for display purposes. 73 Message string `json:"message"` 74} 75 76// DeviceCodeCredential authenticates a user using the device code flow, and provides access tokens for that user account. 77// For more information on the device code authentication flow see: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code. 78type DeviceCodeCredential struct { 79 client *aadIdentityClient 80 tenantID string // Gets the Azure Active Directory tenant (directory) ID of the service principal 81 clientID string // Gets the client (application) ID of the service principal 82 userPrompt func(DeviceCodeMessage) // Sends the user a message with a verification URL and device code to sign in to the login server 83 refreshToken string // Gets the refresh token sent from the service and will be used to retreive new access tokens after the initial request for a token. Thread safety for updates is handled in the NewAuthenticationPolicy since only one goroutine will be updating at a time 84} 85 86// NewDeviceCodeCredential constructs a new DeviceCodeCredential used to authenticate against Azure Active Directory with a device code. 87// options: Options used to configure the management of the requests sent to Azure Active Directory, please see DeviceCodeCredentialOptions for a description of each field. 88func NewDeviceCodeCredential(options *DeviceCodeCredentialOptions) (*DeviceCodeCredential, error) { 89 cp := DeviceCodeCredentialOptions{} 90 if options != nil { 91 cp = *options 92 } 93 cp.init() 94 if !validTenantID(cp.TenantID) { 95 return nil, &CredentialUnavailableError{credentialType: "Device Code Credential", message: tenantIDValidationErr} 96 } 97 authorityHost, err := setAuthorityHost(cp.AuthorityHost) 98 if err != nil { 99 return nil, err 100 } 101 c, err := newAADIdentityClient(authorityHost, pipelineOptions{HTTPClient: cp.HTTPClient, Retry: cp.Retry, Telemetry: cp.Telemetry, Logging: cp.Logging}) 102 if err != nil { 103 return nil, err 104 } 105 return &DeviceCodeCredential{tenantID: cp.TenantID, clientID: cp.ClientID, userPrompt: cp.UserPrompt, client: c}, nil 106} 107 108// GetToken obtains a token from Azure Active Directory, following the device code authentication 109// flow. This function first requests a device code and requests that the user login before continuing to authenticate the device. 110// This function will keep polling the service for a token until the user logs in. 111// scopes: The list of scopes for which the token will have access. The "offline_access" scope is checked for and automatically added in case it isn't present to allow for silent token refresh. 112// ctx: The context for controlling the request lifetime. 113// Returns an AccessToken which can be used to authenticate service client calls. 114func (c *DeviceCodeCredential) GetToken(ctx context.Context, opts azcore.TokenRequestOptions) (*azcore.AccessToken, error) { 115 for i, scope := range opts.Scopes { 116 if scope == "offline_access" { // if we find that the opts.Scopes slice contains "offline_access" then we don't need to do anything and exit 117 break 118 } 119 if i == len(opts.Scopes)-1 && scope != "offline_access" { // if we haven't found "offline_access" when reaching the last element in the slice then we append it 120 opts.Scopes = append(opts.Scopes, "offline_access") 121 } 122 } 123 if len(c.refreshToken) != 0 { 124 tk, err := c.client.refreshAccessToken(ctx, c.tenantID, c.clientID, "", c.refreshToken, opts.Scopes) 125 if err != nil { 126 addGetTokenFailureLogs("Device Code Credential", err, true) 127 return nil, err 128 } 129 // assign new refresh token to the credential for future use 130 c.refreshToken = tk.refreshToken 131 logGetTokenSuccess(c, opts) 132 // passing the access token and/or error back up 133 return tk.token, nil 134 } 135 // if there is no refreshToken, then begin the Device Code flow from the beginning 136 // make initial request to the device code endpoint for a device code and instructions for authentication 137 dc, err := c.client.requestNewDeviceCode(ctx, c.tenantID, c.clientID, opts.Scopes) 138 if err != nil { 139 addGetTokenFailureLogs("Device Code Credential", err, true) 140 return nil, err // TODO check what error type to return here 141 } 142 // send authentication flow instructions back to the user to log in and authorize the device 143 144 c.userPrompt(DeviceCodeMessage{ 145 UserCode: dc.UserCode, 146 VerificationURL: dc.VerificationURL, 147 Message: dc.Message}) 148 // poll the token endpoint until a valid access token is received or until authentication fails 149 for { 150 tk, err := c.client.authenticateDeviceCode(ctx, c.tenantID, c.clientID, dc.DeviceCode, opts.Scopes) 151 // if there is no error, save the refresh token and return the token credential 152 if err == nil { 153 c.refreshToken = tk.refreshToken 154 logGetTokenSuccess(c, opts) 155 return tk.token, err 156 } 157 // if there is an error, check for an AADAuthenticationFailedError in order to check the status for token retrieval 158 // if the error is not an AADAuthenticationFailedError, then fail here since something unexpected occurred 159 if authRespErr := (*AADAuthenticationFailedError)(nil); errors.As(err, &authRespErr) && authRespErr.Message == "authorization_pending" { 160 // wait for the interval specified from the initial device code endpoint and then poll for the token again 161 time.Sleep(time.Duration(dc.Interval) * time.Second) 162 } else { 163 addGetTokenFailureLogs("Device Code Credential", err, true) 164 // any other error should be returned 165 return nil, err 166 } 167 } 168} 169 170// NewAuthenticationPolicy implements the azcore.Credential interface on DeviceCodeCredential. 171func (c *DeviceCodeCredential) NewAuthenticationPolicy(options azcore.AuthenticationOptions) azcore.Policy { 172 return newBearerTokenPolicy(c, options) 173} 174 175// deviceCodeResult is used to store device code related information to help the user login and allow the device code flow to continue 176// to request a token to authenticate a user 177type deviceCodeResult struct { 178 UserCode string `json:"user_code"` // User code returned by the service. 179 DeviceCode string `json:"device_code"` // Device code returned by the service. 180 VerificationURL string `json:"verification_uri"` // Verification URL where the user must navigate to authenticate using the device code and credentials. 181 Interval int64 `json:"interval"` // Polling interval time to check for completion of authentication flow. 182 Message string `json:"message"` // User friendly text response that can be used for display purposes. 183} 184 185var _ azcore.TokenCredential = (*DeviceCodeCredential)(nil) 186