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();