diff --git a/src/AspNet.Security.OpenIdConnect.Server/Events/ProcessChallengeResponseContext.cs b/src/AspNet.Security.OpenIdConnect.Server/Events/ProcessChallengeResponseContext.cs
new file mode 100644
index 00000000..8f0af6a6
--- /dev/null
+++ b/src/AspNet.Security.OpenIdConnect.Server/Events/ProcessChallengeResponseContext.cs
@@ -0,0 +1,51 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+using AspNet.Security.OpenIdConnect.Primitives;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+
+namespace AspNet.Security.OpenIdConnect.Server
+{
+ ///
+ /// Represents the context class associated with the
+ /// event.
+ ///
+ public class ProcessChallengeResponseContext : BaseControlContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ProcessChallengeResponseContext(
+ HttpContext context,
+ OpenIdConnectServerOptions options,
+ AuthenticationTicket ticket,
+ OpenIdConnectRequest request,
+ OpenIdConnectResponse response)
+ : base(context)
+ {
+ Options = options;
+ Ticket = ticket;
+ Request = request;
+ Response = response;
+ }
+
+ ///
+ /// Gets the options used by the OpenID Connect server.
+ ///
+ public OpenIdConnectServerOptions Options { get; }
+
+ ///
+ /// Gets the authorization or token request.
+ ///
+ public new OpenIdConnectRequest Request { get; }
+
+ ///
+ /// Gets the authorization or token response.
+ ///
+ public new OpenIdConnectResponse Response { get; }
+ }
+}
diff --git a/src/AspNet.Security.OpenIdConnect.Server/Events/ProcessSigninResponseContext.cs b/src/AspNet.Security.OpenIdConnect.Server/Events/ProcessSigninResponseContext.cs
new file mode 100644
index 00000000..460bb38a
--- /dev/null
+++ b/src/AspNet.Security.OpenIdConnect.Server/Events/ProcessSigninResponseContext.cs
@@ -0,0 +1,83 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+using AspNet.Security.OpenIdConnect.Primitives;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+
+namespace AspNet.Security.OpenIdConnect.Server
+{
+ ///
+ /// Represents the context class associated with the
+ /// event.
+ ///
+ public class ProcessSigninResponseContext : BaseControlContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ProcessSigninResponseContext(
+ HttpContext context,
+ OpenIdConnectServerOptions options,
+ AuthenticationTicket ticket,
+ OpenIdConnectRequest request,
+ OpenIdConnectResponse response)
+ : base(context)
+ {
+ Options = options;
+ Ticket = ticket;
+ Request = request;
+ Response = response;
+ }
+
+ ///
+ /// Gets the options used by the OpenID Connect server.
+ ///
+ public OpenIdConnectServerOptions Options { get; }
+
+ ///
+ /// Gets the authorization or token request.
+ ///
+ public new OpenIdConnectRequest Request { get; }
+
+ ///
+ /// Gets the authorization or token response.
+ ///
+ public new OpenIdConnectResponse Response { get; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an access token
+ /// should be returned to the client application.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool IncludeAccessToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an authorization code
+ /// should be returned to the client application.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool IncludeAuthorizationCode { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an identity token
+ /// should be returned to the client application.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool IncludeIdentityToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether a refresh token
+ /// should be returned to the client application.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool IncludeRefreshToken { get; set; }
+ }
+}
diff --git a/src/AspNet.Security.OpenIdConnect.Server/Events/ProcessSignoutResponseContext.cs b/src/AspNet.Security.OpenIdConnect.Server/Events/ProcessSignoutResponseContext.cs
new file mode 100644
index 00000000..c7d0af75
--- /dev/null
+++ b/src/AspNet.Security.OpenIdConnect.Server/Events/ProcessSignoutResponseContext.cs
@@ -0,0 +1,51 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+using AspNet.Security.OpenIdConnect.Primitives;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+
+namespace AspNet.Security.OpenIdConnect.Server
+{
+ ///
+ /// Represents the context class associated with the
+ /// event.
+ ///
+ public class ProcessSignoutResponseContext : BaseControlContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ProcessSignoutResponseContext(
+ HttpContext context,
+ OpenIdConnectServerOptions options,
+ AuthenticationTicket ticket,
+ OpenIdConnectRequest request,
+ OpenIdConnectResponse response)
+ : base(context)
+ {
+ Options = options;
+ Ticket = ticket;
+ Request = request;
+ Response = response;
+ }
+
+ ///
+ /// Gets the options used by the OpenID Connect server.
+ ///
+ public OpenIdConnectServerOptions Options { get; }
+
+ ///
+ /// Gets the logout request.
+ ///
+ public new OpenIdConnectRequest Request { get; }
+
+ ///
+ /// Gets the logout response.
+ ///
+ public new OpenIdConnectResponse Response { get; }
+ }
+}
diff --git a/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.cs b/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.cs
index 25812123..318b14ba 100644
--- a/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.cs
+++ b/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.cs
@@ -317,8 +317,89 @@ private async Task HandleSignInAsync(AuthenticationTicket ticket)
ticket.SetPresenters(presenter);
}
- // Only return an authorization code if the request is an authorization request and has response_type=code.
- if (request.IsAuthorizationRequest() && request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Code))
+ var notification = new ProcessSigninResponseContext(Context, Options, ticket, request, response);
+
+ if (request.IsAuthorizationRequest())
+ {
+ // By default, return an authorization code if a response type containing code was specified.
+ notification.IncludeAuthorizationCode = request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Code);
+
+ // By default, return an access token if a response type containing token was specified.
+ notification.IncludeAccessToken = request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token);
+
+ // By default, prevent a refresh token from being returned as the OAuth2 specification
+ // explicitly disallows returning a refresh token from the authorization endpoint.
+ // See https://tools.ietf.org/html/rfc6749#section-4.2.2 for more information.
+ notification.IncludeRefreshToken = false;
+
+ // By default, return an identity token if a response type containing code
+ // was specified and if the openid scope was explicitly or implicitly granted.
+ notification.IncludeIdentityToken =
+ request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken) &&
+ ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId);
+ }
+
+ else
+ {
+ // By default, prevent an authorization code from being returned as this type of token
+ // cannot be issued from the token endpoint in the standard OAuth2/OpenID Connect flows.
+ notification.IncludeAuthorizationCode = false;
+
+ // By default, always return an access token.
+ notification.IncludeAccessToken = true;
+
+ // By default, only return a refresh token is the offline_access scope was granted and if
+ // sliding expiration is disabled or if the request is not a grant_type=refresh_token request.
+ notification.IncludeRefreshToken =
+ ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
+ (Options.UseSlidingExpiration || !request.IsRefreshTokenGrantType());
+
+ // By default, only return an identity token if the openid scope was granted.
+ notification.IncludeIdentityToken = ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId);
+ }
+
+ await Options.Provider.ProcessSigninResponse(notification);
+
+ if (notification.HandledResponse)
+ {
+ Logger.LogDebug("The sign-in response was handled in user code.");
+
+ return true;
+ }
+
+ else if (notification.Skipped)
+ {
+ Logger.LogDebug("The default sign-in handling was skipped from user code.");
+
+ return false;
+ }
+
+ // Flow the changes made to the ticket.
+ ticket = notification.Ticket;
+
+ // Ensure an authentication ticket has been provided or return
+ // an error code indicating that the request was rejected.
+ if (ticket == null)
+ {
+ Logger.LogError("The request was rejected because no authentication ticket was provided.");
+
+ if (request.IsAuthorizationRequest())
+ {
+ return await SendAuthorizationResponseAsync(new OpenIdConnectResponse
+ {
+ Error = OpenIdConnectConstants.Errors.AccessDenied,
+ ErrorDescription = "The authorization was denied by the resource owner."
+ });
+ }
+
+ return await SendTokenResponseAsync(new OpenIdConnectResponse
+ {
+ Error = OpenIdConnectConstants.Errors.InvalidGrant,
+ ErrorDescription = "The token request was rejected by the authorization server."
+ });
+ }
+
+ if (notification.IncludeAuthorizationCode)
{
// Make sure to create a copy of the authentication properties
// to avoid modifying the properties set on the original ticket.
@@ -327,10 +408,7 @@ private async Task HandleSignInAsync(AuthenticationTicket ticket)
response.Code = await SerializeAuthorizationCodeAsync(ticket.Principal, properties, request, response);
}
- // Only return an access token if the request is a token request
- // or an authorization request that specifies response_type=token.
- if (request.IsTokenRequest() || (request.IsAuthorizationRequest() &&
- request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token)))
+ if (notification.IncludeAccessToken)
{
// Make sure to create a copy of the authentication properties
// to avoid modifying the properties set on the original ticket.
@@ -392,35 +470,22 @@ private async Task HandleSignInAsync(AuthenticationTicket ticket)
}
}
- // Only return a refresh token if the request is a token request that specifies scope=offline_access.
- if (request.IsTokenRequest() && ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess))
+ if (notification.IncludeRefreshToken)
{
- // Note: when sliding expiration is disabled, don't return a new refresh token,
- // unless the token request is not a grant_type=refresh_token request.
- if (Options.UseSlidingExpiration || !request.IsRefreshTokenGrantType())
- {
- // Make sure to create a copy of the authentication properties
- // to avoid modifying the properties set on the original ticket.
- var properties = ticket.Properties.Copy();
+ // Make sure to create a copy of the authentication properties
+ // to avoid modifying the properties set on the original ticket.
+ var properties = ticket.Properties.Copy();
- response.RefreshToken = await SerializeRefreshTokenAsync(ticket.Principal, properties, request, response);
- }
+ response.RefreshToken = await SerializeRefreshTokenAsync(ticket.Principal, properties, request, response);
}
- // Only return an identity token if the openid scope was requested and granted
- // to avoid generating and returning an unnecessary token to pure OAuth2 clients.
- if (ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId))
+ if (notification.IncludeIdentityToken)
{
- // Note: don't return an identity token if the request is an
- // authorization request that doesn't use response_type=id_token.
- if (request.IsTokenRequest() || request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken))
- {
- // Make sure to create a copy of the authentication properties
- // to avoid modifying the properties set on the original ticket.
- var properties = ticket.Properties.Copy();
+ // Make sure to create a copy of the authentication properties
+ // to avoid modifying the properties set on the original ticket.
+ var properties = ticket.Properties.Copy();
- response.IdToken = await SerializeIdentityTokenAsync(ticket.Principal, properties, request, response);
- }
+ response.IdToken = await SerializeIdentityTokenAsync(ticket.Principal, properties, request, response);
}
if (request.IsAuthorizationRequest())
@@ -432,6 +497,18 @@ private async Task HandleSignInAsync(AuthenticationTicket ticket)
}
protected override Task HandleSignOutAsync(SignOutContext context)
+ {
+ // Create a new ticket containing an empty identity and
+ // the authentication properties extracted from the context.
+ var ticket = new AuthenticationTicket(
+ new ClaimsPrincipal(new ClaimsIdentity()),
+ new AuthenticationProperties(context.Properties),
+ context.AuthenticationScheme);
+
+ return HandleSignOutAsync(ticket);
+ }
+
+ private async Task HandleSignOutAsync(AuthenticationTicket ticket)
{
// Extract the OpenID Connect request from the ASP.NET Core context.
// If it cannot be found or doesn't correspond to a logout request,
@@ -449,14 +526,47 @@ protected override Task HandleSignOutAsync(SignOutContext context)
throw new InvalidOperationException("A response has already been sent.");
}
- Logger.LogTrace("A log-out operation was triggered: {Properties}.", context.Properties);
+ Logger.LogTrace("A log-out operation was triggered: {Properties}.", ticket.Properties.Items);
+
+ // Prepare a new OpenID Connect response.
+ response = new OpenIdConnectResponse();
+
+ var notification = new ProcessSignoutResponseContext(Context, Options, ticket, request, response);
+ await Options.Provider.ProcessSignoutResponse(notification);
+
+ if (notification.HandledResponse)
+ {
+ Logger.LogDebug("The sign-out response was handled in user code.");
- return SendLogoutResponseAsync(new OpenIdConnectResponse());
+ return true;
+ }
+
+ else if (notification.Skipped)
+ {
+ Logger.LogDebug("The default sign-out handling was skipped from user code.");
+
+ return false;
+ }
+
+ return await SendLogoutResponseAsync(response);
}
- protected override Task HandleForbiddenAsync(ChallengeContext context) => HandleUnauthorizedAsync(context);
+ protected override Task HandleForbiddenAsync(ChallengeContext context)
+ => HandleUnauthorizedAsync(context);
+
+ protected override Task HandleUnauthorizedAsync(ChallengeContext context)
+ {
+ // Create a new ticket containing an empty identity and
+ // the authentication properties extracted from the context.
+ var ticket = new AuthenticationTicket(
+ new ClaimsPrincipal(new ClaimsIdentity()),
+ new AuthenticationProperties(context.Properties),
+ context.AuthenticationScheme);
+
+ return HandleUnauthorizedAsync(ticket);
+ }
- protected override async Task HandleUnauthorizedAsync(ChallengeContext context)
+ private async Task HandleUnauthorizedAsync(AuthenticationTicket ticket)
{
// Extract the OpenID Connect request from the ASP.NET Core context.
// If it cannot be found or doesn't correspond to an authorization
@@ -474,13 +584,6 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con
throw new InvalidOperationException("A response has already been sent.");
}
- // Create a new ticket containing an empty identity and
- // the authentication properties extracted from the challenge.
- var ticket = new AuthenticationTicket(
- new ClaimsPrincipal(new ClaimsIdentity()),
- new AuthenticationProperties(context.Properties),
- context.AuthenticationScheme);
-
// Prepare a new OpenID Connect response.
response = new OpenIdConnectResponse
{
@@ -508,7 +611,24 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con
"The token request was rejected by the authorization server.";
}
- Logger.LogTrace("A challenge operation was triggered: {Properties}.", context.Properties);
+ Logger.LogTrace("A challenge operation was triggered: {Properties}.", ticket.Properties.Items);
+
+ var notification = new ProcessChallengeResponseContext(Context, Options, ticket, request, response);
+ await Options.Provider.ProcessChallengeResponse(notification);
+
+ if (notification.HandledResponse)
+ {
+ Logger.LogDebug("The challenge response was handled in user code.");
+
+ return true;
+ }
+
+ else if (notification.Skipped)
+ {
+ Logger.LogDebug("The default challenge handling was skipped from user code.");
+
+ return false;
+ }
if (request.IsAuthorizationRequest())
{
diff --git a/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerProvider.cs b/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerProvider.cs
index 8084fddf..a8513f99 100644
--- a/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerProvider.cs
+++ b/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerProvider.cs
@@ -192,6 +192,24 @@ public class OpenIdConnectServerProvider
public Func OnHandleUserinfoRequest { get; set; }
= context => Task.FromResult(0);
+ ///
+ /// Represents an event called when processing a challenge response.
+ ///
+ public Func OnProcessChallengeResponse { get; set; }
+ = context => Task.FromResult(0);
+
+ ///
+ /// Represents an event called when processing a sign-in response.
+ ///
+ public Func OnProcessSigninResponse { get; set; }
+ = context => Task.FromResult(0);
+
+ ///
+ /// Represents an event called when processing a sign-out response.
+ ///
+ public Func OnProcessSignoutResponse { get; set; }
+ = context => Task.FromResult(0);
+
///
/// Represents an event called before the authorization response is returned to the caller.
///
@@ -513,6 +531,30 @@ public virtual Task HandleTokenRequest(HandleTokenRequestContext context)
public virtual Task HandleUserinfoRequest(HandleUserinfoRequestContext context)
=> OnHandleUserinfoRequest(context);
+ ///
+ /// Represents an event called when processing a challenge response.
+ ///
+ /// The context instance associated with this event.
+ /// A that can be used to monitor the asynchronous operation.
+ public virtual Task ProcessChallengeResponse(ProcessChallengeResponseContext context)
+ => OnProcessChallengeResponse(context);
+
+ ///
+ /// Represents an event called when processing a sign-in response.
+ ///
+ /// The context instance associated with this event.
+ /// A that can be used to monitor the asynchronous operation.
+ public virtual Task ProcessSigninResponse(ProcessSigninResponseContext context)
+ => OnProcessSigninResponse(context);
+
+ ///
+ /// Represents an event called when processing a sign-out response.
+ ///
+ /// The context instance associated with this event.
+ /// A that can be used to monitor the asynchronous operation.
+ public virtual Task ProcessSignoutResponse(ProcessSignoutResponseContext context)
+ => OnProcessSignoutResponse(context);
+
///
/// Represents an event called before the authorization response is returned to the caller.
///
diff --git a/src/Owin.Security.OpenIdConnect.Server/Events/ProcessChallengeResponseContext.cs b/src/Owin.Security.OpenIdConnect.Server/Events/ProcessChallengeResponseContext.cs
new file mode 100644
index 00000000..68cdea22
--- /dev/null
+++ b/src/Owin.Security.OpenIdConnect.Server/Events/ProcessChallengeResponseContext.cs
@@ -0,0 +1,51 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+using AspNet.Security.OpenIdConnect.Primitives;
+using Microsoft.Owin;
+using Microsoft.Owin.Security;
+using Microsoft.Owin.Security.Notifications;
+
+namespace Owin.Security.OpenIdConnect.Server
+{
+ ///
+ /// Represents the context class associated with the
+ /// event.
+ ///
+ public class ProcessChallengeResponseContext : BaseNotification
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ProcessChallengeResponseContext(
+ IOwinContext context,
+ OpenIdConnectServerOptions options,
+ AuthenticationTicket ticket,
+ OpenIdConnectRequest request,
+ OpenIdConnectResponse response)
+ : base(context, options)
+ {
+ Ticket = ticket;
+ Request = request;
+ Response = response;
+ }
+
+ ///
+ /// Gets the authorization or token request.
+ ///
+ public new OpenIdConnectRequest Request { get; }
+
+ ///
+ /// Gets the authorization or token response.
+ ///
+ public new OpenIdConnectResponse Response { get; }
+
+ ///
+ /// Gets the authentication ticket.
+ ///
+ public AuthenticationTicket Ticket { get; }
+ }
+}
diff --git a/src/Owin.Security.OpenIdConnect.Server/Events/ProcessSigninResponseContext.cs b/src/Owin.Security.OpenIdConnect.Server/Events/ProcessSigninResponseContext.cs
new file mode 100644
index 00000000..85695698
--- /dev/null
+++ b/src/Owin.Security.OpenIdConnect.Server/Events/ProcessSigninResponseContext.cs
@@ -0,0 +1,83 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+using AspNet.Security.OpenIdConnect.Primitives;
+using Microsoft.Owin;
+using Microsoft.Owin.Security;
+using Microsoft.Owin.Security.Notifications;
+
+namespace Owin.Security.OpenIdConnect.Server
+{
+ ///
+ /// Represents the context class associated with the
+ /// event.
+ ///
+ public class ProcessSigninResponseContext : BaseNotification
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ProcessSigninResponseContext(
+ IOwinContext context,
+ OpenIdConnectServerOptions options,
+ AuthenticationTicket ticket,
+ OpenIdConnectRequest request,
+ OpenIdConnectResponse response)
+ : base(context, options)
+ {
+ Ticket = ticket;
+ Request = request;
+ Response = response;
+ }
+
+ ///
+ /// Gets the authorization or token request.
+ ///
+ public new OpenIdConnectRequest Request { get; }
+
+ ///
+ /// Gets the authorization or token response.
+ ///
+ public new OpenIdConnectResponse Response { get; }
+
+ ///
+ /// Gets the authentication ticket.
+ ///
+ public AuthenticationTicket Ticket { get; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an access token
+ /// should be returned to the client application.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool IncludeAccessToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an authorization code
+ /// should be returned to the client application.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool IncludeAuthorizationCode { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an identity token
+ /// should be returned to the client application.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool IncludeIdentityToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether a refresh token
+ /// should be returned to the client application.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool IncludeRefreshToken { get; set; }
+ }
+}
diff --git a/src/Owin.Security.OpenIdConnect.Server/Events/ProcessSignoutResponseContext.cs b/src/Owin.Security.OpenIdConnect.Server/Events/ProcessSignoutResponseContext.cs
new file mode 100644
index 00000000..f789fb1c
--- /dev/null
+++ b/src/Owin.Security.OpenIdConnect.Server/Events/ProcessSignoutResponseContext.cs
@@ -0,0 +1,51 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server
+ * for more information concerning the license and the contributors participating to this project.
+ */
+
+using AspNet.Security.OpenIdConnect.Primitives;
+using Microsoft.Owin;
+using Microsoft.Owin.Security;
+using Microsoft.Owin.Security.Notifications;
+
+namespace Owin.Security.OpenIdConnect.Server
+{
+ ///
+ /// Represents the context class associated with the
+ /// event.
+ ///
+ public class ProcessSignoutResponseContext : BaseNotification
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ProcessSignoutResponseContext(
+ IOwinContext context,
+ OpenIdConnectServerOptions options,
+ AuthenticationTicket ticket,
+ OpenIdConnectRequest request,
+ OpenIdConnectResponse response)
+ : base(context, options)
+ {
+ Ticket = ticket;
+ Request = request;
+ Response = response;
+ }
+
+ ///
+ /// Gets the logout request.
+ ///
+ public new OpenIdConnectRequest Request { get; }
+
+ ///
+ /// Gets the logout response.
+ ///
+ public new OpenIdConnectResponse Response { get; }
+
+ ///
+ /// Gets the authentication ticket.
+ ///
+ public AuthenticationTicket Ticket { get; }
+ }
+}
diff --git a/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.cs b/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.cs
index 056cbf32..cfab596f 100644
--- a/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.cs
+++ b/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.cs
@@ -348,8 +348,89 @@ private async Task HandleSignInAsync(AuthenticationTicket ticket)
ticket.SetPresenters(presenter);
}
- // Only return an authorization code if the request is an authorization request and has response_type=code.
- if (request.IsAuthorizationRequest() && request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Code))
+ var notification = new ProcessSigninResponseContext(Context, Options, ticket, request, response);
+
+ if (request.IsAuthorizationRequest())
+ {
+ // By default, return an authorization code if a response type containing code was specified.
+ notification.IncludeAuthorizationCode = request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Code);
+
+ // By default, return an access token if a response type containing token was specified.
+ notification.IncludeAccessToken = request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token);
+
+ // By default, prevent a refresh token from being returned as the OAuth2 specification
+ // explicitly disallows returning a refresh token from the authorization endpoint.
+ // See https://tools.ietf.org/html/rfc6749#section-4.2.2 for more information.
+ notification.IncludeRefreshToken = false;
+
+ // By default, return an identity token if a response type containing code
+ // was specified and if the openid scope was explicitly or implicitly granted.
+ notification.IncludeIdentityToken =
+ request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken) &&
+ ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId);
+ }
+
+ else
+ {
+ // By default, prevent an authorization code from being returned as this type of token
+ // cannot be issued from the token endpoint in the standard OAuth2/OpenID Connect flows.
+ notification.IncludeAuthorizationCode = false;
+
+ // By default, always return an access token.
+ notification.IncludeAccessToken = true;
+
+ // By default, only return a refresh token is the offline_access scope was granted and if
+ // sliding expiration is disabled or if the request is not a grant_type=refresh_token request.
+ notification.IncludeRefreshToken =
+ ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
+ (Options.UseSlidingExpiration || !request.IsRefreshTokenGrantType());
+
+ // By default, only return an identity token if the openid scope was granted.
+ notification.IncludeIdentityToken = ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId);
+ }
+
+ await Options.Provider.ProcessSigninResponse(notification);
+
+ if (notification.HandledResponse)
+ {
+ Logger.LogDebug("The sign-in response was handled in user code.");
+
+ return true;
+ }
+
+ else if (notification.Skipped)
+ {
+ Logger.LogDebug("The default sign-in handling was skipped from user code.");
+
+ return false;
+ }
+
+ // Flow the changes made to the ticket.
+ ticket = notification.Ticket;
+
+ // Ensure an authentication ticket has been provided or return
+ // an error code indicating that the request was rejected.
+ if (ticket == null)
+ {
+ Logger.LogError("The request was rejected because no authentication ticket was provided.");
+
+ if (request.IsAuthorizationRequest())
+ {
+ return await SendAuthorizationResponseAsync(new OpenIdConnectResponse
+ {
+ Error = OpenIdConnectConstants.Errors.AccessDenied,
+ ErrorDescription = "The authorization was denied by the resource owner."
+ });
+ }
+
+ return await SendTokenResponseAsync(new OpenIdConnectResponse
+ {
+ Error = OpenIdConnectConstants.Errors.InvalidGrant,
+ ErrorDescription = "The token request was rejected by the authorization server."
+ });
+ }
+
+ if (notification.IncludeAuthorizationCode)
{
// Make sure to create a copy of the authentication properties
// to avoid modifying the properties set on the original ticket.
@@ -358,10 +439,7 @@ private async Task HandleSignInAsync(AuthenticationTicket ticket)
response.Code = await SerializeAuthorizationCodeAsync(ticket.Identity, properties, request, response);
}
- // Only return an access token if the request is a token request
- // or an authorization request that specifies response_type=token.
- if (request.IsTokenRequest() || (request.IsAuthorizationRequest() &&
- request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token)))
+ if (notification.IncludeAccessToken)
{
// Make sure to create a copy of the authentication properties
// to avoid modifying the properties set on the original ticket.
@@ -423,35 +501,22 @@ private async Task HandleSignInAsync(AuthenticationTicket ticket)
}
}
- // Only return a refresh token if the request is a token request that specifies scope=offline_access.
- if (request.IsTokenRequest() && ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess))
+ if (notification.IncludeRefreshToken)
{
- // Note: when sliding expiration is disabled, don't return a new refresh token,
- // unless the token request is not a grant_type=refresh_token request.
- if (Options.UseSlidingExpiration || !request.IsRefreshTokenGrantType())
- {
- // Make sure to create a copy of the authentication properties
- // to avoid modifying the properties set on the original ticket.
- var properties = ticket.Properties.Copy();
+ // Make sure to create a copy of the authentication properties
+ // to avoid modifying the properties set on the original ticket.
+ var properties = ticket.Properties.Copy();
- response.RefreshToken = await SerializeRefreshTokenAsync(ticket.Identity, properties, request, response);
- }
+ response.RefreshToken = await SerializeRefreshTokenAsync(ticket.Identity, properties, request, response);
}
- // Only return an identity token if the openid scope was requested and granted
- // to avoid generating and returning an unnecessary token to pure OAuth2 clients.
- if (ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId))
+ if (notification.IncludeIdentityToken)
{
- // Note: don't return an identity token if the request is an
- // authorization request that doesn't use response_type=id_token.
- if (request.IsTokenRequest() || request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken))
- {
- // Make sure to create a copy of the authentication properties
- // to avoid modifying the properties set on the original ticket.
- var properties = ticket.Properties.Copy();
+ // Make sure to create a copy of the authentication properties
+ // to avoid modifying the properties set on the original ticket.
+ var properties = ticket.Properties.Copy();
- response.IdToken = await SerializeIdentityTokenAsync(ticket.Identity, properties, request, response);
- }
+ response.IdToken = await SerializeIdentityTokenAsync(ticket.Identity, properties, request, response);
}
if (request.IsAuthorizationRequest())
@@ -462,7 +527,16 @@ private async Task HandleSignInAsync(AuthenticationTicket ticket)
return await SendTokenResponseAsync(response, ticket);
}
- private async Task HandleLogoutAsync(AuthenticationResponseRevoke context)
+ private Task HandleLogoutAsync(AuthenticationResponseRevoke context)
+ {
+ // Create a new ticket containing an empty identity and
+ // the authentication properties extracted from the challenge.
+ var ticket = new AuthenticationTicket(new ClaimsIdentity(), context.Properties);
+
+ return HandleLogoutAsync(ticket);
+ }
+
+ private async Task HandleLogoutAsync(AuthenticationTicket ticket)
{
// Extract the OpenID Connect request from the OWIN/Katana context.
// If it cannot be found or doesn't correspond to a logout request,
@@ -480,12 +554,41 @@ private async Task HandleLogoutAsync(AuthenticationResponseRevoke context)
throw new InvalidOperationException("A response has already been sent.");
}
- Logger.LogTrace("A log-out operation was triggered: {Properties}.", context.Properties.Dictionary);
+ Logger.LogTrace("A log-out operation was triggered: {Properties}.", ticket.Properties.Dictionary);
+
+ // Prepare a new OpenID Connect response.
+ response = new OpenIdConnectResponse();
+
+ var notification = new ProcessSignoutResponseContext(Context, Options, ticket, request, response);
+ await Options.Provider.ProcessSignoutResponse(notification);
+
+ if (notification.HandledResponse)
+ {
+ Logger.LogDebug("The sign-out response was handled in user code.");
+
+ return true;
+ }
+
+ else if (notification.Skipped)
+ {
+ Logger.LogDebug("The default sign-out handling was skipped from user code.");
+
+ return false;
+ }
- return await SendLogoutResponseAsync(new OpenIdConnectResponse());
+ return await SendLogoutResponseAsync(response);
}
- private async Task HandleChallengeAsync(AuthenticationResponseChallenge context)
+ private Task HandleChallengeAsync(AuthenticationResponseChallenge context)
+ {
+ // Create a new ticket containing an empty identity and
+ // the authentication properties extracted from the challenge.
+ var ticket = new AuthenticationTicket(new ClaimsIdentity(), context.Properties);
+
+ return HandleChallengeAsync(ticket);
+ }
+
+ private async Task HandleChallengeAsync(AuthenticationTicket ticket)
{
// Extract the OpenID Connect request from the OWIN/Katana context.
// If it cannot be found or doesn't correspond to an authorization
@@ -503,10 +606,6 @@ private async Task HandleChallengeAsync(AuthenticationResponseChallenge co
throw new InvalidOperationException("A response has already been sent.");
}
- // Create a new ticket containing an empty identity and
- // the authentication properties extracted from the challenge.
- var ticket = new AuthenticationTicket(new ClaimsIdentity(), context.Properties);
-
// Prepare a new OpenID Connect response.
response = new OpenIdConnectResponse
{
@@ -534,7 +633,24 @@ private async Task HandleChallengeAsync(AuthenticationResponseChallenge co
"The token request was rejected by the authorization server.";
}
- Logger.LogTrace("A challenge operation was triggered: {Properties}.", context.Properties.Dictionary);
+ Logger.LogTrace("A challenge operation was triggered: {Properties}.", ticket.Properties.Dictionary);
+
+ var notification = new ProcessChallengeResponseContext(Context, Options, ticket, request, response);
+ await Options.Provider.ProcessChallengeResponse(notification);
+
+ if (notification.HandledResponse)
+ {
+ Logger.LogDebug("The challenge response was handled in user code.");
+
+ return true;
+ }
+
+ else if (notification.Skipped)
+ {
+ Logger.LogDebug("The default challenge handling was skipped from user code.");
+
+ return false;
+ }
if (request.IsAuthorizationRequest())
{
diff --git a/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerProvider.cs b/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerProvider.cs
index cce201e8..d6756990 100644
--- a/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerProvider.cs
+++ b/src/Owin.Security.OpenIdConnect.Server/OpenIdConnectServerProvider.cs
@@ -192,6 +192,24 @@ public class OpenIdConnectServerProvider
public Func OnHandleUserinfoRequest { get; set; }
= context => Task.FromResult(0);
+ ///
+ /// Represents an event called when processing a challenge response.
+ ///
+ public Func OnProcessChallengeResponse { get; set; }
+ = context => Task.FromResult(0);
+
+ ///
+ /// Represents an event called when processing a sign-in response.
+ ///
+ public Func OnProcessSigninResponse { get; set; }
+ = context => Task.FromResult(0);
+
+ ///
+ /// Represents an event called when processing a sign-out response.
+ ///
+ public Func OnProcessSignoutResponse { get; set; }
+ = context => Task.FromResult(0);
+
///
/// Represents an event called before the authorization response is returned to the caller.
///
@@ -513,6 +531,30 @@ public virtual Task HandleTokenRequest(HandleTokenRequestContext context)
public virtual Task HandleUserinfoRequest(HandleUserinfoRequestContext context)
=> OnHandleUserinfoRequest(context);
+ ///
+ /// Represents an event called when processing a challenge response.
+ ///
+ /// The context instance associated with this event.
+ /// A that can be used to monitor the asynchronous operation.
+ public virtual Task ProcessChallengeResponse(ProcessChallengeResponseContext context)
+ => OnProcessChallengeResponse(context);
+
+ ///
+ /// Represents an event called when processing a sign-in response.
+ ///
+ /// The context instance associated with this event.
+ /// A that can be used to monitor the asynchronous operation.
+ public virtual Task ProcessSigninResponse(ProcessSigninResponseContext context)
+ => OnProcessSigninResponse(context);
+
+ ///
+ /// Represents an event called when processing a sign-out response.
+ ///
+ /// The context instance associated with this event.
+ /// A that can be used to monitor the asynchronous operation.
+ public virtual Task ProcessSignoutResponse(ProcessSignoutResponseContext context)
+ => OnProcessSignoutResponse(context);
+
///
/// Represents an event called before the authorization response is returned to the caller.
///
diff --git a/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.cs b/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.cs
index 6c82969a..95e98041 100644
--- a/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.cs
+++ b/test/AspNet.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.cs
@@ -1040,6 +1040,107 @@ public async Task HandleSignInAsync_ResourcesAreInferredFromAudiences()
Assert.NotNull(response.RefreshToken);
}
+ [Fact]
+ public async Task HandleSignInAsync_ProcessSigninResponse_AllowsOverridingDefaultTokensSelection()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(options =>
+ {
+ options.Provider.OnValidateTokenRequest = context =>
+ {
+ context.Skip();
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnHandleTokenRequest = context =>
+ {
+ var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
+ identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Magnifique");
+
+ context.Validate(new ClaimsPrincipal(identity));
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ context.IncludeAccessToken = false;
+ context.IncludeAuthorizationCode = true;
+ context.IncludeIdentityToken = true;
+ context.IncludeRefreshToken = true;
+
+ return Task.FromResult(0);
+ };
+ });
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act
+ var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
+ {
+ GrantType = OpenIdConnectConstants.GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ });
+
+ // Assert
+ Assert.Null(response.AccessToken);
+ Assert.NotNull(response.Code);
+ Assert.NotNull(response.IdToken);
+ Assert.NotNull(response.RefreshToken);
+ }
+
+ [Fact]
+ public async Task HandleSignInAsync_ProcessSigninResponse_AllowsHandlingResponse()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(options =>
+ {
+ options.Provider.OnValidateTokenRequest = context =>
+ {
+ context.Skip();
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnHandleTokenRequest = context =>
+ {
+ var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
+ identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Magnifique");
+
+ context.Validate(new ClaimsPrincipal(identity));
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ context.HandleResponse();
+
+ context.HttpContext.Response.Headers[HeaderNames.ContentType] = "application/json";
+
+ return context.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(new
+ {
+ name = "Bob le Magnifique"
+ }));
+ };
+ });
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act
+ var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
+ {
+ GrantType = OpenIdConnectConstants.GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ });
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string) response["name"]);
+ }
+
[Theory]
[InlineData("code")]
[InlineData("code id_token")]
@@ -1066,6 +1167,13 @@ public async Task HandleSignInAsync_AnAuthorizationCodeIsReturnedForCodeAndHybri
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAuthorizationCode);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1311,6 +1419,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForImplicitAndHybridF
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1358,6 +1473,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForCodeGrantRequests(
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1401,6 +1523,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForRefreshTokenGrantR
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1438,6 +1567,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForPasswordGrantReque
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1476,6 +1612,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForClientCredentialsG
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1514,6 +1657,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForCustomGrantRequest
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1588,6 +1738,13 @@ public async Task HandleSignInAsync_NoRefreshTokenIsReturnedWhenOfflineAccessSco
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.False(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1634,6 +1791,13 @@ public async Task HandleSignInAsync_ARefreshTokenIsReturnedForCodeGrantRequests(
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1679,6 +1843,13 @@ public async Task HandleSignInAsync_ARefreshTokenIsReturnedForRefreshTokenGrantR
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1725,6 +1896,13 @@ public async Task HandleSignInAsync_NoRefreshTokenIsReturnedWhenSlidingExpiratio
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.False(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1769,6 +1947,13 @@ public async Task HandleSignInAsync_ARefreshTokenIsReturnedForPasswordGrantReque
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1814,6 +1999,13 @@ public async Task HandleSignInAsync_ARefreshTokenIsReturnedForClientCredentialsG
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1859,6 +2051,13 @@ public async Task HandleSignInAsync_ARefreshTokenIsReturnedForCustomGrantRequest
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1895,6 +2094,13 @@ public async Task HandleSignInAsync_NoIdentityTokenIsReturnedWhenOfflineAccessSc
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.False(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1937,6 +2143,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForImplicitAndHybri
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -1985,6 +2198,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForCodeGrantRequest
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -2030,6 +2250,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForRefreshTokenGran
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -2067,6 +2294,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForPasswordGrantReq
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -2106,6 +2340,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForClientCredential
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -2145,6 +2386,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForCustomGrantReque
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.CreateClient());
@@ -2216,6 +2464,49 @@ await context.HttpContext.Authentication.SignOutAsync(
Assert.Equal("A response has already been sent.", exception.Message);
}
+ [Fact]
+ public async Task HandleSignOutAsync_ProcessSignoutResponse_AllowsHandlingResponse()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(options =>
+ {
+ options.Provider.OnValidateLogoutRequest = context =>
+ {
+ context.Validate();
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnHandleLogoutRequest = context =>
+ {
+ context.HandleResponse();
+
+ return context.HttpContext.Authentication.SignOutAsync(
+ OpenIdConnectServerDefaults.AuthenticationScheme);
+ };
+
+ options.Provider.OnProcessSignoutResponse = context =>
+ {
+ context.HandleResponse();
+
+ context.HttpContext.Response.Headers[HeaderNames.ContentType] = "application/json";
+
+ return context.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(new
+ {
+ name = "Bob le Magnifique"
+ }));
+ };
+ });
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act
+ var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest());
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string) response["name"]);
+ }
+
[Fact]
public async Task HandleUnauthorizedAsync_InvalidEndpointCausesAnException()
{
@@ -2404,6 +2695,51 @@ public async Task HandleUnauthorizedAsync_ReturnsDefaultErrorForTokenRequestsWhe
Assert.Null(response.ErrorUri);
}
+ [Fact]
+ public async Task HandleUnauthorizedAsync_ProcessChallengeResponse_AllowsHandlingResponse()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(options =>
+ {
+ options.Provider.OnValidateTokenRequest = context =>
+ {
+ context.Skip();
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnHandleTokenRequest = context =>
+ {
+ return context.HttpContext.Authentication.ChallengeAsync(context.Options.AuthenticationScheme);
+ };
+
+ options.Provider.OnProcessChallengeResponse = context =>
+ {
+ context.HandleResponse();
+
+ context.HttpContext.Response.Headers[HeaderNames.ContentType] = "application/json";
+
+ return context.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(new
+ {
+ name = "Bob le Magnifique"
+ }));
+ };
+ });
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act
+ var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
+ {
+ GrantType = OpenIdConnectConstants.GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ });
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string) response["name"]);
+ }
+
private static TestServer CreateAuthorizationServer(Action configuration = null)
{
var builder = new WebHostBuilder();
diff --git a/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.cs b/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.cs
index d2468d1d..1226cada 100644
--- a/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.cs
+++ b/test/Owin.Security.OpenIdConnect.Server.Tests/OpenIdConnectServerHandlerTests.cs
@@ -1014,6 +1014,107 @@ public async Task HandleSignInAsync_ResourcesAreInferredFromAudiences()
Assert.NotNull(response.RefreshToken);
}
+ [Fact]
+ public async Task HandleSignInAsync_ProcessSigninResponse_AllowsOverridingDefaultTokensSelection()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(options =>
+ {
+ options.Provider.OnValidateTokenRequest = context =>
+ {
+ context.Skip();
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnHandleTokenRequest = context =>
+ {
+ var identity = new ClaimsIdentity(context.Options.AuthenticationType);
+ identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Magnifique");
+
+ context.Validate(identity);
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ context.IncludeAccessToken = false;
+ context.IncludeAuthorizationCode = true;
+ context.IncludeIdentityToken = true;
+ context.IncludeRefreshToken = true;
+
+ return Task.FromResult(0);
+ };
+ });
+
+ var client = new OpenIdConnectClient(server.HttpClient);
+
+ // Act
+ var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
+ {
+ GrantType = OpenIdConnectConstants.GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ });
+
+ // Assert
+ Assert.Null(response.AccessToken);
+ Assert.NotNull(response.Code);
+ Assert.NotNull(response.IdToken);
+ Assert.NotNull(response.RefreshToken);
+ }
+
+ [Fact]
+ public async Task HandleSignInAsync_ProcessSigninResponse_AllowsHandlingResponse()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(options =>
+ {
+ options.Provider.OnValidateTokenRequest = context =>
+ {
+ context.Skip();
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnHandleTokenRequest = context =>
+ {
+ var identity = new ClaimsIdentity(context.Options.AuthenticationType);
+ identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Magnifique");
+
+ context.Validate(identity);
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ context.HandleResponse();
+
+ context.OwinContext.Response.Headers["Content-Type"] = "application/json";
+
+ return context.OwinContext.Response.WriteAsync(JsonConvert.SerializeObject(new
+ {
+ name = "Bob le Magnifique"
+ }));
+ };
+ });
+
+ var client = new OpenIdConnectClient(server.HttpClient);
+
+ // Act
+ var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
+ {
+ GrantType = OpenIdConnectConstants.GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ });
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string) response["name"]);
+ }
+
[Theory]
[InlineData("code")]
[InlineData("code id_token")]
@@ -1040,6 +1141,13 @@ public async Task HandleSignInAsync_AnAuthorizationCodeIsReturnedForCodeAndHybri
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAuthorizationCode);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1270,6 +1378,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForImplicitAndHybridF
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1313,6 +1428,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForCodeGrantRequests(
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1353,6 +1475,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForRefreshTokenGrantR
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1390,6 +1519,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForPasswordGrantReque
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1428,6 +1564,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForClientCredentialsG
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1466,6 +1609,13 @@ public async Task HandleSignInAsync_AnAccessTokenIsReturnedForCustomGrantRequest
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeAccessToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1540,6 +1690,13 @@ public async Task HandleSignInAsync_NoRefreshTokenIsReturnedWhenOfflineAccessSco
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.False(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1582,6 +1739,13 @@ public async Task HandleSignInAsync_ARefreshTokenIsReturnedForCodeGrantRequests(
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1623,6 +1787,13 @@ public async Task HandleSignInAsync_ARefreshTokenIsReturnedForRefreshTokenGrantR
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1665,6 +1836,13 @@ public async Task HandleSignInAsync_NoRefreshTokenIsReturnedWhenSlidingExpiratio
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.False(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1705,6 +1883,13 @@ public async Task HandleSignInAsync_ARefreshTokenIsReturnedForPasswordGrantReque
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1746,6 +1931,13 @@ public async Task HandleSignInAsync_ARefreshTokenIsReturnedForClientCredentialsG
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1787,6 +1979,13 @@ public async Task HandleSignInAsync_ARefreshTokenIsReturnedForCustomGrantRequest
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeRefreshToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1823,6 +2022,13 @@ public async Task HandleSignInAsync_NoIdentityTokenIsReturnedWhenOfflineAccessSc
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.False(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1865,6 +2071,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForImplicitAndHybri
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1909,6 +2122,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForCodeGrantRequest
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1950,6 +2170,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForRefreshTokenGran
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -1987,6 +2214,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForPasswordGrantReq
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -2026,6 +2260,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForClientCredential
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -2065,6 +2306,13 @@ public async Task HandleSignInAsync_AnIdentityTokenIsReturnedForCustomGrantReque
return Task.FromResult(0);
};
+
+ options.Provider.OnProcessSigninResponse = context =>
+ {
+ Assert.True(context.IncludeIdentityToken);
+
+ return Task.FromResult(0);
+ };
});
var client = new OpenIdConnectClient(server.HttpClient);
@@ -2107,6 +2355,50 @@ public async Task HandleSignOutAsync_InvalidEndpointCausesAnException()
Assert.Equal("A logout response cannot be returned from this endpoint.", exception.Message);
}
+ [Fact]
+ public async Task HandleSignOutAsync_ProcessSignoutResponse_AllowsHandlingResponse()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(options =>
+ {
+ options.Provider.OnValidateLogoutRequest = context =>
+ {
+ context.Validate();
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnHandleLogoutRequest = context =>
+ {
+ context.OwinContext.Authentication.SignOut(
+ OpenIdConnectServerDefaults.AuthenticationType);
+ context.HandleResponse();
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnProcessSignoutResponse = context =>
+ {
+ context.HandleResponse();
+
+ context.OwinContext.Response.Headers["Content-Type"] = "application/json";
+
+ return context.OwinContext.Response.WriteAsync(JsonConvert.SerializeObject(new
+ {
+ name = "Bob le Magnifique"
+ }));
+ };
+ });
+
+ var client = new OpenIdConnectClient(server.HttpClient);
+
+ // Act
+ var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest());
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string) response["name"]);
+ }
+
[Fact]
public async Task HandleChallengeAsync_InvalidEndpointCausesAnException()
{
@@ -2257,6 +2549,54 @@ public async Task HandleChallengeAsync_ReturnsDefaultErrorForTokenRequestsWhenNo
Assert.Null(response.ErrorUri);
}
+ [Fact]
+ public async Task HandleChallengeAsync_ProcessChallengeResponse_AllowsHandlingResponse()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(options =>
+ {
+ options.Provider.OnValidateTokenRequest = context =>
+ {
+ context.Skip();
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnHandleTokenRequest = context =>
+ {
+ context.OwinContext.Authentication.Challenge(context.Options.AuthenticationType);
+ context.HandleResponse();
+
+ return Task.FromResult(0);
+ };
+
+ options.Provider.OnProcessChallengeResponse = context =>
+ {
+ context.HandleResponse();
+
+ context.OwinContext.Response.Headers["Content-Type"] = "application/json";
+
+ return context.OwinContext.Response.WriteAsync(JsonConvert.SerializeObject(new
+ {
+ name = "Bob le Magnifique"
+ }));
+ };
+ });
+
+ var client = new OpenIdConnectClient(server.HttpClient);
+
+ // Act
+ var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
+ {
+ GrantType = OpenIdConnectConstants.GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ });
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string) response["name"]);
+ }
+
private static TestServer CreateAuthorizationServer(Action configuration = null)
{
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();