#456 Unify OIDC Code/IdToken/Hybride flows.
This commit is contained in:
parent
bbcabc0212
commit
34bc9c52e1
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||||
|
|
@ -34,6 +35,28 @@ namespace OpenIdConnectSample
|
||||||
{
|
{
|
||||||
loggerfactory.AddConsole(LogLevel.Information);
|
loggerfactory.AddConsole(LogLevel.Information);
|
||||||
|
|
||||||
|
// Simple error page
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!context.Response.HasStarted)
|
||||||
|
{
|
||||||
|
context.Response.Clear();
|
||||||
|
context.Response.StatusCode = 500;
|
||||||
|
await context.Response.WriteAsync(ex.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.UseIISPlatformHandler();
|
app.UseIISPlatformHandler();
|
||||||
|
|
||||||
app.UseCookieAuthentication(new CookieAuthenticationOptions
|
app.UseCookieAuthentication(new CookieAuthenticationOptions
|
||||||
|
|
@ -52,17 +75,30 @@ namespace OpenIdConnectSample
|
||||||
|
|
||||||
app.Run(async context =>
|
app.Run(async context =>
|
||||||
{
|
{
|
||||||
if (!context.User.Identities.Any(identity => identity.IsAuthenticated))
|
if (context.Request.Path.Equals("/signout"))
|
||||||
{
|
{
|
||||||
await context.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
|
await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
context.Response.ContentType = "text/html";
|
||||||
context.Response.ContentType = "text/plain";
|
await context.Response.WriteAsync($"<html><body>Signing out {context.User.Identity.Name}<br>{Environment.NewLine}");
|
||||||
await context.Response.WriteAsync("Hello First timer");
|
await context.Response.WriteAsync("<a href=\"/\">Sign In</a>");
|
||||||
|
await context.Response.WriteAsync($"</body></html>");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Response.ContentType = "text/plain";
|
if (!context.User.Identities.Any(identity => identity.IsAuthenticated))
|
||||||
await context.Response.WriteAsync("Hello Authenticated User");
|
{
|
||||||
|
await context.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.ContentType = "text/html";
|
||||||
|
await context.Response.WriteAsync($"<html><body>Hello Authenticated User {context.User.Identity.Name}<br>{Environment.NewLine}");
|
||||||
|
foreach (var claim in context.User.Claims)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync($"{claim.Type}: {claim.Value}<br>{Environment.NewLine}");
|
||||||
|
}
|
||||||
|
await context.Response.WriteAsync("<a href=\"/signout\">Sign Out</a>");
|
||||||
|
await context.Response.WriteAsync($"</body></html>");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -308,16 +308,16 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
{
|
{
|
||||||
Logger.LogTrace(10, "Entering: {0}." + nameof(HandleRemoteAuthenticateAsync), GetType());
|
Logger.LogTrace(10, "Entering: {0}." + nameof(HandleRemoteAuthenticateAsync), GetType());
|
||||||
|
|
||||||
OpenIdConnectMessage message = null;
|
OpenIdConnectMessage authorizationResponse = null;
|
||||||
|
|
||||||
if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
|
authorizationResponse = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
|
||||||
|
|
||||||
// response_mode=query (explicit or not) and a response_type containing id_token
|
// response_mode=query (explicit or not) and a response_type containing id_token
|
||||||
// or token are not considered as a safe combination and MUST be rejected.
|
// or token are not considered as a safe combination and MUST be rejected.
|
||||||
// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security
|
// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security
|
||||||
if (!string.IsNullOrEmpty(message.IdToken) || !string.IsNullOrEmpty(message.AccessToken))
|
if (!string.IsNullOrEmpty(authorizationResponse.IdToken) || !string.IsNullOrEmpty(authorizationResponse.AccessToken))
|
||||||
{
|
{
|
||||||
if (Options.SkipUnrecognizedRequests)
|
if (Options.SkipUnrecognizedRequests)
|
||||||
{
|
{
|
||||||
|
|
@ -336,10 +336,10 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
&& Request.Body.CanRead)
|
&& Request.Body.CanRead)
|
||||||
{
|
{
|
||||||
var form = await Request.ReadFormAsync();
|
var form = await Request.ReadFormAsync();
|
||||||
message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
|
authorizationResponse = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message == null)
|
if (authorizationResponse == null)
|
||||||
{
|
{
|
||||||
if (Options.SkipUnrecognizedRequests)
|
if (Options.SkipUnrecognizedRequests)
|
||||||
{
|
{
|
||||||
|
|
@ -349,54 +349,52 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
return AuthenticateResult.Fail("No message.");
|
return AuthenticateResult.Fail("No message.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AuthenticateResult result;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var messageReceivedContext = await RunMessageReceivedEventAsync(message);
|
var messageReceivedContext = await RunMessageReceivedEventAsync(authorizationResponse);
|
||||||
if (messageReceivedContext.HandledResponse)
|
if (CheckEventResult(messageReceivedContext, out result))
|
||||||
{
|
{
|
||||||
return AuthenticateResult.Success(messageReceivedContext.Ticket);
|
return result;
|
||||||
}
|
}
|
||||||
else if (messageReceivedContext.Skipped)
|
authorizationResponse = messageReceivedContext.ProtocolMessage;
|
||||||
{
|
|
||||||
return AuthenticateResult.Skip();
|
|
||||||
}
|
|
||||||
message = messageReceivedContext.ProtocolMessage;
|
|
||||||
|
|
||||||
// Fail if state is missing, it's required for the correlation id.
|
// Fail if state is missing, it's required for the correlation id.
|
||||||
if (string.IsNullOrEmpty(message.State))
|
if (string.IsNullOrEmpty(authorizationResponse.State))
|
||||||
{
|
{
|
||||||
// This wasn't a valid OIDC message, it may not have been intended for us.
|
// This wasn't a valid OIDC message, it may not have been intended for us.
|
||||||
|
Logger.LogDebug(11, "message.State is null or empty.");
|
||||||
if (Options.SkipUnrecognizedRequests)
|
if (Options.SkipUnrecognizedRequests)
|
||||||
{
|
{
|
||||||
return AuthenticateResult.Skip();
|
return AuthenticateResult.Skip();
|
||||||
}
|
}
|
||||||
Logger.LogDebug(11, "message.State is null or empty.");
|
|
||||||
return AuthenticateResult.Fail(Resources.MessageStateIsNullOrEmpty);
|
return AuthenticateResult.Fail(Resources.MessageStateIsNullOrEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if state exists and we failed to 'unprotect' this is not a message we should process.
|
// if state exists and we failed to 'unprotect' this is not a message we should process.
|
||||||
var properties = Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(message.State));
|
var properties = Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(authorizationResponse.State));
|
||||||
if (properties == null)
|
if (properties == null)
|
||||||
{
|
{
|
||||||
|
Logger.LogDebug(12, "Unable to read the message.State.");
|
||||||
if (Options.SkipUnrecognizedRequests)
|
if (Options.SkipUnrecognizedRequests)
|
||||||
{
|
{
|
||||||
// Not for us?
|
// Not for us?
|
||||||
return AuthenticateResult.Skip();
|
return AuthenticateResult.Skip();
|
||||||
}
|
}
|
||||||
Logger.LogError(12, "Unable to read the message.State.");
|
|
||||||
return AuthenticateResult.Fail(Resources.MessageStateIsInvalid);
|
return AuthenticateResult.Fail(Resources.MessageStateIsInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if any of the error fields are set, throw error null
|
// if any of the error fields are set, throw error null
|
||||||
if (!string.IsNullOrEmpty(message.Error))
|
if (!string.IsNullOrEmpty(authorizationResponse.Error))
|
||||||
{
|
{
|
||||||
Logger.LogError(13, "Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'.", message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null");
|
Logger.LogError(13, "Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'.", authorizationResponse.Error, authorizationResponse.ErrorDescription ?? "ErrorDecription null", authorizationResponse.ErrorUri ?? "ErrorUri null");
|
||||||
return AuthenticateResult.Fail(new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.MessageContainsError, message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null")));
|
return AuthenticateResult.Fail(new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.MessageContainsError, authorizationResponse.Error, authorizationResponse.ErrorDescription ?? "ErrorDecription null", authorizationResponse.ErrorUri ?? "ErrorUri null")));
|
||||||
}
|
}
|
||||||
|
|
||||||
string userstate = null;
|
string userstate = null;
|
||||||
properties.Items.TryGetValue(OpenIdConnectDefaults.UserstatePropertiesKey, out userstate);
|
properties.Items.TryGetValue(OpenIdConnectDefaults.UserstatePropertiesKey, out userstate);
|
||||||
message.State = userstate;
|
authorizationResponse.State = userstate;
|
||||||
|
|
||||||
if (!ValidateCorrelationId(properties))
|
if (!ValidateCorrelationId(properties))
|
||||||
{
|
{
|
||||||
|
|
@ -409,38 +407,113 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
|
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogTrace(15, "Authorization response received.");
|
var authorizationResponseReceivedContext = await RunAuthorizationResponseReceivedEventAsync(authorizationResponse, properties);
|
||||||
var authorizationResponseReceivedContext = new AuthorizationResponseReceivedContext(Context, Options, properties)
|
if (CheckEventResult(authorizationResponseReceivedContext, out result))
|
||||||
{
|
{
|
||||||
ProtocolMessage = message
|
return result;
|
||||||
};
|
|
||||||
await Options.Events.AuthorizationResponseReceived(authorizationResponseReceivedContext);
|
|
||||||
if (authorizationResponseReceivedContext.HandledResponse)
|
|
||||||
{
|
|
||||||
Logger.LogDebug(16, "AuthorizationResponseReceived.HandledResponse");
|
|
||||||
return AuthenticateResult.Success(authorizationResponseReceivedContext.Ticket);
|
|
||||||
}
|
}
|
||||||
else if (authorizationResponseReceivedContext.Skipped)
|
authorizationResponse = authorizationResponseReceivedContext.ProtocolMessage;
|
||||||
{
|
|
||||||
Logger.LogDebug(17, "AuthorizationResponseReceived.Skipped");
|
|
||||||
return AuthenticateResult.Skip();
|
|
||||||
}
|
|
||||||
message = authorizationResponseReceivedContext.ProtocolMessage;
|
|
||||||
properties = authorizationResponseReceivedContext.Properties;
|
properties = authorizationResponseReceivedContext.Properties;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(message.IdToken) && !string.IsNullOrEmpty(message.Code))
|
PopulateSessionProperties(authorizationResponse, properties);
|
||||||
|
|
||||||
|
AuthenticationTicket ticket = null;
|
||||||
|
JwtSecurityToken jwt = null;
|
||||||
|
string nonce = null;
|
||||||
|
var validationParameters = Options.TokenValidationParameters.Clone();
|
||||||
|
|
||||||
|
// Hybrid or Implicit flow
|
||||||
|
if (!string.IsNullOrEmpty(authorizationResponse.IdToken))
|
||||||
{
|
{
|
||||||
return await HandleCodeOnlyFlow(message, properties);
|
Logger.LogDebug(23, "'id_token' received.");
|
||||||
|
ticket = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt);
|
||||||
|
|
||||||
|
nonce = jwt?.Payload.Nonce;
|
||||||
|
if (!string.IsNullOrEmpty(nonce))
|
||||||
|
{
|
||||||
|
nonce = ReadNonceCookie(nonce);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrEmpty(message.IdToken))
|
|
||||||
|
Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext()
|
||||||
{
|
{
|
||||||
return await HandleIdTokenFlows(message, properties);
|
ClientId = Options.ClientId,
|
||||||
}
|
ProtocolMessage = authorizationResponse,
|
||||||
else
|
ValidatedIdToken = jwt,
|
||||||
|
Nonce = nonce
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: AuthorizationResponseValidated event?
|
||||||
|
|
||||||
|
OpenIdConnectMessage tokenEndpointResponse = null;
|
||||||
|
|
||||||
|
// Authorization Code or Hybrid flow
|
||||||
|
if (!string.IsNullOrEmpty(authorizationResponse.Code))
|
||||||
{
|
{
|
||||||
Logger.LogTrace(18, "Cannot process the message. Both id_token and code are missing.");
|
// TODO: Does this event provide any value over AuthorizationResponseReceived or AuthorizationResponseValidated?
|
||||||
return AuthenticateResult.Fail(Resources.IdTokenCodeMissing);
|
var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(authorizationResponse, properties, ticket, jwt);
|
||||||
|
if (CheckEventResult(authorizationCodeReceivedContext, out result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
authorizationResponse = authorizationCodeReceivedContext.ProtocolMessage;
|
||||||
|
var code = authorizationCodeReceivedContext.Code;
|
||||||
|
|
||||||
|
tokenEndpointResponse = await RedeemAuthorizationCodeAsync(code, authorizationCodeReceivedContext.RedirectUri);
|
||||||
|
|
||||||
|
var authorizationCodeRedeemedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, properties);
|
||||||
|
if (CheckEventResult(authorizationCodeRedeemedContext, out result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
authorizationResponse = authorizationCodeRedeemedContext.ProtocolMessage;
|
||||||
|
tokenEndpointResponse = authorizationCodeRedeemedContext.TokenEndpointResponse;
|
||||||
|
|
||||||
|
// We only have to process the IdToken if we didn't already get one in the AuthorizationResponse
|
||||||
|
if (ticket == 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].
|
||||||
|
validationParameters.RequireSignedTokens = false;
|
||||||
|
|
||||||
|
ticket = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out jwt);
|
||||||
|
|
||||||
|
nonce = jwt?.Payload.Nonce;
|
||||||
|
if (!string.IsNullOrEmpty(nonce))
|
||||||
|
{
|
||||||
|
nonce = ReadNonceCookie(nonce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext()
|
||||||
|
{
|
||||||
|
ClientId = Options.ClientId,
|
||||||
|
ProtocolMessage = tokenEndpointResponse,
|
||||||
|
ValidatedIdToken = jwt,
|
||||||
|
Nonce = nonce
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var authenticationValidatedContext = await RunAuthenticationValidatedEventAsync(authorizationResponse, ticket, properties, tokenEndpointResponse);
|
||||||
|
if (CheckEventResult(authenticationValidatedContext, out result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
authorizationResponse = authenticationValidatedContext.ProtocolMessage;
|
||||||
|
tokenEndpointResponse = authenticationValidatedContext.TokenEndpointResponse;
|
||||||
|
ticket = authenticationValidatedContext.Ticket;
|
||||||
|
|
||||||
|
if (Options.SaveTokensAsClaims)
|
||||||
|
{
|
||||||
|
SaveTokens(ticket.Principal, tokenEndpointResponse ?? authorizationResponse, jwt.Issuer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Options.GetClaimsFromUserInfoEndpoint)
|
||||||
|
{
|
||||||
|
return await GetUserInformationAsync(tokenEndpointResponse ?? authorizationResponse, jwt, ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthenticateResult.Success(ticket);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
|
@ -456,179 +529,43 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var authenticationFailedContext = await RunAuthenticationFailedEventAsync(message, exception);
|
var authenticationFailedContext = await RunAuthenticationFailedEventAsync(authorizationResponse, exception);
|
||||||
if (authenticationFailedContext.HandledResponse)
|
if (CheckEventResult(authenticationFailedContext, out result))
|
||||||
{
|
{
|
||||||
return AuthenticateResult.Success(authenticationFailedContext.Ticket);
|
return result;
|
||||||
}
|
|
||||||
else if (authenticationFailedContext.Skipped)
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Skip();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw;
|
return AuthenticateResult.Fail(exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization Code Flow
|
private bool CheckEventResult(BaseControlContext context, out AuthenticateResult result)
|
||||||
private async Task<AuthenticateResult> HandleCodeOnlyFlow(OpenIdConnectMessage message, AuthenticationProperties properties)
|
|
||||||
{
|
{
|
||||||
AuthenticationTicket ticket = null;
|
if (context.HandledResponse)
|
||||||
JwtSecurityToken jwt = null;
|
|
||||||
|
|
||||||
Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext()
|
|
||||||
{
|
{
|
||||||
ClientId = Options.ClientId,
|
result = AuthenticateResult.Success(context.Ticket);
|
||||||
ProtocolMessage = message,
|
return true;
|
||||||
});
|
|
||||||
|
|
||||||
var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(message, properties, ticket, jwt);
|
|
||||||
if (authorizationCodeReceivedContext.HandledResponse)
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Success(authorizationCodeReceivedContext.Ticket);
|
|
||||||
}
|
}
|
||||||
else if (authorizationCodeReceivedContext.Skipped)
|
else if (context.Skipped)
|
||||||
{
|
{
|
||||||
return AuthenticateResult.Skip();
|
result = AuthenticateResult.Skip();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
message = authorizationCodeReceivedContext.ProtocolMessage;
|
result = null;
|
||||||
var code = authorizationCodeReceivedContext.Code;
|
return false;
|
||||||
|
|
||||||
// Redeeming authorization code for tokens
|
|
||||||
Logger.LogTrace(21, "Id Token is null. Redeeming code '{0}' for tokens.", code);
|
|
||||||
|
|
||||||
var tokenEndpointResponse = await RedeemAuthorizationCodeAsync(code, authorizationCodeReceivedContext.RedirectUri);
|
|
||||||
|
|
||||||
var authorizationCodeRedeemedContext = await RunTokenResponseReceivedEventAsync(message, tokenEndpointResponse, properties);
|
|
||||||
if (authorizationCodeRedeemedContext.HandledResponse)
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Success(authorizationCodeRedeemedContext.Ticket);
|
|
||||||
}
|
|
||||||
else if (authorizationCodeRedeemedContext.Skipped)
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Skip();
|
|
||||||
}
|
|
||||||
|
|
||||||
message = authorizationCodeRedeemedContext.ProtocolMessage;
|
|
||||||
tokenEndpointResponse = authorizationCodeRedeemedContext.TokenEndpointResponse;
|
|
||||||
|
|
||||||
// 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.RequireSignedTokens = false;
|
|
||||||
|
|
||||||
ticket = ValidateToken(tokenEndpointResponse.IdToken, message, properties, validationParameters, out jwt);
|
|
||||||
|
|
||||||
var nonce = jwt?.Payload.Nonce;
|
|
||||||
if (!string.IsNullOrEmpty(nonce))
|
|
||||||
{
|
|
||||||
nonce = ReadNonceCookie(nonce);
|
|
||||||
}
|
|
||||||
|
|
||||||
Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext()
|
|
||||||
{
|
|
||||||
ClientId = Options.ClientId,
|
|
||||||
ProtocolMessage = tokenEndpointResponse,
|
|
||||||
ValidatedIdToken = jwt,
|
|
||||||
Nonce = nonce
|
|
||||||
});
|
|
||||||
|
|
||||||
var authenticationValidatedContext = await RunAuthenticationValidatedEventAsync(message, ticket, properties, tokenEndpointResponse);
|
|
||||||
if (authenticationValidatedContext.HandledResponse)
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Success(authenticationValidatedContext.Ticket);
|
|
||||||
}
|
|
||||||
else if (authenticationValidatedContext.Skipped)
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Skip();
|
|
||||||
}
|
|
||||||
ticket = authenticationValidatedContext.Ticket;
|
|
||||||
|
|
||||||
if (Options.SaveTokensAsClaims)
|
|
||||||
{
|
|
||||||
// Persist the tokens extracted from the token response.
|
|
||||||
SaveTokens(ticket.Principal, tokenEndpointResponse, jwt.Issuer, saveRefreshToken: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Options.GetClaimsFromUserInfoEndpoint)
|
|
||||||
{
|
|
||||||
Logger.LogTrace(22, "Sending request to user info endpoint for retrieving claims.");
|
|
||||||
ticket = await GetUserInformationAsync(tokenEndpointResponse, jwt, ticket);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AuthenticateResult.Success(ticket);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implicit Flow or Hybrid Flow
|
private void PopulateSessionProperties(OpenIdConnectMessage message, AuthenticationProperties properties)
|
||||||
private async Task<AuthenticateResult> HandleIdTokenFlows(OpenIdConnectMessage message, AuthenticationProperties properties)
|
|
||||||
{
|
{
|
||||||
Logger.LogTrace(23, "'id_token' received: '{0}'", message.IdToken);
|
if (!string.IsNullOrEmpty(message.SessionState))
|
||||||
|
|
||||||
JwtSecurityToken jwt = null;
|
|
||||||
var validationParameters = Options.TokenValidationParameters.Clone();
|
|
||||||
var ticket = ValidateToken(message.IdToken, message, properties, validationParameters, out jwt);
|
|
||||||
|
|
||||||
var nonce = jwt?.Payload.Nonce;
|
|
||||||
if (!string.IsNullOrEmpty(nonce))
|
|
||||||
{
|
{
|
||||||
nonce = ReadNonceCookie(nonce);
|
properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext()
|
if (!string.IsNullOrEmpty(_configuration.CheckSessionIframe))
|
||||||
{
|
{
|
||||||
ClientId = Options.ClientId,
|
properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe;
|
||||||
ProtocolMessage = message,
|
|
||||||
ValidatedIdToken = jwt,
|
|
||||||
Nonce = nonce
|
|
||||||
});
|
|
||||||
|
|
||||||
var authenticationValidatedContext = await RunAuthenticationValidatedEventAsync(message, ticket, properties, tokenEndpointResponse: null);
|
|
||||||
if (authenticationValidatedContext.HandledResponse)
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Success(authenticationValidatedContext.Ticket);
|
|
||||||
}
|
}
|
||||||
else if (authenticationValidatedContext.Skipped)
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Skip();
|
|
||||||
}
|
|
||||||
message = authenticationValidatedContext.ProtocolMessage;
|
|
||||||
ticket = authenticationValidatedContext.Ticket;
|
|
||||||
|
|
||||||
// Hybrid Flow
|
|
||||||
if (message.Code != null)
|
|
||||||
{
|
|
||||||
var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(message, properties, ticket, jwt);
|
|
||||||
if (authorizationCodeReceivedContext.HandledResponse)
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Success(authorizationCodeReceivedContext.Ticket);
|
|
||||||
}
|
|
||||||
else if (authorizationCodeReceivedContext.Skipped)
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Skip();
|
|
||||||
}
|
|
||||||
message = authorizationCodeReceivedContext.ProtocolMessage;
|
|
||||||
ticket = authorizationCodeReceivedContext.Ticket;
|
|
||||||
|
|
||||||
if (Options.SaveTokensAsClaims)
|
|
||||||
{
|
|
||||||
// TODO: call SaveTokens with the token response and set
|
|
||||||
// saveRefreshToken to true when the hybrid flow is fully implemented.
|
|
||||||
SaveTokens(ticket.Principal, message, jwt.Issuer, saveRefreshToken: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Implicit Flow
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (Options.SaveTokensAsClaims)
|
|
||||||
{
|
|
||||||
// Note: don't save the refresh token when it is extracted from the authorization
|
|
||||||
// response, since it's not a valid parameter when using the implicit flow.
|
|
||||||
// See http://openid.net/specs/openid-connect-core-1_0.html#Authentication
|
|
||||||
// and https://tools.ietf.org/html/rfc6749#section-4.2.2.
|
|
||||||
SaveTokens(ticket.Principal, message, jwt.Issuer, saveRefreshToken: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return AuthenticateResult.Success(ticket);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -639,6 +576,8 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
/// <returns>OpenIdConnect message that has tokens inside it.</returns>
|
/// <returns>OpenIdConnect message that has tokens inside it.</returns>
|
||||||
protected virtual async Task<OpenIdConnectMessage> RedeemAuthorizationCodeAsync(string authorizationCode, string redirectUri)
|
protected virtual async Task<OpenIdConnectMessage> RedeemAuthorizationCodeAsync(string authorizationCode, string redirectUri)
|
||||||
{
|
{
|
||||||
|
Logger.LogDebug(21, "Redeeming code for tokens.");
|
||||||
|
|
||||||
var openIdMessage = new OpenIdConnectMessage()
|
var openIdMessage = new OpenIdConnectMessage()
|
||||||
{
|
{
|
||||||
ClientId = Options.ClientId,
|
ClientId = Options.ClientId,
|
||||||
|
|
@ -648,6 +587,8 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
RedirectUri = redirectUri
|
RedirectUri = redirectUri
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Event that lets you customize the message. E.g. use certificates, specify resources.
|
||||||
|
|
||||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, _configuration.TokenEndpoint);
|
var requestMessage = new HttpRequestMessage(HttpMethod.Post, _configuration.TokenEndpoint);
|
||||||
requestMessage.Content = new FormUrlEncodedContent(openIdMessage.Parameters);
|
requestMessage.Content = new FormUrlEncodedContent(openIdMessage.Parameters);
|
||||||
var responseMessage = await Backchannel.SendAsync(requestMessage);
|
var responseMessage = await Backchannel.SendAsync(requestMessage);
|
||||||
|
|
@ -663,21 +604,28 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
/// <param name="message">message that is being processed</param>
|
/// <param name="message">message that is being processed</param>
|
||||||
/// <param name="ticket">authentication ticket with claims principal and identities</param>
|
/// <param name="ticket">authentication ticket with claims principal and identities</param>
|
||||||
/// <returns>Authentication ticket with identity with additional claims, if any.</returns>
|
/// <returns>Authentication ticket with identity with additional claims, if any.</returns>
|
||||||
protected virtual async Task<AuthenticationTicket> GetUserInformationAsync(OpenIdConnectMessage message, JwtSecurityToken jwt, AuthenticationTicket ticket)
|
protected virtual async Task<AuthenticateResult> GetUserInformationAsync(OpenIdConnectMessage message, JwtSecurityToken jwt, AuthenticationTicket ticket)
|
||||||
{
|
{
|
||||||
var userInfoEndpoint = _configuration?.UserInfoEndpoint;
|
var userInfoEndpoint = _configuration?.UserInfoEndpoint;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userInfoEndpoint))
|
if (string.IsNullOrEmpty(userInfoEndpoint))
|
||||||
{
|
{
|
||||||
Logger.LogWarning(24, nameof(_configuration.UserInfoEndpoint) + " is not set. Request to retrieve claims cannot be completed.");
|
Logger.LogDebug(24, $"{nameof(_configuration.UserInfoEndpoint)} is not set. Claims cannot be retrieved.");
|
||||||
return ticket;
|
return AuthenticateResult.Success(ticket);
|
||||||
}
|
}
|
||||||
|
if (string.IsNullOrEmpty(message.AccessToken))
|
||||||
|
{
|
||||||
|
Logger.LogDebug(47, "The access_token is not available. Claims cannot be retrieved.");
|
||||||
|
return AuthenticateResult.Success(ticket);
|
||||||
|
}
|
||||||
|
Logger.LogTrace(22, "Retrieving claims from the user info endpoint.");
|
||||||
|
|
||||||
var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint);
|
var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint);
|
||||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", message.AccessToken);
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", message.AccessToken);
|
||||||
var responseMessage = await Backchannel.SendAsync(requestMessage);
|
var responseMessage = await Backchannel.SendAsync(requestMessage);
|
||||||
responseMessage.EnsureSuccessStatusCode();
|
responseMessage.EnsureSuccessStatusCode();
|
||||||
var userInfoResponse = await responseMessage.Content.ReadAsStringAsync();
|
var userInfoResponse = await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
JObject user;
|
JObject user;
|
||||||
var contentType = responseMessage.Content.Headers.ContentType;
|
var contentType = responseMessage.Content.Headers.ContentType;
|
||||||
if (contentType.MediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase))
|
if (contentType.MediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
@ -691,17 +639,14 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new NotSupportedException("Unknown response type: " + contentType.MediaType);
|
return AuthenticateResult.Fail("Unknown response type: " + contentType.MediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userInformationReceivedContext = await RunUserInformationReceivedEventAsync(ticket, message, user);
|
var userInformationReceivedContext = await RunUserInformationReceivedEventAsync(ticket, message, user);
|
||||||
if (userInformationReceivedContext.HandledResponse)
|
AuthenticateResult result;
|
||||||
|
if (CheckEventResult(userInformationReceivedContext, out result))
|
||||||
{
|
{
|
||||||
return userInformationReceivedContext.Ticket;
|
return result;
|
||||||
}
|
|
||||||
else if (userInformationReceivedContext.Skipped)
|
|
||||||
{
|
|
||||||
return ticket;
|
|
||||||
}
|
}
|
||||||
ticket = userInformationReceivedContext.Ticket;
|
ticket = userInformationReceivedContext.Ticket;
|
||||||
user = userInformationReceivedContext.User;
|
user = userInformationReceivedContext.User;
|
||||||
|
|
@ -742,7 +687,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, jwt.Issuer));
|
identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, jwt.Issuer));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ticket;
|
return AuthenticateResult.Success(ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -750,8 +695,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="principal">The principal in which tokens are saved.</param>
|
/// <param name="principal">The principal in which tokens are saved.</param>
|
||||||
/// <param name="message">The OpenID Connect response.</param>
|
/// <param name="message">The OpenID Connect response.</param>
|
||||||
/// <param name="saveRefreshToken">A <see cref="bool"/> indicating whether the refresh token should be stored.</param>
|
private void SaveTokens(ClaimsPrincipal principal, OpenIdConnectMessage message, string issuer)
|
||||||
private void SaveTokens(ClaimsPrincipal principal, OpenIdConnectMessage message, string issuer, bool saveRefreshToken)
|
|
||||||
{
|
{
|
||||||
var identity = (ClaimsIdentity)principal.Identity;
|
var identity = (ClaimsIdentity)principal.Identity;
|
||||||
|
|
||||||
|
|
@ -767,7 +711,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
ClaimValueTypes.String, issuer));
|
ClaimValueTypes.String, issuer));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveRefreshToken && !string.IsNullOrEmpty(message.RefreshToken))
|
if (!string.IsNullOrEmpty(message.RefreshToken))
|
||||||
{
|
{
|
||||||
identity.AddClaim(new Claim(OpenIdConnectParameterNames.RefreshToken, message.RefreshToken,
|
identity.AddClaim(new Claim(OpenIdConnectParameterNames.RefreshToken, message.RefreshToken,
|
||||||
ClaimValueTypes.String, issuer));
|
ClaimValueTypes.String, issuer));
|
||||||
|
|
@ -911,6 +855,25 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
return messageReceivedContext;
|
return messageReceivedContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<AuthorizationResponseReceivedContext> RunAuthorizationResponseReceivedEventAsync(OpenIdConnectMessage message, AuthenticationProperties properties)
|
||||||
|
{
|
||||||
|
Logger.LogTrace(15, "Authorization response received.");
|
||||||
|
var authorizationResponseReceivedContext = new AuthorizationResponseReceivedContext(Context, Options, properties)
|
||||||
|
{
|
||||||
|
ProtocolMessage = message
|
||||||
|
};
|
||||||
|
await Options.Events.AuthorizationResponseReceived(authorizationResponseReceivedContext);
|
||||||
|
if (authorizationResponseReceivedContext.HandledResponse)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(16, "AuthorizationResponseReceived.HandledResponse");
|
||||||
|
}
|
||||||
|
else if (authorizationResponseReceivedContext.Skipped)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(17, "AuthorizationResponseReceived.Skipped");
|
||||||
|
}
|
||||||
|
return authorizationResponseReceivedContext;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<AuthorizationCodeReceivedContext> RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage message, AuthenticationProperties properties, AuthenticationTicket ticket, JwtSecurityToken jwt)
|
private async Task<AuthorizationCodeReceivedContext> RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage message, AuthenticationProperties properties, AuthenticationTicket ticket, JwtSecurityToken jwt)
|
||||||
{
|
{
|
||||||
var redirectUri = properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey];
|
var redirectUri = properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey];
|
||||||
|
|
@ -960,13 +923,13 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
return tokenResponseReceivedContext;
|
return tokenResponseReceivedContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<AuthenticationValidatedContext> RunAuthenticationValidatedEventAsync(OpenIdConnectMessage message, AuthenticationTicket ticket, AuthenticationProperties properties, OpenIdConnectMessage tokenEndpointResponse)
|
private async Task<AuthenticationValidatedContext> RunAuthenticationValidatedEventAsync(OpenIdConnectMessage message, AuthenticationTicket ticket, AuthenticationProperties properties, OpenIdConnectMessage tokenResponse)
|
||||||
{
|
{
|
||||||
var authenticationValidatedContext = new AuthenticationValidatedContext(Context, Options, properties)
|
var authenticationValidatedContext = new AuthenticationValidatedContext(Context, Options, properties)
|
||||||
{
|
{
|
||||||
Ticket = ticket,
|
Ticket = ticket,
|
||||||
ProtocolMessage = message,
|
ProtocolMessage = message,
|
||||||
TokenEndpointResponse = tokenEndpointResponse,
|
TokenEndpointResponse = tokenResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
await Options.Events.AuthenticationValidated(authenticationValidatedContext);
|
await Options.Events.AuthenticationValidated(authenticationValidatedContext);
|
||||||
|
|
@ -1027,10 +990,13 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
return authenticationFailedContext;
|
return authenticationFailedContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthenticationTicket ValidateToken(string idToken, OpenIdConnectMessage message, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt)
|
private AuthenticationTicket ValidateToken(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt)
|
||||||
{
|
{
|
||||||
AuthenticationTicket ticket = null;
|
if (!Options.SecurityTokenValidator.CanReadToken(idToken))
|
||||||
jwt = null;
|
{
|
||||||
|
Logger.LogError(48, "Unable to read the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'.", idToken);
|
||||||
|
throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken));
|
||||||
|
}
|
||||||
|
|
||||||
if (_configuration != null)
|
if (_configuration != null)
|
||||||
{
|
{
|
||||||
|
|
@ -1047,16 +1013,12 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
}
|
}
|
||||||
|
|
||||||
SecurityToken validatedToken = null;
|
SecurityToken validatedToken = null;
|
||||||
ClaimsPrincipal principal = null;
|
var principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out validatedToken);
|
||||||
if (Options.SecurityTokenValidator.CanReadToken(idToken))
|
jwt = validatedToken as JwtSecurityToken;
|
||||||
|
if (jwt == null)
|
||||||
{
|
{
|
||||||
principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out validatedToken);
|
Logger.LogError(45, "The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'", validatedToken?.GetType());
|
||||||
jwt = validatedToken as JwtSecurityToken;
|
throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJwt, validatedToken?.GetType()));
|
||||||
if (jwt == null)
|
|
||||||
{
|
|
||||||
Logger.LogError(45, "The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'", validatedToken?.GetType());
|
|
||||||
throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJwt, validatedToken?.GetType()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validatedToken == null)
|
if (validatedToken == null)
|
||||||
|
|
@ -1065,16 +1027,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken));
|
throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme);
|
var 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Options.UseTokenLifetime)
|
if (Options.UseTokenLifetime)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue