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