Adding support for signing in using "code flow"
This commit is contained in:
parent
c6aa9371c7
commit
8d7f052cf4
|
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
using Microsoft.AspNet.Authentication.OpenIdConnect;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.IdentityModel.Protocols;
|
||||
|
||||
namespace Microsoft.AspNet.Authentication.Notifications
|
||||
{
|
||||
/// <summary>
|
||||
/// This Notification can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint.
|
||||
/// </summary>
|
||||
public class AuthorizationCodeRedeemedNotification : BaseNotification<OpenIdConnectAuthenticationOptions>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="AuthorizationCodeRedeemedNotification"/>
|
||||
/// </summary>
|
||||
public AuthorizationCodeRedeemedNotification(HttpContext context, OpenIdConnectAuthenticationOptions options) : base(context, options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the 'code'.
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="OpenIdConnectTokenEndpointResponse"/> that contains the tokens and json response received after redeeming the code at the token endpoint.
|
||||
/// </summary>
|
||||
public OpenIdConnectTokenEndpointResponse TokenEndpointResponse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="OpenIdConnectMessage"/>.
|
||||
/// </summary>
|
||||
public OpenIdConnectMessage ProtocolMessage { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ 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;
|
||||
|
|
@ -16,6 +18,7 @@ using Microsoft.Framework.Caching.Distributed;
|
|||
using Microsoft.Framework.Internal;
|
||||
using Microsoft.Framework.Logging;
|
||||
using Microsoft.IdentityModel.Protocols;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
||||
{
|
||||
|
|
@ -28,6 +31,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
private const string UriSchemeDelimiter = "://";
|
||||
private OpenIdConnectConfiguration _configuration;
|
||||
|
||||
protected HttpClient Backchannel { get; private set; }
|
||||
|
||||
public OpenIdConnectAuthenticationHandler(HttpClient backchannel)
|
||||
{
|
||||
Backchannel = backchannel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles Signout
|
||||
/// </summary>
|
||||
|
|
@ -116,17 +126,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
properties.RedirectUri = CurrentUri;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(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.IsNullOrEmpty(Options.RedirectUri))
|
||||
{
|
||||
properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, Options.RedirectUri);
|
||||
}
|
||||
|
||||
if (_configuration == null && Options.ConfigurationManager != null)
|
||||
{
|
||||
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
|
||||
|
|
@ -195,6 +194,19 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
message = redirectToIdentityProviderNotification.ProtocolMessage;
|
||||
}
|
||||
|
||||
var redirectUriForCode = message.RedirectUri;
|
||||
if (string.IsNullOrEmpty(redirectUriForCode))
|
||||
{
|
||||
Logger.LogDebug(Resources.OIDCH_0031_Using_Options_RedirectUri, Options.RedirectUri);
|
||||
redirectUriForCode = Options.RedirectUri;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(redirectUriForCode))
|
||||
{
|
||||
// When redeeming a 'code' for an AccessToken, this value is needed
|
||||
properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, redirectUriForCode);
|
||||
}
|
||||
|
||||
message.State = Options.StateDataFormat.Protect(properties);
|
||||
|
||||
var redirectUri = message.CreateAuthenticationRequestUrl();
|
||||
|
|
@ -243,27 +255,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
|
||||
try
|
||||
{
|
||||
if (Logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
Logger.LogDebug(Resources.OIDCH_0001_MessageReceived, message.BuildRedirectUrl());
|
||||
}
|
||||
|
||||
var messageReceivedNotification =
|
||||
new MessageReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
|
||||
{
|
||||
ProtocolMessage = message
|
||||
};
|
||||
|
||||
await Options.Notifications.MessageReceived(messageReceivedNotification);
|
||||
var messageReceivedNotification = await RunMessageReceivedNotificationAsync(message);
|
||||
if (messageReceivedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0002_MessageReceivedNotificationHandledResponse);
|
||||
return messageReceivedNotification.AuthenticationTicket;
|
||||
}
|
||||
|
||||
if (messageReceivedNotification.Skipped)
|
||||
else if (messageReceivedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0003_MessageReceivedNotificationSkipped);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -302,178 +300,19 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
|
||||
}
|
||||
|
||||
AuthenticationTicket ticket = null;
|
||||
JwtSecurityToken jwt = null;
|
||||
|
||||
// OpenIdConnect protocol allows a Code to be received without the id_token
|
||||
if (!string.IsNullOrEmpty(message.IdToken))
|
||||
if (string.IsNullOrEmpty(message.IdToken) && !string.IsNullOrEmpty(message.Code))
|
||||
{
|
||||
Logger.LogDebug(Resources.OIDCH_0020_IdTokenReceived, message.IdToken);
|
||||
var securityTokenReceivedNotification =
|
||||
new SecurityTokenReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
|
||||
{
|
||||
ProtocolMessage = message,
|
||||
};
|
||||
|
||||
await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification);
|
||||
if (securityTokenReceivedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse);
|
||||
return securityTokenReceivedNotification.AuthenticationTicket;
|
||||
}
|
||||
|
||||
if (securityTokenReceivedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0009_SecurityTokenReceivedNotificationSkipped);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Copy and augment to avoid cross request race conditions for updated configurations.
|
||||
var validationParameters = Options.TokenValidationParameters.Clone();
|
||||
if (_configuration != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(validationParameters.ValidIssuer))
|
||||
{
|
||||
validationParameters.ValidIssuer = _configuration.Issuer;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_configuration.Issuer))
|
||||
{
|
||||
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(new[] { _configuration.Issuer }) ?? new[] { _configuration.Issuer };
|
||||
}
|
||||
|
||||
validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys;
|
||||
}
|
||||
|
||||
SecurityToken validatedToken = null;
|
||||
ClaimsPrincipal principal = null;
|
||||
foreach (var validator in Options.SecurityTokenValidators)
|
||||
{
|
||||
if (validator.CanReadToken(message.IdToken))
|
||||
{
|
||||
principal = validator.ValidateToken(message.IdToken, validationParameters, out validatedToken);
|
||||
jwt = validatedToken as JwtSecurityToken;
|
||||
if (jwt == null)
|
||||
{
|
||||
Logger.LogError(Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType());
|
||||
throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validatedToken == null)
|
||||
{
|
||||
Logger.LogError(Resources.OIDCH_0011_UnableToValidateToken, message.IdToken);
|
||||
throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0011_UnableToValidateToken, message.IdToken));
|
||||
}
|
||||
|
||||
ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme);
|
||||
if (!string.IsNullOrEmpty(message.SessionState))
|
||||
{
|
||||
ticket.Properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState;
|
||||
}
|
||||
|
||||
if (_configuration != null && !string.IsNullOrEmpty(_configuration.CheckSessionIframe))
|
||||
{
|
||||
ticket.Properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe;
|
||||
}
|
||||
|
||||
// Rename?
|
||||
if (Options.UseTokenLifetime)
|
||||
{
|
||||
var issued = validatedToken.ValidFrom;
|
||||
if (issued != DateTime.MinValue)
|
||||
{
|
||||
ticket.Properties.IssuedUtc = issued;
|
||||
}
|
||||
|
||||
var expires = validatedToken.ValidTo;
|
||||
if (expires != DateTime.MinValue)
|
||||
{
|
||||
ticket.Properties.ExpiresUtc = expires;
|
||||
}
|
||||
}
|
||||
|
||||
var securityTokenValidatedNotification =
|
||||
new SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
|
||||
{
|
||||
AuthenticationTicket = ticket,
|
||||
ProtocolMessage = message
|
||||
};
|
||||
|
||||
await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification);
|
||||
if (securityTokenValidatedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse);
|
||||
return securityTokenValidatedNotification.AuthenticationTicket;
|
||||
}
|
||||
|
||||
if (securityTokenValidatedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0013_SecurityTokenValidatedNotificationSkipped);
|
||||
return null;
|
||||
}
|
||||
|
||||
string nonce = jwt.Payload.Nonce;
|
||||
if (Options.CacheNonces)
|
||||
{
|
||||
if (await Options.NonceCache.GetAsync(nonce) != null)
|
||||
{
|
||||
await Options.NonceCache.RemoveAsync(nonce);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the nonce cannot be removed, it was
|
||||
// already used and MUST be rejected.
|
||||
nonce = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
nonce = ReadNonceCookie(nonce);
|
||||
}
|
||||
|
||||
var protocolValidationContext = new OpenIdConnectProtocolValidationContext
|
||||
{
|
||||
AuthorizationCode = message.Code,
|
||||
Nonce = nonce,
|
||||
};
|
||||
|
||||
Options.ProtocolValidator.Validate(jwt, protocolValidationContext);
|
||||
return await HandleCodeOnlyFlow(message, properties);
|
||||
}
|
||||
|
||||
if (message.Code != null)
|
||||
else if (!string.IsNullOrEmpty(message.IdToken))
|
||||
{
|
||||
Logger.LogDebug(Resources.OIDCH_0014_AuthorizationCodeReceived, message.Code);
|
||||
if (ticket == null)
|
||||
{
|
||||
ticket = new AuthenticationTicket(properties, Options.AuthenticationScheme);
|
||||
}
|
||||
|
||||
var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options)
|
||||
{
|
||||
AuthenticationTicket = ticket,
|
||||
Code = message.Code,
|
||||
JwtSecurityToken = jwt,
|
||||
ProtocolMessage = message,
|
||||
RedirectUri = ticket.Properties.Items.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey) ?
|
||||
ticket.Properties.Items[OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey] : string.Empty,
|
||||
};
|
||||
|
||||
await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification);
|
||||
if (authorizationCodeReceivedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0015_AuthorizationCodeReceivedNotificationHandledResponse);
|
||||
return authorizationCodeReceivedNotification.AuthenticationTicket;
|
||||
}
|
||||
|
||||
if (authorizationCodeReceivedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0016_AuthorizationCodeReceivedNotificationSkipped);
|
||||
return null;
|
||||
}
|
||||
return await HandleIdTokenFlows(message, properties);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug(Resources.OIDCH_0045_Id_Token_Code_Missing);
|
||||
return null;
|
||||
}
|
||||
|
||||
return ticket;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
|
@ -489,23 +328,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
}
|
||||
}
|
||||
|
||||
var authenticationFailedNotification =
|
||||
new AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
|
||||
{
|
||||
ProtocolMessage = message,
|
||||
Exception = exception
|
||||
};
|
||||
|
||||
await Options.Notifications.AuthenticationFailed(authenticationFailedNotification);
|
||||
var authenticationFailedNotification = await RunAuthenticationFailedNotificationAsync(message, exception);
|
||||
if (authenticationFailedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0018_AuthenticationFailedNotificationHandledResponse);
|
||||
return authenticationFailedNotification.AuthenticationTicket;
|
||||
}
|
||||
|
||||
if (authenticationFailedNotification.Skipped)
|
||||
else if (authenticationFailedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0019_AuthenticationFailedNotificationSkipped);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -513,6 +342,215 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
}
|
||||
}
|
||||
|
||||
private async Task<AuthenticationTicket> HandleCodeOnlyFlow(OpenIdConnectMessage message, AuthenticationProperties properties)
|
||||
{
|
||||
AuthenticationTicket ticket = null;
|
||||
JwtSecurityToken jwt = null;
|
||||
|
||||
OpenIdConnectTokenEndpointResponse tokenEndpointResponse = null;
|
||||
string idToken = null;
|
||||
var authorizationCodeReceivedNotification = await RunAuthorizationCodeReceivedNotificationAsync(message, properties, ticket, jwt);
|
||||
if (authorizationCodeReceivedNotification.HandledResponse)
|
||||
{
|
||||
return authorizationCodeReceivedNotification.AuthenticationTicket;
|
||||
}
|
||||
else if (authorizationCodeReceivedNotification.Skipped)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Redeeming authorization code for tokens
|
||||
Logger.LogDebug(Resources.OIDCH_0038_Redeeming_Auth_Code, message.Code);
|
||||
|
||||
tokenEndpointResponse = await RedeemAuthorizationCodeAsync(message.Code, authorizationCodeReceivedNotification.RedirectUri);
|
||||
idToken = tokenEndpointResponse.Message.IdToken;
|
||||
|
||||
var authorizationCodeRedeemedNotification = await RunAuthorizationCodeRedeemedNotificationAsync(message, tokenEndpointResponse);
|
||||
if (authorizationCodeRedeemedNotification.HandledResponse)
|
||||
{
|
||||
return authorizationCodeRedeemedNotification.AuthenticationTicket;
|
||||
}
|
||||
else if (authorizationCodeRedeemedNotification.Skipped)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// no need to validate signature when token is received using "code flow" as per spec [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation].
|
||||
var validationParameters = Options.TokenValidationParameters.Clone();
|
||||
validationParameters.ValidateSignature = false;
|
||||
|
||||
ticket = ValidateToken(idToken, message, properties, validationParameters, out jwt);
|
||||
|
||||
if (Options.GetClaimsFromUserInfoEndpoint)
|
||||
{
|
||||
Logger.LogDebug(Resources.OIDCH_0040_Sending_Request_UIEndpoint);
|
||||
ticket = await GetUserInformationAsync(properties, tokenEndpointResponse.Message, ticket);
|
||||
}
|
||||
|
||||
var securityTokenValidatedNotification = await RunSecurityTokenValidatedNotificationAsync(message, ticket);
|
||||
if (securityTokenValidatedNotification.HandledResponse)
|
||||
{
|
||||
return securityTokenValidatedNotification.AuthenticationTicket;
|
||||
}
|
||||
else if (securityTokenValidatedNotification.Skipped)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// If id_token is received using code only flow, no need to validate chash.
|
||||
await ValidateOpenIdConnectProtocolAsync(jwt, message, false);
|
||||
|
||||
return ticket;
|
||||
}
|
||||
|
||||
private async Task<AuthenticationTicket> HandleIdTokenFlows(OpenIdConnectMessage message, AuthenticationProperties properties)
|
||||
{
|
||||
AuthenticationTicket ticket = null;
|
||||
JwtSecurityToken jwt = null;
|
||||
|
||||
var securityTokenReceivedNotification = await RunSecurityTokenReceivedNotificationAsync(message);
|
||||
if (securityTokenReceivedNotification.HandledResponse)
|
||||
{
|
||||
return securityTokenReceivedNotification.AuthenticationTicket;
|
||||
}
|
||||
else if (securityTokenReceivedNotification.Skipped)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var validationParameters = Options.TokenValidationParameters.Clone();
|
||||
ticket = ValidateToken(message.IdToken, message, properties, validationParameters, out jwt);
|
||||
|
||||
var securityTokenValidatedNotification = await RunSecurityTokenValidatedNotificationAsync(message, ticket);
|
||||
if (securityTokenValidatedNotification.HandledResponse)
|
||||
{
|
||||
return securityTokenValidatedNotification.AuthenticationTicket;
|
||||
}
|
||||
else if (securityTokenValidatedNotification.Skipped)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
await ValidateOpenIdConnectProtocolAsync(jwt, message);
|
||||
|
||||
if (message.Code != null)
|
||||
{
|
||||
var authorizationCodeReceivedNotification = await RunAuthorizationCodeReceivedNotificationAsync(message, properties, ticket, jwt);
|
||||
if (authorizationCodeReceivedNotification.HandledResponse)
|
||||
{
|
||||
return authorizationCodeReceivedNotification.AuthenticationTicket;
|
||||
}
|
||||
else if (authorizationCodeReceivedNotification.Skipped)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return ticket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redeems the authorization code for tokens at the token endpoint
|
||||
/// </summary>
|
||||
/// <param name="authorizationCode">The authorization code to redeem.</param>
|
||||
/// <param name="redirectUri">Uri that was passed in the request sent for the authorization code.</param>
|
||||
/// <returns>OpenIdConnect message that has tokens inside it.</returns>
|
||||
protected virtual async Task<OpenIdConnectTokenEndpointResponse> RedeemAuthorizationCodeAsync(string authorizationCode, string redirectUri)
|
||||
{
|
||||
var openIdMessage = new OpenIdConnectMessage()
|
||||
{
|
||||
ClientId = Options.ClientId,
|
||||
ClientSecret = Options.ClientSecret,
|
||||
Code = authorizationCode,
|
||||
GrantType = "authorization_code",
|
||||
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 OpenIdConnectTokenEndpointResponse(jsonTokenResponse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Goes to UserInfo endpoint to retrieve additional claims and add any unique claims to the given identity.
|
||||
/// </summary>
|
||||
/// <param name="properties">Authentication Properties</param>
|
||||
/// <param name="message">message that is being processed</param>
|
||||
/// <param name="ticket">authentication ticket with claims principal and identities</param>
|
||||
/// <returns>Authentication ticket with identity with additional claims, if any.</returns>
|
||||
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))
|
||||
{
|
||||
Logger.LogWarning(Resources.OIDCH_0046_UserInfo_Endpoint_Not_Set);
|
||||
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 subjectClaimType = identity.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (subjectClaimType == null)
|
||||
{
|
||||
Logger.LogError(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0041_Subject_Claim_Not_Found, identity.ToString()));
|
||||
return ticket;
|
||||
}
|
||||
|
||||
var userInfoSubjectClaimValue = user.Value<string>(JwtRegisteredClaimNames.Sub);
|
||||
|
||||
// check if the sub claim matches
|
||||
if (userInfoSubjectClaimValue == null || !string.Equals(userInfoSubjectClaimValue, subjectClaimType.Value, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogError(Resources.OIDCH_0039_Subject_Claim_Mismatch);
|
||||
return ticket;
|
||||
}
|
||||
|
||||
foreach (var claim in identity.Claims)
|
||||
{
|
||||
// If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set
|
||||
var shortClaimTypeName = claim.Properties.ContainsKey(JwtSecurityTokenHandler.ShortClaimTypeProperty) ?
|
||||
claim.Properties[JwtSecurityTokenHandler.ShortClaimTypeProperty] : string.Empty;
|
||||
|
||||
// checking if claim in the identity (generated from id_token) has the same type as a claim retrieved from userinfo endpoint
|
||||
JToken value;
|
||||
var isClaimIncluded = user.TryGetValue(claim.Type, out value) || user.TryGetValue(shortClaimTypeName, out value);
|
||||
|
||||
// if a same claim exists (matching both type and value) both in id_token identity and userinfo response, remove the json entry from the userinfo response
|
||||
if (isClaimIncluded && claim.Value.Equals(value.ToString(), StringComparison.Ordinal))
|
||||
{
|
||||
if (!user.Remove(claim.Type))
|
||||
{
|
||||
user.Remove(shortClaimTypeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adding remaining unique claims from userinfo endpoint to the identity
|
||||
foreach (var pair in user)
|
||||
{
|
||||
JToken value;
|
||||
var claimValue = user.TryGetValue(pair.Key, out value) ? value.ToString() : null;
|
||||
identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, Options.ClaimsIssuer));
|
||||
}
|
||||
|
||||
return new AuthenticationTicket(new ClaimsPrincipal(identity), ticket.Properties, ticket.AuthenticationScheme);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the nonce to <see cref="HttpResponse.Cookies"/>.
|
||||
/// </summary>
|
||||
|
|
@ -609,6 +647,251 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
}
|
||||
}
|
||||
|
||||
private async Task<MessageReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>> RunMessageReceivedNotificationAsync(OpenIdConnectMessage message)
|
||||
{
|
||||
Logger.LogDebug(Resources.OIDCH_0001_MessageReceived, message.BuildRedirectUrl());
|
||||
var messageReceivedNotification =
|
||||
new MessageReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
|
||||
{
|
||||
ProtocolMessage = message
|
||||
};
|
||||
|
||||
await Options.Notifications.MessageReceived(messageReceivedNotification);
|
||||
if (messageReceivedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0002_MessageReceivedNotificationHandledResponse);
|
||||
}
|
||||
else if (messageReceivedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0003_MessageReceivedNotificationSkipped);
|
||||
}
|
||||
|
||||
return messageReceivedNotification;
|
||||
}
|
||||
|
||||
private async Task<AuthorizationCodeReceivedNotification> RunAuthorizationCodeReceivedNotificationAsync(OpenIdConnectMessage message, AuthenticationProperties properties, AuthenticationTicket ticket, JwtSecurityToken jwt)
|
||||
{
|
||||
var redirectUri = properties.Items.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey) ?
|
||||
properties.Items[OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey] : Options.RedirectUri;
|
||||
|
||||
Logger.LogDebug(Resources.OIDCH_0014_AuthorizationCodeReceived, message.Code);
|
||||
|
||||
var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options)
|
||||
{
|
||||
Code = message.Code,
|
||||
ProtocolMessage = message,
|
||||
RedirectUri = redirectUri,
|
||||
AuthenticationTicket = ticket,
|
||||
JwtSecurityToken = jwt
|
||||
};
|
||||
|
||||
await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification);
|
||||
if (authorizationCodeReceivedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0015_AuthorizationCodeReceivedNotificationHandledResponse);
|
||||
}
|
||||
else if (authorizationCodeReceivedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0016_AuthorizationCodeReceivedNotificationSkipped);
|
||||
}
|
||||
|
||||
return authorizationCodeReceivedNotification;
|
||||
}
|
||||
|
||||
private async Task<AuthorizationCodeRedeemedNotification> RunAuthorizationCodeRedeemedNotificationAsync(OpenIdConnectMessage message, OpenIdConnectTokenEndpointResponse tokenEndpointResponse)
|
||||
{
|
||||
Logger.LogDebug(Resources.OIDCH_0042_AuthorizationCodeRedeemed, message.Code);
|
||||
var authorizationCodeRedeemedNotification = new AuthorizationCodeRedeemedNotification(Context, Options)
|
||||
{
|
||||
Code = message.Code,
|
||||
ProtocolMessage = message,
|
||||
TokenEndpointResponse = tokenEndpointResponse
|
||||
};
|
||||
|
||||
await Options.Notifications.AuthorizationCodeRedeemed(authorizationCodeRedeemedNotification);
|
||||
if (authorizationCodeRedeemedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0043_AuthorizationCodeRedeemedNotificationHandledResponse);
|
||||
}
|
||||
else if (authorizationCodeRedeemedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0044_AuthorizationCodeRedeemedNotificationSkipped);
|
||||
}
|
||||
return authorizationCodeRedeemedNotification;
|
||||
}
|
||||
|
||||
private async Task<SecurityTokenReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>> RunSecurityTokenReceivedNotificationAsync(OpenIdConnectMessage message)
|
||||
{
|
||||
Logger.LogDebug(Resources.OIDCH_0020_IdTokenReceived, message.IdToken);
|
||||
var securityTokenReceivedNotification =
|
||||
new SecurityTokenReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
|
||||
{
|
||||
ProtocolMessage = message,
|
||||
};
|
||||
|
||||
await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification);
|
||||
if (securityTokenReceivedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse);
|
||||
}
|
||||
else if (securityTokenReceivedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0009_SecurityTokenReceivedNotificationSkipped);
|
||||
}
|
||||
|
||||
return securityTokenReceivedNotification;
|
||||
}
|
||||
|
||||
private async Task<SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>> RunSecurityTokenValidatedNotificationAsync(OpenIdConnectMessage message, AuthenticationTicket ticket)
|
||||
{
|
||||
var securityTokenValidatedNotification =
|
||||
new SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
|
||||
{
|
||||
AuthenticationTicket = ticket,
|
||||
ProtocolMessage = message
|
||||
};
|
||||
|
||||
await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification);
|
||||
if (securityTokenValidatedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse);
|
||||
}
|
||||
else if (securityTokenValidatedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0013_SecurityTokenValidatedNotificationSkipped);
|
||||
}
|
||||
|
||||
return securityTokenValidatedNotification;
|
||||
}
|
||||
|
||||
private async Task<AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>> RunAuthenticationFailedNotificationAsync(OpenIdConnectMessage message, Exception exception)
|
||||
{
|
||||
var authenticationFailedNotification =
|
||||
new AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
|
||||
{
|
||||
ProtocolMessage = message,
|
||||
Exception = exception
|
||||
};
|
||||
|
||||
await Options.Notifications.AuthenticationFailed(authenticationFailedNotification);
|
||||
if (authenticationFailedNotification.HandledResponse)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0018_AuthenticationFailedNotificationHandledResponse);
|
||||
}
|
||||
else if (authenticationFailedNotification.Skipped)
|
||||
{
|
||||
Logger.LogVerbose(Resources.OIDCH_0019_AuthenticationFailedNotificationSkipped);
|
||||
}
|
||||
|
||||
return authenticationFailedNotification;
|
||||
}
|
||||
|
||||
private AuthenticationTicket ValidateToken(string idToken, OpenIdConnectMessage message, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt)
|
||||
{
|
||||
AuthenticationTicket ticket = null;
|
||||
jwt = null;
|
||||
|
||||
if (_configuration != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(validationParameters.ValidIssuer))
|
||||
{
|
||||
validationParameters.ValidIssuer = _configuration.Issuer;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_configuration.Issuer))
|
||||
{
|
||||
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(new[] { _configuration.Issuer }) ?? new[] { _configuration.Issuer };
|
||||
}
|
||||
|
||||
validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys;
|
||||
}
|
||||
|
||||
SecurityToken validatedToken = null;
|
||||
ClaimsPrincipal principal = null;
|
||||
foreach (var validator in Options.SecurityTokenValidators)
|
||||
{
|
||||
if (validator.CanReadToken(idToken))
|
||||
{
|
||||
principal = validator.ValidateToken(idToken, validationParameters, out validatedToken);
|
||||
jwt = validatedToken as JwtSecurityToken;
|
||||
if (jwt == null)
|
||||
{
|
||||
Logger.LogError(Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType());
|
||||
throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validatedToken == null)
|
||||
{
|
||||
Logger.LogError(Resources.OIDCH_0011_UnableToValidateToken, idToken);
|
||||
throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0011_UnableToValidateToken, idToken));
|
||||
}
|
||||
|
||||
ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme);
|
||||
if (!string.IsNullOrEmpty(message.SessionState))
|
||||
{
|
||||
ticket.Properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState;
|
||||
}
|
||||
|
||||
if (_configuration != null && !string.IsNullOrEmpty(_configuration.CheckSessionIframe))
|
||||
{
|
||||
ticket.Properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe;
|
||||
}
|
||||
|
||||
// Rename?
|
||||
if (Options.UseTokenLifetime)
|
||||
{
|
||||
var issued = validatedToken.ValidFrom;
|
||||
if (issued != DateTime.MinValue)
|
||||
{
|
||||
ticket.Properties.IssuedUtc = issued;
|
||||
}
|
||||
|
||||
var expires = validatedToken.ValidTo;
|
||||
if (expires != DateTime.MinValue)
|
||||
{
|
||||
ticket.Properties.ExpiresUtc = expires;
|
||||
}
|
||||
}
|
||||
|
||||
return ticket;
|
||||
}
|
||||
|
||||
private async Task ValidateOpenIdConnectProtocolAsync(JwtSecurityToken jwt, OpenIdConnectMessage message, bool ValidateCHash = true)
|
||||
{
|
||||
string nonce = jwt.Payload.Nonce;
|
||||
if (Options.CacheNonces)
|
||||
{
|
||||
if (await Options.NonceCache.GetAsync(nonce) != null)
|
||||
{
|
||||
await Options.NonceCache.RemoveAsync(nonce);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the nonce cannot be removed, it was
|
||||
// already used and MUST be rejected.
|
||||
nonce = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
nonce = ReadNonceCookie(nonce);
|
||||
}
|
||||
|
||||
var protocolValidationContext = new OpenIdConnectProtocolValidationContext
|
||||
{
|
||||
Nonce = nonce
|
||||
};
|
||||
|
||||
// If authorization code is null, protocol validator does not validate the chash
|
||||
if (ValidateCHash)
|
||||
{
|
||||
protocolValidationContext.AuthorizationCode = message.Code;
|
||||
}
|
||||
|
||||
Options.ProtocolValidator.Validate(jwt, protocolValidationContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls InvokeReplyPathAsync
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -103,6 +103,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
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
|
||||
|
||||
if (Options.ConfigurationManager == null)
|
||||
{
|
||||
if (Options.Configuration != null)
|
||||
|
|
@ -122,10 +127,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,13 +139,15 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
}
|
||||
}
|
||||
|
||||
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")]
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
{
|
||||
AuthenticationFailed = notification => Task.FromResult(0);
|
||||
AuthorizationCodeReceived = notification => Task.FromResult(0);
|
||||
AuthorizationCodeRedeemed = notificaion => Task.FromResult(0);
|
||||
MessageReceived = notification => Task.FromResult(0);
|
||||
SecurityTokenReceived = notification => Task.FromResult(0);
|
||||
SecurityTokenValidated = notification => Task.FromResult(0);
|
||||
|
|
@ -36,6 +37,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
/// </summary>
|
||||
public Func<AuthorizationCodeReceivedNotification, Task> AuthorizationCodeReceived { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after "authorization code" is redeemed for tokens at the token endpoint.
|
||||
/// </summary>
|
||||
public Func<AuthorizationCodeRedeemedNotification, Task> AuthorizationCodeRedeemed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a protocol message is first received.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
AuthenticationScheme = authenticationScheme;
|
||||
BackchannelTimeout = TimeSpan.FromMinutes(1);
|
||||
Caption = OpenIdConnectAuthenticationDefaults.Caption;
|
||||
GetClaimsFromUserInfoEndpoint = false;
|
||||
ProtocolValidator = new OpenIdConnectProtocolValidator();
|
||||
RefreshOnIssuerKeyNotFound = true;
|
||||
ResponseMode = OpenIdConnectResponseModes.FormPost;
|
||||
|
|
@ -164,6 +165,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
/// </summary>
|
||||
public bool DefaultToCurrentUriOnRedirect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Boolean to set whether the middleware should go to user info endpoint to retrieve additional claims or not after creating an identity from id_token received from token endpoint.
|
||||
/// </summary>
|
||||
public bool GetClaimsFromUserInfoEndpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the discovery endpoint for obtaining metadata
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.IdentityModel.Protocols;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to store the response returned from token endpoint
|
||||
/// </summary>
|
||||
public class OpenIdConnectTokenEndpointResponse
|
||||
{
|
||||
public OpenIdConnectTokenEndpointResponse(JObject jsonResponse)
|
||||
{
|
||||
JsonResponse = jsonResponse;
|
||||
Message = new OpenIdConnectMessage()
|
||||
{
|
||||
AccessToken = JsonResponse.Value<string>(OpenIdConnectParameterNames.AccessToken),
|
||||
IdToken = JsonResponse.Value<string>(OpenIdConnectParameterNames.IdToken),
|
||||
TokenType = JsonResponse.Value<string>(OpenIdConnectParameterNames.TokenType),
|
||||
ExpiresIn = JsonResponse.Value<string>(OpenIdConnectParameterNames.ExpiresIn)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OpenIdConnect message that contains the id token and access tokens
|
||||
/// </summary>
|
||||
public OpenIdConnectMessage Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Json response returned from the token endpoint
|
||||
/// </summary>
|
||||
public JObject JsonResponse { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -180,6 +180,78 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
|
|||
get { return ResourceManager.GetString("OIDCH_0037_RedirectUri"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OIDCH_0038: Id Token is null. Redeeming code : {0} for tokens.
|
||||
/// </summary>
|
||||
internal static string OIDCH_0038_Redeeming_Auth_Code
|
||||
{
|
||||
get { return ResourceManager.GetString("OIDCH_0038_Redeeming_Auth_Code"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OIDCH_0039: Subject claim received from userinfo endpoint does not match the one in the id token.
|
||||
/// </summary>
|
||||
internal static string OIDCH_0039_Subject_Claim_Mismatch
|
||||
{
|
||||
get { return ResourceManager.GetString("OIDCH_0039_Subject_Claim_Mismatch"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OIDCH_0040: Sending request to user info endpoint for retrieving claims.
|
||||
/// </summary>
|
||||
internal static string OIDCH_0040_Sending_Request_UIEndpoint
|
||||
{
|
||||
get { return ResourceManager.GetString("OIDCH_0040_Sending_Request_UIEndpoint"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OIDCH_0041: Subject claim not found in {0}.
|
||||
/// </summary>
|
||||
internal static string OIDCH_0041_Subject_Claim_Not_Found
|
||||
{
|
||||
get { return ResourceManager.GetString("OIDCH_0041_Subject_Claim_Not_Found"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OIDCH_0042: AuthorizationCode redeemed: '{0}'
|
||||
/// </summary>
|
||||
internal static string OIDCH_0042_AuthorizationCodeRedeemed
|
||||
{
|
||||
get { return ResourceManager.GetString("OIDCH_0042_AuthorizationCodeRedeemed"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OIDCH_0043: AuthorizationCodeRedeemedNotification.HandledResponse
|
||||
/// </summary>
|
||||
internal static string OIDCH_0043_AuthorizationCodeRedeemedNotificationHandledResponse
|
||||
{
|
||||
get { return ResourceManager.GetString("OIDCH_0043_AuthorizationCodeRedeemedNotificationHandledResponse"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OIDCH_0044: AuthorizationCodeRedeemedNotification.Skipped
|
||||
/// </summary>
|
||||
internal static string OIDCH_0044_AuthorizationCodeRedeemedNotificationSkipped
|
||||
{
|
||||
get { return ResourceManager.GetString("OIDCH_0044_AuthorizationCodeRedeemedNotificationSkipped"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OIDCH_0045: Cannot process the message.Both id_token and code are missing.
|
||||
/// </summary>
|
||||
internal static string OIDCH_0045_Id_Token_Code_Missing
|
||||
{
|
||||
get { return ResourceManager.GetString("OIDCH_0045_Id_Token_Code_Missing"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OIDCH_0046: UserInfo endpoint is not set. Request to retrieve claims from userinfo endpoint cannot be completed.
|
||||
/// </summary>
|
||||
internal static string OIDCH_0046_UserInfo_Endpoint_Not_Set
|
||||
{
|
||||
get { return ResourceManager.GetString("OIDCH_0046_UserInfo_Endpoint_Not_Set"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OIDCH_0000: Entering: '{0}'.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -162,6 +162,15 @@
|
|||
<data name="OIDCH_0037_RedirectUri" xml:space="preserve">
|
||||
<value>OIDCH_0037: RedirectUri is: '{0}'.</value>
|
||||
</data>
|
||||
<data name="OIDCH_0038_Redeeming_Auth_Code" xml:space="preserve">
|
||||
<value>OIDCH_0038: Id Token is null. Redeeming code : {0} for tokens.</value>
|
||||
</data>
|
||||
<data name="OIDCH_0039_Subject_Claim_Mismatch" xml:space="preserve">
|
||||
<value>OIDCH_0039: Subject claim received from userinfo endpoint does not match the one in the id token.</value>
|
||||
</data>
|
||||
<data name="OIDCH_0040_Sending_Request_UIEndpoint" xml:space="preserve">
|
||||
<value>OIDCH_0040: Sending request to user info endpoint for retrieving claims.</value>
|
||||
</data>
|
||||
<data name="OIDCH_0000_AuthenticateCoreAsync" xml:space="preserve">
|
||||
<value>OIDCH_0000: Entering: '{0}'.</value>
|
||||
</data>
|
||||
|
|
@ -228,4 +237,22 @@
|
|||
<data name="OIDCH_0021_AutomaticConfigurationRefresh" xml:space="preserve">
|
||||
<value>OIDCH_0021: exception of type 'SecurityTokenSignatureKeyNotFoundException' thrown, Options.ConfigurationManager.RequestRefresh() called.</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="OIDCH_0041_Subject_Claim_Not_Found" xml:space="preserve">
|
||||
<value>OIDCH_0041: Subject claim not found in {0}.</value>
|
||||
</data>
|
||||
<data name="OIDCH_0042_AuthorizationCodeRedeemed" xml:space="preserve">
|
||||
<value>OIDCH_0042: Authorization Code redeemed: '{0}'</value>
|
||||
</data>
|
||||
<data name="OIDCH_0043_AuthorizationCodeRedeemedNotificationHandledResponse" xml:space="preserve">
|
||||
<value>OIDCH_0043: AuthorizationCodeRedeemedNotification.HandledResponse</value>
|
||||
</data>
|
||||
<data name="OIDCH_0044_AuthorizationCodeRedeemedNotificationSkipped" xml:space="preserve">
|
||||
<value>OIDCH_0044: AuthorizationCodeRedeemedNotification.Skipped</value>
|
||||
</data>
|
||||
<data name="OIDCH_0045_Id_Token_Code_Missing" xml:space="preserve">
|
||||
<value>OIDCH_0045: Cannot process the message. Both id_token and code are missing.</value>
|
||||
</data>
|
||||
<data name="OIDCH_0046_UserInfo_Endpoint_Not_Set" xml:space="preserve">
|
||||
<value>OIDCH_0046: UserInfo endpoint is not set. Request to retrieve claims from userinfo endpoint cannot be completed.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -43,6 +43,12 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
{ "OIDCH_0020:", LogLevel.Debug },
|
||||
{ "OIDCH_0021:", LogLevel.Verbose },
|
||||
{ "OIDCH_0026:", LogLevel.Error },
|
||||
{ "OIDCH_0038:", LogLevel.Debug },
|
||||
{ "OIDCH_0040:", LogLevel.Debug },
|
||||
{ "OIDCH_0042:", LogLevel.Debug },
|
||||
{ "OIDCH_0043:", LogLevel.Verbose },
|
||||
{ "OIDCH_0044:", LogLevel.Verbose },
|
||||
{ "OIDCH_0045:", LogLevel.Debug }
|
||||
};
|
||||
|
||||
BuildLogEntryList();
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Authentication.OpenIdConnect;
|
||||
using Microsoft.AspNet.Http.Authentication;
|
||||
using Microsoft.AspNet.Http.Features.Authentication;
|
||||
using Microsoft.IdentityModel.Protocols;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
||||
{
|
||||
|
|
@ -14,7 +18,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
public class OpenIdConnectAuthenticationHandlerForTestingAuthenticate : OpenIdConnectAuthenticationHandler
|
||||
{
|
||||
public OpenIdConnectAuthenticationHandlerForTestingAuthenticate()
|
||||
: base()
|
||||
: base(null)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +36,23 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
protected override async Task<OpenIdConnectTokenEndpointResponse> RedeemAuthorizationCodeAsync(string authorizationCode, string redirectUri)
|
||||
{
|
||||
var jsonResponse = new JObject();
|
||||
jsonResponse.Add(OpenIdConnectParameterNames.IdToken, "test token");
|
||||
return new OpenIdConnectTokenEndpointResponse(jsonResponse);
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticationTicket> GetUserInformationAsync(AuthenticationProperties properties, OpenIdConnectMessage message, AuthenticationTicket ticket)
|
||||
{
|
||||
var claimsIdentity = (ClaimsIdentity)ticket.Principal.Identity;
|
||||
if (claimsIdentity == null)
|
||||
{
|
||||
claimsIdentity = new ClaimsIdentity();
|
||||
}
|
||||
claimsIdentity.AddClaim(new Claim("test claim", "test value"));
|
||||
return new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), ticket.Properties, ticket.AuthenticationScheme);
|
||||
}
|
||||
|
||||
//public override bool ShouldHandleScheme(string authenticationScheme)
|
||||
//{
|
||||
|
|
|
|||
|
|
@ -104,8 +104,9 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue();
|
||||
options.Notifications = new OpenIdConnectAuthenticationNotifications
|
||||
{
|
||||
AuthorizationCodeReceived = notification =>
|
||||
AuthorizationCodeRedeemed = notification =>
|
||||
{
|
||||
notification.HandleResponse();
|
||||
if (notification.ProtocolMessage.State == null && !notification.ProtocolMessage.Parameters.ContainsKey(ExpectedStateParameter))
|
||||
return Task.FromResult<object>(null);
|
||||
|
||||
|
|
@ -138,7 +139,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
get
|
||||
{
|
||||
var formater = new AuthenticationPropertiesFormaterKeyValue();
|
||||
var dataset = new TheoryData<LogLevel, int[], Action< OpenIdConnectAuthenticationOptions >, OpenIdConnectMessage>();
|
||||
var dataset = new TheoryData<LogLevel, int[], Action<OpenIdConnectAuthenticationOptions>, OpenIdConnectMessage>();
|
||||
var properties = new AuthenticationProperties();
|
||||
var message = new OpenIdConnectMessage();
|
||||
var validState = UrlEncoder.Default.UrlEncode(formater.Protect(properties));
|
||||
|
|
@ -155,13 +156,13 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
|
||||
// State - null, empty string, invalid
|
||||
message = new OpenIdConnectMessage();
|
||||
dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7 }, StateNullOptions, message);
|
||||
dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7, 28 }, StateNullOptions, message);
|
||||
dataset.Add(LogLevel.Verbose, new int[] { 4, 7 }, StateNullOptions, message);
|
||||
dataset.Add(LogLevel.Error, new int[] { }, StateNullOptions, message);
|
||||
|
||||
message = new OpenIdConnectMessage();
|
||||
message.State = string.Empty;
|
||||
dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7 }, StateEmptyOptions, message);
|
||||
dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7, 28 }, StateEmptyOptions, message);
|
||||
dataset.Add(LogLevel.Verbose, new int[] { 4, 7 }, StateEmptyOptions, message);
|
||||
dataset.Add(LogLevel.Error, new int[] { }, StateEmptyOptions, message);
|
||||
|
||||
|
|
@ -225,6 +226,34 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
dataset.Add(LogLevel.Verbose, new int[] { 7, 16 }, AuthorizationCodeReceivedSkippedOptions, message);
|
||||
dataset.Add(LogLevel.Error, new int[] { }, AuthorizationCodeReceivedSkippedOptions, message);
|
||||
|
||||
message = new OpenIdConnectMessage();
|
||||
message.Code = Guid.NewGuid().ToString();
|
||||
message.State = validState;
|
||||
message.IdToken = "test token";
|
||||
dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20, 14, 15 }, AuthorizationCodeReceivedHandledOptions, message);
|
||||
dataset.Add(LogLevel.Verbose, new int[] { 7, 15 }, AuthorizationCodeReceivedHandledOptions, message);
|
||||
dataset.Add(LogLevel.Error, new int[] { }, AuthorizationCodeReceivedHandledOptions, message);
|
||||
|
||||
dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20, 14, 16 }, AuthorizationCodeReceivedSkippedOptions, message);
|
||||
dataset.Add(LogLevel.Verbose, new int[] { 7, 16 }, AuthorizationCodeReceivedSkippedOptions, message);
|
||||
dataset.Add(LogLevel.Error, new int[] { }, AuthorizationCodeReceivedSkippedOptions, message);
|
||||
|
||||
// CodeReceivedAndRedeemed and GetUserInformationFromUIEndpoint
|
||||
message = new OpenIdConnectMessage();
|
||||
message.IdToken = null;
|
||||
message.Code = Guid.NewGuid().ToString();
|
||||
dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7, 14, 23, 25, 26 }, CodeReceivedAndRedeemedHandledOptions, message);
|
||||
dataset.Add(LogLevel.Verbose, new int[] { 4, 7, 26 }, CodeReceivedAndRedeemedHandledOptions, message);
|
||||
dataset.Add(LogLevel.Error, new int[] { }, CodeReceivedAndRedeemedHandledOptions, message);
|
||||
|
||||
dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7, 14, 23, 25, 27 }, CodeReceivedAndRedeemedSkippedOptions, message);
|
||||
dataset.Add(LogLevel.Verbose, new int[] { 4, 7, 27 }, CodeReceivedAndRedeemedSkippedOptions, message);
|
||||
dataset.Add(LogLevel.Error, new int[] { }, CodeReceivedAndRedeemedSkippedOptions, message);
|
||||
|
||||
dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7, 14, 23, 25, 24, 12 }, GetUserInfoFromUIEndpoint, message);
|
||||
dataset.Add(LogLevel.Verbose, new int[] { 4, 7, 12 }, GetUserInfoFromUIEndpoint, message);
|
||||
dataset.Add(LogLevel.Error, new int[] { }, GetUserInfoFromUIEndpoint, message);
|
||||
|
||||
return dataset;
|
||||
}
|
||||
}
|
||||
|
|
@ -243,6 +272,8 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
private static void AuthorizationCodeReceivedHandledOptions(OpenIdConnectAuthenticationOptions options)
|
||||
{
|
||||
DefaultOptions(options);
|
||||
options.SecurityTokenValidators = new Collection<ISecurityTokenValidator> { MockSecurityTokenValidator() };
|
||||
options.ProtocolValidator = MockProtocolValidator();
|
||||
options.Notifications =
|
||||
new OpenIdConnectAuthenticationNotifications
|
||||
{
|
||||
|
|
@ -257,6 +288,8 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
private static void AuthorizationCodeReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options)
|
||||
{
|
||||
DefaultOptions(options);
|
||||
options.SecurityTokenValidators = new Collection<ISecurityTokenValidator> { MockSecurityTokenValidator() };
|
||||
options.ProtocolValidator = MockProtocolValidator();
|
||||
options.Notifications =
|
||||
new OpenIdConnectAuthenticationNotifications
|
||||
{
|
||||
|
|
@ -271,6 +304,8 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
private static void AuthenticationErrorHandledOptions(OpenIdConnectAuthenticationOptions options)
|
||||
{
|
||||
DefaultOptions(options);
|
||||
options.SecurityTokenValidators = new Collection<ISecurityTokenValidator> { MockSecurityTokenValidator() };
|
||||
options.ProtocolValidator = MockProtocolValidator();
|
||||
options.Notifications =
|
||||
new OpenIdConnectAuthenticationNotifications
|
||||
{
|
||||
|
|
@ -285,6 +320,8 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
private static void AuthenticationErrorSkippedOptions(OpenIdConnectAuthenticationOptions options)
|
||||
{
|
||||
DefaultOptions(options);
|
||||
options.SecurityTokenValidators = new Collection<ISecurityTokenValidator> { MockSecurityTokenValidator() };
|
||||
options.ProtocolValidator = MockProtocolValidator();
|
||||
options.Notifications =
|
||||
new OpenIdConnectAuthenticationNotifications
|
||||
{
|
||||
|
|
@ -310,6 +347,57 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
};
|
||||
}
|
||||
|
||||
private static void CodeReceivedAndRedeemedHandledOptions(OpenIdConnectAuthenticationOptions options)
|
||||
{
|
||||
DefaultOptions(options);
|
||||
options.ResponseType = OpenIdConnectResponseTypes.Code;
|
||||
options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue();
|
||||
options.Notifications =
|
||||
new OpenIdConnectAuthenticationNotifications
|
||||
{
|
||||
AuthorizationCodeRedeemed = (notification) =>
|
||||
{
|
||||
notification.HandleResponse();
|
||||
return Task.FromResult<object>(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void CodeReceivedAndRedeemedSkippedOptions(OpenIdConnectAuthenticationOptions options)
|
||||
{
|
||||
DefaultOptions(options);
|
||||
options.ResponseType = OpenIdConnectResponseTypes.Code;
|
||||
options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue();
|
||||
options.Notifications =
|
||||
new OpenIdConnectAuthenticationNotifications
|
||||
{
|
||||
AuthorizationCodeRedeemed = (notification) =>
|
||||
{
|
||||
notification.SkipToNextMiddleware();
|
||||
return Task.FromResult<object>(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void GetUserInfoFromUIEndpoint(OpenIdConnectAuthenticationOptions options)
|
||||
{
|
||||
DefaultOptions(options);
|
||||
options.ResponseType = OpenIdConnectResponseTypes.Code;
|
||||
options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue();
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
options.SecurityTokenValidators = new Collection<ISecurityTokenValidator> { MockSecurityTokenValidator() };
|
||||
options.Notifications =
|
||||
new OpenIdConnectAuthenticationNotifications
|
||||
{
|
||||
SecurityTokenValidated = (notification) =>
|
||||
{
|
||||
var claimValue = notification.AuthenticationTicket.Principal.FindFirst("test claim");
|
||||
Assert.Equal(claimValue.Value, "test value");
|
||||
notification.HandleResponse();
|
||||
return Task.FromResult<object>(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
private static void MessageReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options)
|
||||
{
|
||||
DefaultOptions(options);
|
||||
|
|
@ -357,6 +445,21 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
};
|
||||
}
|
||||
|
||||
private static ISecurityTokenValidator MockSecurityTokenValidator()
|
||||
{
|
||||
var mockValidator = new Mock<ISecurityTokenValidator>();
|
||||
mockValidator.Setup(v => v.ValidateToken(It.IsAny<string>(), It.IsAny<TokenValidationParameters>(), out specCompliantJwt)).Returns(new ClaimsPrincipal());
|
||||
mockValidator.Setup(v => v.CanReadToken(It.IsAny<string>())).Returns(true);
|
||||
return mockValidator.Object;
|
||||
}
|
||||
|
||||
private static OpenIdConnectProtocolValidator MockProtocolValidator()
|
||||
{
|
||||
var mockProtocolValidator = new Mock<OpenIdConnectProtocolValidator>();
|
||||
mockProtocolValidator.Setup(v => v.Validate(It.IsAny<JwtSecurityToken>(), It.IsAny<OpenIdConnectProtocolValidationContext>()));
|
||||
return mockProtocolValidator.Object;
|
||||
}
|
||||
|
||||
private static void SecurityTokenValidatorCannotReadToken(OpenIdConnectAuthenticationOptions options)
|
||||
{
|
||||
AuthenticationErrorHandledOptions(options);
|
||||
|
|
@ -380,10 +483,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
private static void SecurityTokenValidatorValidatesAllTokens(OpenIdConnectAuthenticationOptions options)
|
||||
{
|
||||
DefaultOptions(options);
|
||||
var mockValidator = new Mock<ISecurityTokenValidator>();
|
||||
mockValidator.Setup(v => v.ValidateToken(It.IsAny<string>(), It.IsAny<TokenValidationParameters>(), out specCompliantJwt)).Returns(new ClaimsPrincipal());
|
||||
mockValidator.Setup(v => v.CanReadToken(It.IsAny<string>())).Returns(true);
|
||||
options.SecurityTokenValidators = new Collection<ISecurityTokenValidator> { mockValidator.Object };
|
||||
options.SecurityTokenValidators = new Collection<ISecurityTokenValidator> { MockSecurityTokenValidator() };
|
||||
options.ProtocolValidator.RequireTimeStampInNonce = false;
|
||||
options.ProtocolValidator.RequireNonce = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,10 +142,6 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
{
|
||||
properties.Items.Add("item1", Guid.NewGuid().ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, queryValues.RedirectUri);
|
||||
}
|
||||
|
||||
var server = CreateServer(options =>
|
||||
{
|
||||
|
|
@ -164,6 +160,16 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
|
|||
|
||||
var transaction = await SendAsync(server, DefaultHost + challenge);
|
||||
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
|
||||
|
||||
if (challenge != ChallengeWithProperties)
|
||||
{
|
||||
if (userState != null)
|
||||
{
|
||||
properties.Items.Add(OpenIdConnectAuthenticationDefaults.UserstatePropertiesKey, userState);
|
||||
}
|
||||
properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, queryValues.RedirectUri);
|
||||
}
|
||||
|
||||
queryValues.State = stateDataFormat.Protect(properties);
|
||||
queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters(new string[] { OpenIdConnectParameterNames.State }));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue