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
// ASP.NET Identity and the external providers.
app.UseOpenIddict(builder => {
builder.Options.AllowInsecureHttp = true;
// 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.
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
// 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))) {
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,

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

@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server;
@ -34,5 +35,32 @@ namespace OpenIddict {
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.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace OpenIddict {
/// <summary>
@ -41,6 +42,13 @@ namespace OpenIddict {
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>
/// Gets the <see cref="IServiceProvider"/> used to resolve services.
/// </summary>

46
src/OpenIddict.Mvc/OpenIddictController.cs

@ -8,7 +8,6 @@ using System;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using JetBrains.Annotations;
@ -16,19 +15,14 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace OpenIddict.Mvc {
// 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 OpenIddictController(
[NotNull] OpenIddictServices<TUser, TApplication> services,
[NotNull] IOptions<OpenIddictOptions> options) {
public OpenIddictController([NotNull] OpenIddictServices<TUser, TApplication> services) {
Services = services;
Options = options.Value;
}
/// <summary>
@ -36,11 +30,6 @@ namespace OpenIddict.Mvc {
/// </summary>
protected virtual OpenIddictServices<TUser, TApplication> Services { get; }
/// <summary>
/// Gets the OpenIddict options used by the server.
/// </summary>
protected virtual OpenIddictOptions Options { get; }
[HttpGet, HttpPost]
public virtual async Task<IActionResult> Authorize() {
// 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(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
Options.AuthenticationScheme);
Services.Options.AuthenticationScheme);
ticket.SetResources(request.GetResources());
ticket.SetScopes(request.GetScopes());
@ -139,59 +128,54 @@ namespace OpenIddict.Mvc {
}
[Authorize, HttpPost, ValidateAntiForgeryToken]
public virtual IActionResult Deny() {
public virtual Task<IActionResult> Deny() {
var response = HttpContext.GetOpenIdConnectResponse();
if (response != null) {
return View("Error", response);
return Task.FromResult<IActionResult>(View("Error", response));
}
var request = HttpContext.GetOpenIdConnectRequest();
if (request == null) {
return View("Error", new OpenIdConnectMessage {
return Task.FromResult<IActionResult>(View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}));
}
// Notify ASOS that the authorization grant has been denied by the resource owner.
// Note: OpenIdConnectServerHandler will automatically take care of redirecting
// 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]
public virtual async Task<IActionResult> Logout() {
public virtual Task<IActionResult> Logout() {
var response = HttpContext.GetOpenIdConnectResponse();
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();
if (request == null) {
return View("Error", new OpenIdConnectMessage {
return Task.FromResult<IActionResult>(View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}));
}
return View("Logout", Tuple.Create(request, identity));
return Task.FromResult<IActionResult>(View("Logout", request));
}
[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
// when the user agent is redirected from the external identity provider
// 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
// 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>
<p class="lead text-left">Are you sure you want to sign out?</p>
@if (Model.Item2 != null && Model.Item2.Identity != null) {
<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">
<form action="@Url.Action("Signout", "Authorization")" enctype="application/x-www-form-urlencoded" method="post">
@Html.AntiForgeryToken()
@* 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" />
}

Loading…
Cancel
Save