Browse Source

Implement HandleLogoutRequest to handle logout requests without requiring user consent

pull/95/head
Kévin Chalet 10 years ago
parent
commit
4c4eb16d85
  1. 2
      samples/Mvc.Server/Startup.cs
  2. 2
      src/OpenIddict.Core/OpenIddictProvider.Authentication.cs
  3. 28
      src/OpenIddict.Core/OpenIddictProvider.Session.cs
  4. 8
      src/OpenIddict.Core/OpenIddictServices.cs
  5. 46
      src/OpenIddict.Mvc/OpenIddictController.cs
  6. 13
      src/OpenIddict.Mvc/Views/Shared/Logout.cshtml

2
samples/Mvc.Server/Startup.cs

@ -81,6 +81,8 @@ namespace Mvc.Server {
// Note: OpenIddict must be added after // Note: OpenIddict must be added after
// ASP.NET Identity and the external providers. // ASP.NET Identity and the external providers.
app.UseOpenIddict(builder => { app.UseOpenIddict(builder => {
builder.Options.AllowInsecureHttp = true;
// You can customize the default Content Security Policy (CSP) by calling UseNWebsec explicitly. // You can customize the default Content Security Policy (CSP) by calling UseNWebsec explicitly.
// This can be useful to allow your HTML views to reference remote scripts/images/styles. // This can be useful to allow your HTML views to reference remote scripts/images/styles.
builder.UseNWebsec(directives => { builder.UseNWebsec(directives => {

2
src/OpenIddict.Core/OpenIddictProvider.Authentication.cs

@ -117,7 +117,7 @@ namespace OpenIddict {
// Ensure the client application is listed as a valid audience in the identity token // Ensure the client application is listed as a valid audience in the identity token
// and that the identity token corresponds to the authenticated user. // and that the identity token corresponds to the authenticated user.
if (!principal.HasClaim(JwtRegisteredClaimNames.Aud, context.Request.ClientId) || if (!principal.HasClaim(OpenIdConnectConstants.Claims.Audience, context.Request.ClientId) ||
!principal.HasClaim(ClaimTypes.NameIdentifier, context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier))) { !principal.HasClaim(ClaimTypes.NameIdentifier, context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier))) {
context.Reject( context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest, error: OpenIdConnectConstants.Errors.InvalidRequest,

28
src/OpenIddict.Core/OpenIddictProvider.Session.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server; using AspNet.Security.OpenIdConnect.Server;
@ -34,5 +35,32 @@ namespace OpenIddict {
context.Validate(); context.Validate();
} }
public override async Task HandleLogoutRequest([NotNull] HandleLogoutRequestContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication>>();
// When the client application sends an id_token_hint parameter, the corresponding identity can be retrieved using
// AuthenticateAsync and used as a way to determine whether the logout request has been sent by a legit caller.
// If the token cannot be extracted, don't handle the logout request at this stage and continue to the next middleware.
var principal = await context.HttpContext.Authentication.AuthenticateAsync(context.Options.AuthenticationScheme);
if (principal == null) {
return;
}
// Ensure that the identity token corresponds to the authenticated user. If the token cannot be
// validated, don't handle the logout request at this stage and continue to the next middleware.
if (!principal.HasClaim(ClaimTypes.NameIdentifier, context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier))) {
return;
}
// Immediately sign out the user and redirect him
// to the post_logout_redirect_uri if provided.
await services.SignIn.SignOutAsync();
await context.HttpContext.Authentication.SignOutAsync(context.Options.AuthenticationScheme);
// Mark the response as handled
// to skip the rest of the pipeline.
context.HandleResponse();
}
} }
} }

8
src/OpenIddict.Core/OpenIddictServices.cs

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace OpenIddict { namespace OpenIddict {
/// <summary> /// <summary>
@ -41,6 +42,13 @@ namespace OpenIddict {
get { return Services.GetRequiredService<ILogger<OpenIddictManager<TUser, TApplication>>>(); } get { return Services.GetRequiredService<ILogger<OpenIddictManager<TUser, TApplication>>>(); }
} }
/// <summary>
/// Gets the <see cref="OpenIddictOptions"/>.
/// </summary>
public virtual OpenIddictOptions Options {
get { return Services.GetRequiredService<IOptions<OpenIddictOptions>>().Value; }
}
/// <summary> /// <summary>
/// Gets the <see cref="IServiceProvider"/> used to resolve services. /// Gets the <see cref="IServiceProvider"/> used to resolve services.
/// </summary> /// </summary>

46
src/OpenIddict.Mvc/OpenIddictController.cs

@ -8,7 +8,6 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Extensions;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -16,19 +15,14 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace OpenIddict.Mvc { namespace OpenIddict.Mvc {
// Note: this controller is generic and doesn't need to be marked as internal to prevent MVC from discovering it. // Note: this controller is generic and doesn't need to be marked as internal to prevent MVC from discovering it.
public class OpenIddictController<TUser, TApplication> : Controller where TUser : class where TApplication : class { public class OpenIddictController<TUser, TApplication> : Controller where TUser : class where TApplication : class {
public OpenIddictController( public OpenIddictController([NotNull] OpenIddictServices<TUser, TApplication> services) {
[NotNull] OpenIddictServices<TUser, TApplication> services,
[NotNull] IOptions<OpenIddictOptions> options) {
Services = services; Services = services;
Options = options.Value;
} }
/// <summary> /// <summary>
@ -36,11 +30,6 @@ namespace OpenIddict.Mvc {
/// </summary> /// </summary>
protected virtual OpenIddictServices<TUser, TApplication> Services { get; } protected virtual OpenIddictServices<TUser, TApplication> Services { get; }
/// <summary>
/// Gets the OpenIddict options used by the server.
/// </summary>
protected virtual OpenIddictOptions Options { get; }
[HttpGet, HttpPost] [HttpGet, HttpPost]
public virtual async Task<IActionResult> Authorize() { public virtual async Task<IActionResult> Authorize() {
// Note: when a fatal error occurs during the request processing, an OpenID Connect response // Note: when a fatal error occurs during the request processing, an OpenID Connect response
@ -127,7 +116,7 @@ namespace OpenIddict.Mvc {
var ticket = new AuthenticationTicket( var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity), new ClaimsPrincipal(identity),
new AuthenticationProperties(), new AuthenticationProperties(),
Options.AuthenticationScheme); Services.Options.AuthenticationScheme);
ticket.SetResources(request.GetResources()); ticket.SetResources(request.GetResources());
ticket.SetScopes(request.GetScopes()); ticket.SetScopes(request.GetScopes());
@ -139,59 +128,54 @@ namespace OpenIddict.Mvc {
} }
[Authorize, HttpPost, ValidateAntiForgeryToken] [Authorize, HttpPost, ValidateAntiForgeryToken]
public virtual IActionResult Deny() { public virtual Task<IActionResult> Deny() {
var response = HttpContext.GetOpenIdConnectResponse(); var response = HttpContext.GetOpenIdConnectResponse();
if (response != null) { if (response != null) {
return View("Error", response); return Task.FromResult<IActionResult>(View("Error", response));
} }
var request = HttpContext.GetOpenIdConnectRequest(); var request = HttpContext.GetOpenIdConnectRequest();
if (request == null) { if (request == null) {
return View("Error", new OpenIdConnectMessage { return Task.FromResult<IActionResult>(View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.ServerError, Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred" ErrorDescription = "An internal error has occurred"
}); }));
} }
// Notify ASOS that the authorization grant has been denied by the resource owner. // Notify ASOS that the authorization grant has been denied by the resource owner.
// Note: OpenIdConnectServerHandler will automatically take care of redirecting // Note: OpenIdConnectServerHandler will automatically take care of redirecting
// the user agent to the client application using the appropriate response_mode. // the user agent to the client application using the appropriate response_mode.
return Forbid(Options.AuthenticationScheme); return Task.FromResult<IActionResult>(Forbid(Services.Options.AuthenticationScheme));
} }
[HttpGet] [HttpGet]
public virtual async Task<IActionResult> Logout() { public virtual Task<IActionResult> Logout() {
var response = HttpContext.GetOpenIdConnectResponse(); var response = HttpContext.GetOpenIdConnectResponse();
if (response != null) { if (response != null) {
return View("Error", response); return Task.FromResult<IActionResult>(View("Error", response));
} }
// When invoked, the logout endpoint might receive an unauthenticated request if the server cookie has expired.
// When the client application sends an id_token_hint parameter, the corresponding identity can be retrieved
// using AuthenticateAsync or using User when the authorization server is declared as AuthenticationMode.Active.
var identity = await HttpContext.Authentication.AuthenticateAsync(Options.AuthenticationScheme);
var request = HttpContext.GetOpenIdConnectRequest(); var request = HttpContext.GetOpenIdConnectRequest();
if (request == null) { if (request == null) {
return View("Error", new OpenIdConnectMessage { return Task.FromResult<IActionResult>(View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.ServerError, Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred" ErrorDescription = "An internal error has occurred"
}); }));
} }
return View("Logout", Tuple.Create(request, identity)); return Task.FromResult<IActionResult>(View("Logout", request));
} }
[HttpPost, ValidateAntiForgeryToken] [HttpPost, ValidateAntiForgeryToken]
public virtual async Task<IActionResult> Logout([FromServices] SignInManager<TUser> manager) { public virtual async Task<IActionResult> Signout() {
// Instruct the cookies middleware to delete the local cookie created // Instruct the cookies middleware to delete the local cookie created
// when the user agent is redirected from the external identity provider // when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook). // after a successful authentication flow (e.g Google or Facebook).
await manager.SignOutAsync(); await Services.SignIn.SignOutAsync();
// Returning a SignOutResult will ask ASOS to redirect the user agent // Returning a SignOutResult will ask ASOS to redirect the user agent
// to the post_logout_redirect_uri specified by the client application. // to the post_logout_redirect_uri specified by the client application.
return SignOut(Options.AuthenticationScheme); return SignOut(Services.Options.AuthenticationScheme);
} }
} }
} }

13
src/OpenIddict.Mvc/Views/Shared/Logout.cshtml

@ -1,16 +1,15 @@
<div class="jumbotron"> @using Microsoft.IdentityModel.Protocols.OpenIdConnect
@model OpenIdConnectMessage
<div class="jumbotron">
<h1>Log out</h1> <h1>Log out</h1>
<p class="lead text-left">Are you sure you want to sign out?</p> <p class="lead text-left">Are you sure you want to sign out?</p>
@if (Model.Item2 != null && Model.Item2.Identity != null) { <form action="@Url.Action("Signout", "Authorization")" enctype="application/x-www-form-urlencoded" method="post">
<p class="lead text-left">Connected user: @Model.Item2.Identity.Name</p>
}
<form action="@Url.Action("Logout", "Authorization")" enctype="application/x-www-form-urlencoded" method="post">
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
@* Flow the logout request *@ @* Flow the logout request *@
@foreach (var parameter in Model.Item1.Parameters) { @foreach (var parameter in Model.Parameters) {
<input type="hidden" name="@parameter.Key" value="@parameter.Value" /> <input type="hidden" name="@parameter.Key" value="@parameter.Value" />
} }

Loading…
Cancel
Save