-
Notifications
You must be signed in to change notification settings - Fork 186
/
DeviceCodeAuthenticationProvider.cs
236 lines (209 loc) · 9.88 KB
/
DeviceCodeAuthenticationProvider.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Client;
using PnP.Core.Auth.Services.Builder.Configuration;
using PnP.Core.Auth.Utilities;
using System;
using System.Configuration;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace PnP.Core.Auth
{
/// <summary>
/// Authentication Provider that uses a device code flow for authentication
/// </summary>
public class DeviceCodeAuthenticationProvider : OAuthAuthenticationProvider
{
/// <summary>
/// The Redirect URI for the authentication flow
/// </summary>
public Uri RedirectUri { get; set; }
/// <summary>
/// Action to notify the end user about the device code request
/// </summary>
public Action<DeviceCodeNotification> DeviceCodeVerification { get; set; }
// Instance private member, to keep the token cache at service instance level
private IPublicClientApplication publicClientApplication;
// Instance private member, to keep the Msal Http Client Factory at service instance level
private readonly IMsalHttpClientFactory msalHttpClientFactory;
/// <summary>
/// Public constructor for external consumers of the library
/// </summary>
/// <param name="clientId">The Client ID for the Authentication Provider</param>
/// <param name="tenantId">The Tenant ID for the Authentication Provider</param>
/// <param name="redirectUri">The Redirect URI for the authentication flow</param>
/// <param name="deviceCodeVerification">External action to manage the Device Code verification</param>
public DeviceCodeAuthenticationProvider(string clientId, string tenantId,
Uri redirectUri, Action<DeviceCodeNotification> deviceCodeVerification)
: this(clientId, tenantId, new PnPCoreAuthenticationDeviceCodeOptions()
{
RedirectUri = redirectUri
}, deviceCodeVerification)
{
}
/// <summary>
/// Public constructor for external consumers of the library
/// </summary>
/// <param name="clientId">The Client ID for the Authentication Provider</param>
/// <param name="tenantId">The Tenant ID for the Authentication Provider</param>
/// <param name="options">Options for the authentication provider</param>
/// <param name="deviceCodeVerification">External action to manage the Device Code verification</param>
public DeviceCodeAuthenticationProvider(string clientId, string tenantId,
PnPCoreAuthenticationDeviceCodeOptions options, Action<DeviceCodeNotification> deviceCodeVerification)
: this(null, null)
{
DeviceCodeVerification = deviceCodeVerification;
Init(new PnPCoreAuthenticationCredentialConfigurationOptions
{
ClientId = clientId,
TenantId = tenantId,
DeviceCode = options
});
}
/// <summary>
/// Public constructor leveraging DI to initialize the ILogger and IMsalHttpClientFactory interfaces
/// </summary>
/// <param name="logger">The instance of the logger service provided by DI</param>
/// <param name="msalHttpClientFactory">The instance of the Msal Http Client Factory service provided by DI</param>
public DeviceCodeAuthenticationProvider(ILogger<OAuthAuthenticationProvider> logger, IMsalHttpClientFactory msalHttpClientFactory)
: base(logger)
{
this.msalHttpClientFactory = msalHttpClientFactory;
}
/// <summary>
/// Initializes the Authentication Provider
/// </summary>
/// <param name="options">The options to use</param>
internal override void Init(PnPCoreAuthenticationCredentialConfigurationOptions options)
{
// We need the DeviceCode options
if (options.DeviceCode == null)
{
throw new ConfigurationErrorsException(
PnPCoreAuthResources.DeviceCodeAuthenticationProvider_InvalidConfiguration);
}
ClientId = !string.IsNullOrEmpty(options.ClientId) ? options.ClientId : AuthGlobals.DefaultClientId;
TenantId = !string.IsNullOrEmpty(options.TenantId) ? options.TenantId : AuthGlobals.OrganizationsTenantId;
RedirectUri = options.DeviceCode.RedirectUri ?? AuthGlobals.DefaultRedirectUri;
// Build the MSAL client
publicClientApplication = PublicClientApplicationBuilder
.Create(ClientId)
.WithPnPAdditionalAuthenticationSettings(
options.DeviceCode.AuthorityUri,
RedirectUri,
TenantId,
options.Environment,
options.AzureADLoginAuthority)
.WithHttpClientFactory(msalHttpClientFactory)
.Build();
// Log the initialization information
Log?.LogInformation(PnPCoreAuthResources.DeviceCodeAuthenticationProvider_LogInit);
}
/// <summary>
/// Authenticates the specified request message.
/// </summary>
/// <param name="resource">Request uri</param>
/// <param name="request">The <see cref="HttpRequestMessage"/> to authenticate.</param>
/// <returns>The task to await.</returns>
public override async Task AuthenticateRequestAsync(Uri resource, HttpRequestMessage request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (resource == null)
{
throw new ArgumentNullException(nameof(resource));
}
request.Headers.Authorization = new AuthenticationHeaderValue("bearer",
await GetAccessTokenAsync(resource).ConfigureAwait(false));
}
/// <summary>
/// Gets an access token for the requested resource and scope
/// </summary>
/// <param name="resource">Resource to request an access token for (unused)</param>
/// <param name="scopes">Scopes to request</param>
/// <returns>An access token</returns>
public override async Task<string> GetAccessTokenAsync(Uri resource, string[] scopes)
{
if (resource == null)
{
throw new ArgumentNullException(nameof(resource));
}
if (scopes == null)
{
throw new ArgumentNullException(nameof(scopes));
}
if (DeviceCodeVerification == null)
{
throw new ConfigurationErrorsException(
PnPCoreAuthResources.DeviceCodeAuthenticationProvider_MissingDeviceCodeVerification);
}
AuthenticationResult tokenResult = null;
var account = await publicClientApplication.GetAccountsAsync().ConfigureAwait(false);
try
{
// Try to get the token from the tokens cache
tokenResult = await publicClientApplication.AcquireTokenSilent(scopes, account.FirstOrDefault())
.ExecuteAsync().ConfigureAwait(false);
}
catch (MsalUiRequiredException)
{
// Try to get the token directly through AAD if it is not available in the tokens cache
tokenResult = await publicClientApplication.AcquireTokenWithDeviceCode(scopes,
deviceCodeResult =>
{
DeviceCodeVerification.Invoke(new DeviceCodeNotification
{
UserCode = deviceCodeResult.UserCode,
Message = deviceCodeResult.Message,
VerificationUrl = new Uri(deviceCodeResult.VerificationUrl)
});
return Task.FromResult(0);
})
.ExecuteAsync().ConfigureAwait(false);
}
// Log the access token retrieval action
Log?.LogDebug(PnPCoreAuthResources.AuthenticationProvider_LogAccessTokenRetrieval,
GetType().Name, resource, scopes.Aggregate(string.Empty, (c, n) => c + ", " + n).TrimEnd(','));
// Return the Access Token, if we've got it
// In case of any exception while retrieving the access token,
// MSAL will throw an exception that we simply bubble up
return tokenResult.AccessToken;
}
/// <summary>
/// Gets an access token for the requested resource
/// </summary>
/// <param name="resource">Resource to request an access token for</param>
/// <returns>An access token</returns>
public override async Task<string> GetAccessTokenAsync(Uri resource)
{
if (resource == null)
{
throw new ArgumentNullException(nameof(resource));
}
// Use the .default scope if the scopes are not provided
return await GetAccessTokenAsync(resource,
new string[] { $"{resource.Scheme}://{resource.Authority}/.default" }).ConfigureAwait(false);
}
}
/// <summary>
/// Provides information about the Device Code authentication request
/// </summary>
public class DeviceCodeNotification
{
/// <summary>
/// User friendly text response that can be used for display purpose.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Verification URL where the user must navigate to authenticate using the device code and credentials
/// </summary>
public Uri VerificationUrl { get; set; }
/// <summary>
/// Device code returned by the service
/// </summary>
public string UserCode { get; set; }
}
}