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