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