-
Notifications
You must be signed in to change notification settings - Fork 241
/
AuthenticationProvider.cs
245 lines (217 loc) · 10.8 KB
/
AuthenticationProvider.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
237
238
239
240
241
242
243
244
245
// <copyright file="AuthenticationProvider.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// </copyright>
// THIS CODE HAS NOT BEEN TESTED RIGOROUSLY.USING THIS CODE IN PRODUCTION ENVIRONMENT IS STRICTLY NOT RECOMMENDED.
// THIS SAMPLE IS PURELY FOR DEMONSTRATION PURPOSES ONLY.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
namespace Sample.Common.Authentication
{
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Graph.Communications.Client.Authentication;
using Microsoft.Graph.Communications.Common;
using Microsoft.Graph.Communications.Common.Telemetry;
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
/// <summary>
/// The authentication provider for this bot instance.
/// </summary>
/// <seealso cref="IRequestAuthenticationProvider" />
public class AuthenticationProvider : ObjectRoot, IRequestAuthenticationProvider
{
/// <summary>
/// The application name.
/// </summary>
private readonly string appName;
/// <summary>
/// The application identifier.
/// </summary>
private readonly string appId;
/// <summary>
/// The application secret.
/// </summary>
private readonly string appSecret;
/// <summary>
/// The open ID configuration refresh interval.
/// </summary>
private readonly TimeSpan openIdConfigRefreshInterval = TimeSpan.FromHours(2);
/// <summary>
/// The previous update timestamp for OpenIdConfig.
/// </summary>
private DateTime prevOpenIdConfigUpdateTimestamp = DateTime.MinValue;
/// <summary>
/// The open identifier configuration.
/// </summary>
private OpenIdConnectConfiguration openIdConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationProvider" /> class.
/// </summary>
/// <param name="appName">The application name.</param>
/// <param name="appId">The application identifier.</param>
/// <param name="appSecret">The application secret.</param>
/// <param name="logger">The logger.</param>
public AuthenticationProvider(string appName, string appId, string appSecret, IGraphLogger logger)
: base(logger.NotNull(nameof(logger)).CreateShim(nameof(AuthenticationProvider)))
{
this.appName = appName.NotNullOrWhitespace(nameof(appName));
this.appId = appId.NotNullOrWhitespace(nameof(appId));
this.appSecret = appSecret.NotNullOrWhitespace(nameof(appSecret));
}
/// <summary>
/// Authenticates the specified request message.
/// This method will be called any time there is an outbound request.
/// In this case we are using the Microsoft.IdentityModel.Clients.ActiveDirectory library
/// to stamp the outbound http request with the OAuth 2.0 token using an AAD application id
/// and application secret. Alternatively, this method can support certificate validation.
/// Note that this is only for demonstration purpose. Since acquiring token can be a costly operation,
/// it is recomended to acquire token before hand and cache the token, instead of acquiring it for every incoming request.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="tenant">The tenant.</param>
/// <returns>
/// The <see cref="Task" />.
/// </returns>
public async Task AuthenticateOutboundRequestAsync(HttpRequestMessage request, string tenant)
{
const string schema = "Bearer";
const string replaceString = "{tenant}";
const string oauthV2TokenLink = "https://login.microsoftonline.com/{tenant}";
const string resource = "https://graph.microsoft.com";
// If no tenant was specified, we craft the token link using the common tenant.
// https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints
tenant = string.IsNullOrWhiteSpace(tenant) ? "common" : tenant;
var tokenLink = oauthV2TokenLink.Replace(replaceString, tenant);
var scopes = new string[] { $"{resource}/.default" };
this.GraphLogger.Info("AuthenticationProvider: Generating OAuth token.");
var app = ConfidentialClientApplicationBuilder.Create(this.appId)
.WithAuthority(tokenLink)
.WithClientSecret(this.appSecret)
.Build();
AuthenticationResult result;
try
{
result = await this.AcquireTokenWithRetryAsync(app, scopes, 3).ConfigureAwait(false);
}
catch (Exception ex)
{
this.GraphLogger.Error(ex, $"Failed to generate token for client: {this.appId}");
throw;
}
this.GraphLogger.Info($"AuthenticationProvider: Generated OAuth token. Expires in {result.ExpiresOn.Subtract(DateTimeOffset.UtcNow).TotalMinutes} minutes.");
request.Headers.Authorization = new AuthenticationHeaderValue(schema, result.AccessToken);
}
/// <summary>
/// Validates the request asynchronously.
/// This method will be called any time we have an incoming request.
/// Returning invalid result will trigger a Forbidden response.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>
/// The <see cref="RequestValidationResult" /> structure.
/// </returns>
public async Task<RequestValidationResult> ValidateInboundRequestAsync(HttpRequestMessage request)
{
var token = request?.Headers?.Authorization?.Parameter;
if (string.IsNullOrWhiteSpace(token))
{
return new RequestValidationResult { IsValid = false };
}
// Currently the service does not sign outbound request using AAD, instead it is signed
// with a private certificate. In order for us to be able to ensure the certificate is
// valid we need to download the corresponding public keys from a trusted source.
const string authDomain = "https://api.aps.skype.com/v1/.well-known/OpenIdConfiguration";
if (this.openIdConfiguration == null || DateTime.Now > this.prevOpenIdConfigUpdateTimestamp.Add(this.openIdConfigRefreshInterval))
{
this.GraphLogger.Info("Updating OpenID configuration");
// Download the OIDC configuration which contains the JWKS
IConfigurationManager<OpenIdConnectConfiguration> configurationManager =
new ConfigurationManager<OpenIdConnectConfiguration>(
authDomain,
new OpenIdConnectConfigurationRetriever());
this.openIdConfiguration = await configurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false);
this.prevOpenIdConfigUpdateTimestamp = DateTime.Now;
}
// The incoming token should be issued by graph.
var authIssuers = new[]
{
"https://graph.microsoft.com",
"https://api.botframework.com",
};
// Configure the TokenValidationParameters.
// Aet the Issuer(s) and Audience(s) to validate and
// assign the SigningKeys which were downloaded from AuthDomain.
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidIssuers = authIssuers,
ValidAudience = this.appId,
IssuerSigningKeys = this.openIdConfiguration.SigningKeys,
};
ClaimsPrincipal claimsPrincipal;
try
{
// Now validate the token. If the token is not valid for any reason, an exception will be thrown by the method
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
claimsPrincipal = handler.ValidateToken(token, validationParameters, out _);
}
// Token expired... should somehow return 401 (Unauthorized)
// catch (SecurityTokenExpiredException ex)
// Tampered token
// catch (SecurityTokenInvalidSignatureException ex)
// Some other validation error
// catch (SecurityTokenValidationException ex)
catch (Exception ex)
{
// Some other error
this.GraphLogger.Error(ex, $"Failed to validate token for client: {this.appId}.");
return new RequestValidationResult() { IsValid = false };
}
const string ClaimType = "http://schemas.microsoft.com/identity/claims/tenantid";
var tenantClaim = claimsPrincipal.FindFirst(claim => claim.Type.Equals(ClaimType, StringComparison.Ordinal));
if (string.IsNullOrEmpty(tenantClaim?.Value))
{
// No tenant claim given to us. reject the request.
return new RequestValidationResult { IsValid = false };
}
request.Properties.Add(HttpConstants.HeaderNames.Tenant, tenantClaim.Value);
return new RequestValidationResult { IsValid = true, TenantId = tenantClaim.Value };
}
/// <summary>
/// Acquires the token and retries if failure occurs.
/// </summary>
/// <param name="app">The application.</param>
/// <param name="scopes">The resources.</param>
/// <param name="attempts">The attempts.</param>
/// <returns>
/// The <see cref="AuthenticationResult" />.
/// </returns>
private async Task<AuthenticationResult> AcquireTokenWithRetryAsync(IConfidentialClientApplication app, string[] scopes, int attempts)
{
while (true)
{
attempts--;
try
{
return await app.AcquireTokenForClient(scopes)
.ExecuteAsync()
.ConfigureAwait(false);
}
catch (Exception)
{
if (attempts < 1)
{
throw;
}
}
await Task.Delay(1000).ConfigureAwait(false);
}
}
}
}