-
Notifications
You must be signed in to change notification settings - Fork 597
Adding code only flow for sign in. #258
Changes from all commits
42f9a55
6e32a19
5e076e9
2b0d0ad
9fc2eb7
3d0eb38
719e66e
f3d44a9
8e5c49a
9c94b49
d430b5d
c3b28a9
c6f45ba
081ee8f
a34370a
334e66f
d350329
abd67c1
37208ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,22 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Globalization; | ||
using System.IdentityModel.Tokens; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Net.Http; | ||
using System.Net.Http.Headers; | ||
using System.Security.Claims; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNet.Authentication.Notifications; | ||
using Microsoft.AspNet.Http; | ||
using Microsoft.AspNet.Http.Authentication; | ||
using Microsoft.Framework.Logging; | ||
using Microsoft.IdentityModel.Protocols; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace Microsoft.AspNet.Authentication.OpenIdConnect | ||
{ | ||
|
@@ -38,6 +42,13 @@ private string CurrentUri | |
} | ||
} | ||
|
||
protected HttpClient Backchannel { get; private set; } | ||
|
||
public OpenIdConnectAuthenticationHandler(HttpClient backchannel) | ||
{ | ||
Backchannel = backchannel; | ||
} | ||
|
||
protected override void ApplyResponseGrant() | ||
{ | ||
ApplyResponseGrantAsync().GetAwaiter().GetResult(); | ||
|
@@ -153,17 +164,16 @@ protected override async Task ApplyResponseChallengeAsync() | |
properties.RedirectUri = CurrentUri; | ||
} | ||
|
||
if (!string.IsNullOrWhiteSpace(Options.RedirectUri)) | ||
{ | ||
Logger.LogDebug(Resources.OIDCH_0031_Using_Options_RedirectUri, Options.RedirectUri); | ||
} | ||
|
||
// When redeeming a 'code' for an AccessToken, this value is needed | ||
if (!string.IsNullOrWhiteSpace(Options.RedirectUri)) | ||
{ | ||
Logger.LogDebug(Resources.OIDCH_0031_Using_Options_RedirectUri, Options.RedirectUri); | ||
properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey, Options.RedirectUri); | ||
} | ||
|
||
// adding response type to properties so that we can determine the response type of an incoming message. | ||
properties.Items.Add(OpenIdConnectParameterNames.ResponseType, Options.ResponseType); | ||
|
||
if (_configuration == null && Options.ConfigurationManager != null) | ||
{ | ||
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); | ||
|
@@ -183,6 +193,12 @@ protected override async Task ApplyResponseChallengeAsync() | |
State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + UrlEncoder.UrlEncode(Options.StateDataFormat.Protect(properties)) | ||
}; | ||
|
||
// if response_type=code, nonce is not required. | ||
if (Options.ResponseType.Equals(OpenIdConnectResponseTypes.Code)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to check the value of OpenIdConnectMessage.ResponseType, since it can be set in RedirectToIdentityProvider later. So any micro changes should be based off of the final request sent to the IDP. |
||
{ | ||
Options.ProtocolValidator.RequireNonce = false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will influence id_token + code that comes on a different message. |
||
} | ||
|
||
if (Options.ProtocolValidator.RequireNonce) | ||
{ | ||
message.Nonce = Options.ProtocolValidator.GenerateNonce(); | ||
|
@@ -238,6 +254,7 @@ protected override AuthenticationTicket AuthenticateCore() | |
/// <remarks>Uses log id's OIDCH-0000 - OIDCH-0025</remarks> | ||
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync() | ||
{ | ||
OpenIdConnectMessage tokens = null; | ||
Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType()); | ||
|
||
// Allow login to be constrained to a specific path. Need to make this runtime configurable. | ||
|
@@ -299,12 +316,16 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync() | |
} | ||
|
||
var properties = GetPropertiesFromState(message.State); | ||
|
||
if (properties == null) | ||
{ | ||
Logger.LogError(Resources.OIDCH_0005_MessageStateIsInvalid); | ||
return null; | ||
} | ||
|
||
var isCodeOnlyFlow = properties.Items.ContainsKey(OpenIdConnectParameterNames.ResponseType) ? | ||
(properties.Items[OpenIdConnectParameterNames.ResponseType] == OpenIdConnectResponseTypes.Code) : false; | ||
|
||
// devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users. | ||
if (!string.IsNullOrWhiteSpace(message.Error)) | ||
{ | ||
|
@@ -321,6 +342,21 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync() | |
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); | ||
} | ||
|
||
// Redeeming authorization code for tokens | ||
if (string.IsNullOrWhiteSpace(message.IdToken) && isCodeOnlyFlow) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just because Options.ResponseType == CodeOnly, that does not mean the message is 'code' only. We need to determine the type of response from the actual message received. This is particularly important when a site is being attacked. Also, consider that RedirectToIdentityProvider can change or set the ResponseType. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the response_type property to the state. |
||
{ | ||
Logger.LogDebug(Resources.OIDCH_0037_Redeeming_Auth_Code, message.Code); | ||
|
||
var redirectUri = properties.Items.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey) ? | ||
properties.Items[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] : Options.RedirectUri; | ||
|
||
tokens = await RedeemAuthorizationCode(message.Code, redirectUri); | ||
if (tokens != null) | ||
{ | ||
message.IdToken = tokens.IdToken; | ||
} | ||
} | ||
|
||
// OpenIdConnect protocol allows a Code to be received without the id_token | ||
if (!string.IsNullOrWhiteSpace(message.IdToken)) | ||
{ | ||
|
@@ -409,6 +445,12 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync() | |
} | ||
} | ||
|
||
if (Options.GetClaimsFromUserInfoEndpoint) | ||
{ | ||
Logger.LogDebug(Resources.OIDCH_0039_Sending_Request_UIEndpoint); | ||
ticket = await GetUserInformationAsync(properties, tokens, ticket); | ||
} | ||
|
||
var securityTokenValidatedNotification = | ||
new SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options) | ||
{ | ||
|
@@ -429,27 +471,31 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync() | |
return null; | ||
} | ||
|
||
string nonce = jwt.Payload.Nonce; | ||
if (Options.NonceCache != null) | ||
// If id_token is received using code only flow, no need to validate chash and nonce. | ||
if (!isCodeOnlyFlow) | ||
{ | ||
// if the nonce cannot be removed, it was used | ||
if (!Options.NonceCache.TryRemoveNonce(nonce)) | ||
string nonce = jwt.Payload.Nonce; | ||
if (Options.NonceCache != null) | ||
{ | ||
// if the nonce cannot be removed, it was used | ||
if (!Options.NonceCache.TryRemoveNonce(nonce)) | ||
{ | ||
nonce = null; | ||
} | ||
} | ||
else | ||
{ | ||
nonce = null; | ||
nonce = ReadNonceCookie(nonce); | ||
} | ||
} | ||
else | ||
{ | ||
nonce = ReadNonceCookie(nonce); | ||
} | ||
|
||
var protocolValidationContext = new OpenIdConnectProtocolValidationContext | ||
{ | ||
AuthorizationCode = message.Code, | ||
Nonce = nonce, | ||
}; | ||
var protocolValidationContext = new OpenIdConnectProtocolValidationContext | ||
{ | ||
AuthorizationCode = message.Code, | ||
Nonce = nonce, | ||
}; | ||
|
||
Options.ProtocolValidator.Validate(jwt, protocolValidationContext); | ||
Options.ProtocolValidator.Validate(jwt, protocolValidationContext); | ||
} | ||
} | ||
|
||
if (message.Code != null) | ||
|
@@ -520,6 +566,90 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync() | |
} | ||
} | ||
|
||
protected virtual async Task<OpenIdConnectMessage> RedeemAuthorizationCode(string authorizationCode, string redirectUri) | ||
{ | ||
var openIdMessage = new OpenIdConnectMessage() | ||
{ | ||
ClientId = Options.ClientId, | ||
ClientSecret = Options.ClientSecret, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can start with ClientSecret, but we will need an issue for: Certs and SignedJwt (AssertionCredentials). |
||
Code = authorizationCode, | ||
GrantType = "authorization_code", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Look for a constant in Wilson, if not open an issue in Wilson. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Issue opened in Wilson AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#186 |
||
RedirectUri = redirectUri | ||
}; | ||
|
||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, _configuration.TokenEndpoint); | ||
requestMessage.Content = new FormUrlEncodedContent(openIdMessage.Parameters); | ||
var responseMessage = await Backchannel.SendAsync(requestMessage); | ||
responseMessage.EnsureSuccessStatusCode(); | ||
var tokenResonse = await responseMessage.Content.ReadAsStringAsync(); | ||
var jsonTokenResponse = JObject.Parse(tokenResonse); | ||
return new OpenIdConnectMessage() | ||
{ | ||
AccessToken = jsonTokenResponse.Value<string>(OpenIdConnectParameterNames.AccessToken), | ||
IdToken = jsonTokenResponse.Value<string>(OpenIdConnectParameterNames.IdToken), | ||
TokenType = jsonTokenResponse.Value<string>(OpenIdConnectParameterNames.TokenType), | ||
ExpiresIn = jsonTokenResponse.Value<string>(OpenIdConnectParameterNames.ExpiresIn) | ||
}; | ||
} | ||
|
||
protected virtual async Task<AuthenticationTicket> GetUserInformationAsync(AuthenticationProperties properties, OpenIdConnectMessage message, AuthenticationTicket ticket) | ||
{ | ||
string userInfoEndpoint = null; | ||
if (_configuration != null) | ||
{ | ||
userInfoEndpoint = _configuration.UserInfoEndpoint; | ||
} | ||
|
||
if (string.IsNullOrEmpty(userInfoEndpoint)) | ||
{ | ||
userInfoEndpoint = Options.Configuration != null ? Options.Configuration.UserInfoEndpoint : null; | ||
} | ||
|
||
if (string.IsNullOrEmpty(userInfoEndpoint)) | ||
{ | ||
Logger.LogError("UserInfo endpoint is not set. Request to retrieve claims from userinfo endpoint cannot be completed."); | ||
return ticket; | ||
} | ||
|
||
var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint); | ||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", message.AccessToken); | ||
var responseMessage = await Backchannel.SendAsync(requestMessage); | ||
responseMessage.EnsureSuccessStatusCode(); | ||
var userInfoResponse = await responseMessage.Content.ReadAsStringAsync(); | ||
var user = JObject.Parse(userInfoResponse); | ||
|
||
var identity = (ClaimsIdentity)ticket.Principal.Identity; | ||
var subject = identity.FindFirst(ClaimTypes.NameIdentifier).Value; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we specifically look for the 'sub' claim. Best to check with Mike Jones. Need to be mind-full of how claims mapping will influence this. When you use the ClaimsIdentity, a mapping has been performed by the JwtSecurityTokenHandler. |
||
|
||
// check if the sub claim matches | ||
var userInfoSubject = user.Value<string>("sub"); | ||
if (userInfoSubject == null || !string.Equals(userInfoSubject, subject, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
Logger.LogError(Resources.OIDCH_0038_Subject_Claim_Mismatch); | ||
throw new ArgumentException(Resources.OIDCH_0038_Subject_Claim_Mismatch); | ||
} | ||
|
||
var userInfoIdentity = new ClaimsIdentity(identity); | ||
foreach (var pair in user) | ||
{ | ||
JToken value; | ||
var claimValue = user.TryGetValue(pair.Key, out value) ? value.ToString() : null; | ||
|
||
var inboundClaimTypeMap = new Dictionary<string, string>(JwtSecurityTokenHandler.InboundClaimTypeMap); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each time a claim is mapped by the JwtSecurityTokenHandler the 'short' name is captured in the properties of the claim. See JwtSecurityTokenHandler.ShortClaimTypeProperty |
||
string longClaimTypeName; | ||
inboundClaimTypeMap.TryGetValue(pair.Key, out longClaimTypeName); | ||
|
||
// checking if claim exist with either short or long name | ||
if (!(identity.HasClaim(pair.Key, claimValue) || identity.HasClaim(longClaimTypeName, claimValue))) | ||
{ | ||
userInfoIdentity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, Options.ClaimsIssuer)); | ||
} | ||
} | ||
|
||
ticket.Principal.AddIdentity(userInfoIdentity); | ||
return ticket; | ||
} | ||
|
||
/// <summary> | ||
/// Adds the nonce to <see cref="HttpResponse.Cookies"/>. | ||
/// </summary> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
|
@@ -13,11 +13,11 @@ | |
using Microsoft.AspNet.Builder; | ||
using Microsoft.AspNet.DataProtection; | ||
using Microsoft.AspNet.Http; | ||
using Microsoft.Framework.Internal; | ||
using Microsoft.Framework.Logging; | ||
using Microsoft.Framework.OptionsModel; | ||
using Microsoft.IdentityModel.Protocols; | ||
using Microsoft.Framework.Internal; | ||
using Microsoft.Framework.WebEncoders; | ||
using Microsoft.IdentityModel.Protocols; | ||
|
||
namespace Microsoft.AspNet.Authentication.OpenIdConnect | ||
{ | ||
|
@@ -100,6 +100,11 @@ public OpenIdConnectAuthenticationMiddleware( | |
Options.TokenValidationParameters.ValidAudience = Options.ClientId; | ||
} | ||
|
||
Backchannel = new HttpClient(ResolveHttpMessageHandler(Options)); | ||
Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET OpenIdConnect middleware"); | ||
Backchannel.Timeout = Options.BackchannelTimeout; | ||
Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Configurable via options? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It isn't configurable on any of our providers. https://github.com/aspnet/Security/blob/dev/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationMiddleware.cs#L80 |
||
|
||
if (Options.ConfigurationManager == null) | ||
{ | ||
if (Options.Configuration != null) | ||
|
@@ -119,21 +124,20 @@ public OpenIdConnectAuthenticationMiddleware( | |
Options.MetadataAddress += ".well-known/openid-configuration"; | ||
} | ||
|
||
var httpClient = new HttpClient(ResolveHttpMessageHandler(Options)); | ||
httpClient.Timeout = Options.BackchannelTimeout; | ||
httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB | ||
Options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(Options.MetadataAddress, httpClient); | ||
Options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(Options.MetadataAddress, Backchannel); | ||
} | ||
} | ||
} | ||
|
||
protected HttpClient Backchannel { get; private set; } | ||
|
||
/// <summary> | ||
/// Provides the <see cref="AuthenticationHandler"/> object for processing authentication-related requests. | ||
/// </summary> | ||
/// <returns>An <see cref="AuthenticationHandler"/> configured with the <see cref="OpenIdConnectAuthenticationOptions"/> supplied to the constructor.</returns> | ||
protected override AuthenticationHandler<OpenIdConnectAuthenticationOptions> CreateHandler() | ||
{ | ||
return new OpenIdConnectAuthenticationHandler(); | ||
return new OpenIdConnectAuthenticationHandler(Backchannel); | ||
} | ||
|
||
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,6 +55,7 @@ public OpenIdConnectAuthenticationOptions(string authenticationScheme) | |
AuthenticationScheme = authenticationScheme; | ||
BackchannelTimeout = TimeSpan.FromMinutes(1); | ||
Caption = OpenIdConnectAuthenticationDefaults.Caption; | ||
GetClaimsFromUserInfoEndpoint = false; | ||
ProtocolValidator = new OpenIdConnectProtocolValidator(); | ||
RefreshOnIssuerKeyNotFound = true; | ||
ResponseMode = OpenIdConnectResponseModes.FormPost; | ||
|
@@ -163,6 +164,11 @@ public string Caption | |
/// </summary> | ||
public bool DefaultToCurrentUriOnRedirect { get; set; } | ||
|
||
/// <summary> | ||
/// Boolean to set whether the middleware should go to user info endpoint to retrieve claims or not. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want to indicate that this is an additional step after setting the Identity to the Id_token that arrived from the TokenEndpoint. |
||
/// </summary> | ||
public bool GetClaimsFromUserInfoEndpoint { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the discovery endpoint for obtaining metadata | ||
/// </summary> | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BOM?