Browse Source

Extend request_id to the logout endpoint and update AuthorizationController to only flow the request_id instead of the entire payload

pull/178/head
Kévin Chalet 10 years ago
parent
commit
113176d023
  1. 13
      samples/Mvc.Server/Controllers/AuthorizationController.cs
  2. 5
      samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs
  3. 5
      samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs
  4. 4
      samples/Mvc.Server/Views/Authorization/Authorize.cshtml
  5. 8
      samples/Mvc.Server/Views/Authorization/Logout.cshtml
  6. 26
      src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs
  7. 160
      src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs
  8. 3
      src/OpenIddict.Core/OpenIddictConstants.cs

13
samples/Mvc.Server/Controllers/AuthorizationController.cs

@ -5,7 +5,6 @@
*/
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
@ -51,11 +50,11 @@ namespace Mvc.Server {
});
}
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel {
ApplicationName = application.DisplayName,
Parameters = request.ToDictionary(
parameter => parameter.Key,
parameter => (string) parameter.Value),
RequestId = request.RequestId,
Scope = request.Scope
});
}
@ -103,10 +102,10 @@ namespace Mvc.Server {
// Extract the authorization request from the ASP.NET environment.
var request = HttpContext.GetOpenIdConnectRequest();
// Flow the request_id to allow OpenIddict to restore
// the original logout request from the distributed cache.
return View(new LogoutViewModel {
Parameters = request.ToDictionary(
parameter => parameter.Key,
parameter => (string) parameter.Value)
RequestId = request.RequestId
});
}

5
samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Mvc.Server.ViewModels.Authorization {
@ -8,7 +7,7 @@ namespace Mvc.Server.ViewModels.Authorization {
public string ApplicationName { get; set; }
[BindNever]
public IDictionary<string, string> Parameters { get; set; }
public string RequestId { get; set; }
[Display(Name = "Scope")]
public string Scope { get; set; }

5
samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs

@ -1,9 +1,8 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Mvc.Server.ViewModels.Authorization {
public class LogoutViewModel {
[BindNever]
public IDictionary<string, string> Parameters { get; set; }
public string RequestId { get; set; }
}
}

4
samples/Mvc.Server/Views/Authorization/Authorize.cshtml

@ -8,9 +8,7 @@
<form method="post">
@Html.AntiForgeryToken()
@foreach (var parameter in Model.Parameters) {
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}
<input type="hidden" name="request_id" value="@Model.RequestId" />
<input formaction="@Url.Action("Accept")" class="btn btn-lg btn-success" name="Authorize" type="submit" value="Yes" />
<input formaction="@Url.Action("Deny")" class="btn btn-lg btn-danger" name="Deny" type="submit" value="No" />

8
samples/Mvc.Server/Views/Authorization/Logout.cshtml

@ -1,11 +1,11 @@
<div class="jumbotron">
@model LogoutViewModel
<div class="jumbotron">
<h1>Log out</h1>
<p class="lead text-left">Are you sure you want to sign out?</p>
<form asp-controller="Authorization" asp-action="Logout" method="post">
@foreach (var parameter in Model.Parameters) {
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}
<input type="hidden" name="request_id" value="@Model.RequestId" />
<input class="btn btn-lg btn-success" name="Confirm" type="submit" value="Yes" />
</form>

26
src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs

@ -57,7 +57,11 @@ namespace OpenIddict.Infrastructure {
// If a request_id parameter can be found in the authorization request,
// restore the complete authorization request stored in the distributed cache.
if (!string.IsNullOrEmpty(context.Request.RequestId)) {
var payload = await services.Options.Cache.GetAsync(OpenIddictConstants.Environment.Request + context.Request.RequestId);
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId;
var payload = await services.Options.Cache.GetAsync(key);
if (payload == null) {
services.Logger.LogError("The authorization request was rejected because an unknown " +
"or invalid request_id parameter was specified.");
@ -357,7 +361,7 @@ namespace OpenIddict.Infrastructure {
if (string.IsNullOrEmpty(context.Request.RequestId)) {
// Generate a request identifier. Note: using a crypto-secure
// random number generator is not necessary in this case.
var identifier = Guid.NewGuid().ToString();
context.Request.RequestId = Guid.NewGuid().ToString();
// Store the serialized authorization request parameters in the distributed cache.
var stream = new MemoryStream();
@ -368,17 +372,19 @@ namespace OpenIddict.Infrastructure {
serializer.Serialize(writer, context.Request);
}
var options = new DistributedCacheEntryOptions {
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId;
await services.Options.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions {
AbsoluteExpiration = context.Options.SystemClock.UtcNow + TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10)
};
await services.Options.Cache.SetAsync(OpenIddictConstants.Environment.Request + identifier, stream.ToArray(), options);
});
// Create a new authorization request containing only the request_id parameter.
var address = QueryHelpers.AddQueryString(
uri: context.HttpContext.Request.PathBase + context.HttpContext.Request.Path,
name: OpenIdConnectConstants.Parameters.RequestId, value: identifier);
name: OpenIdConnectConstants.Parameters.RequestId, value: context.Request.RequestId);
context.HttpContext.Response.Redirect(address);
@ -397,10 +403,14 @@ namespace OpenIddict.Infrastructure {
// Remove the authorization request from the distributed cache.
if (!string.IsNullOrEmpty(context.Request.RequestId)) {
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId;
// Note: the ApplyAuthorizationResponse event is called for both successful
// and errored authorization responses but discrimination is not necessary here,
// as the authorization request must be removed from the distributed cache in both cases.
await services.Options.Cache.RemoveAsync(OpenIddictConstants.Environment.Request + context.Request.RequestId);
await services.Options.Cache.RemoveAsync(key);
}
if (!context.Options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Response.Error) &&

160
src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs

@ -4,6 +4,8 @@
* the license and the contributors participating to this project.
*/
using System;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
@ -12,12 +14,50 @@ using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
namespace OpenIddict.Infrastructure {
public partial class OpenIddictProvider<TUser, TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
where TUser : class where TApplication : class where TAuthorization : class where TScope : class where TToken : class {
public override async Task ExtractLogoutRequest([NotNull] ExtractLogoutRequestContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
// If a request_id parameter can be found in the logout request,
// restore the complete logout request stored in the distributed cache.
if (!string.IsNullOrEmpty(context.Request.RequestId)) {
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId;
var payload = await services.Options.Cache.GetAsync(key);
if (payload == null) {
services.Logger.LogError("The logout request was rejected because an unknown " +
"or invalid request_id parameter was specified.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Invalid request: timeout expired.");
return;
}
// Restore the logout request parameters from the serialized payload.
using (var reader = new BsonReader(new MemoryStream(payload))) {
var serializer = JsonSerializer.CreateDefault();
// Note: JsonSerializer.Populate() automatically preserves
// the original request parameters resolved from the cache
// when parameters with the same names are specified.
serializer.Populate(reader, context.Request);
}
}
}
public override async Task ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
@ -54,61 +94,113 @@ namespace OpenIddict.Infrastructure {
// Only validate the id_token_hint if the user is still logged in.
// If the authentication cookie doesn't exist or is no longer valid,
// the user agent is immediately redirected to the client application.
if (context.HttpContext.User.Identities.Any(identity => identity.IsAuthenticated)) {
// Ensure that the authentication cookie contains the required ClaimTypes.NameIdentifier claim.
// If it cannot be found, don't handle the logout request at this stage and continue to the next middleware.
var identifier = context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier);
if (string.IsNullOrEmpty(identifier)) {
services.Logger.LogWarning("The logout request was not silently processed because the mandatory " +
"ClaimTypes.NameIdentifier claim was missing from the current principal.");
if (!context.HttpContext.User.Identities.Any(identity => identity.IsAuthenticated)) {
services.Logger.LogDebug("The logout request was silently processed without requiring user confirmation " +
"because the user was not authenticated or his session was no longer valid.");
context.SkipToNextMiddleware();
// Redirect the user agent back to the client application.
await context.HttpContext.Authentication.SignOutAsync(context.Options.AuthenticationScheme);
return;
}
// Mark the response as handled
// to skip the rest of the pipeline.
context.HandleResponse();
return;
}
// At this stage, ensure the authentication cookie contains the required ClaimTypes.NameIdentifier claim.
// If it cannot be found, don't handle the logout request at this stage and continue to the next middleware.
var identifier = context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier);
if (!string.IsNullOrEmpty(identifier)) {
// 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) {
services.Logger.LogInformation("The logout request was not silently processed because " +
"the id_token_hint parameter was missing or invalid.");
if (principal != null && principal.HasClaim(ClaimTypes.NameIdentifier, identifier)) {
services.Logger.LogInformation("The user '{Username}' was successfully logged out without requiring confirmation.",
services.Users.GetUserName(principal));
context.SkipToNextMiddleware();
// Delete the ASP.NET Core Identity cookies.
await services.SignIn.SignOutAsync();
// Redirect the user agent back to the client application.
await context.HttpContext.Authentication.SignOutAsync(context.Options.AuthenticationScheme);
// Mark the response as handled
// to skip the rest of the pipeline.
context.HandleResponse();
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, identifier)) {
services.Logger.LogWarning("The logout request was not silently processed because the principal extracted " +
"from the id_token_hint parameter didn't correspond to the logged in user.");
else {
services.Logger.LogInformation("The logout request was not silently processed because " +
"the id_token_hint parameter was missing or invalid or " +
"didn't correspond to the logged in user.");
}
}
context.SkipToNextMiddleware();
else {
services.Logger.LogWarning("The logout request was not silently processed because the mandatory " +
"ClaimTypes.NameIdentifier claim was missing from the current principal.");
}
return;
// If no request_id parameter can be found in the current request, assume the OpenID Connect
// request was not serialized yet and store the entire payload in the distributed cache
// to make it easier to flow across requests and internal/external logout workflows.
if (string.IsNullOrEmpty(context.Request.RequestId)) {
// Generate a request identifier. Note: using a crypto-secure
// random number generator is not necessary in this case.
context.Request.RequestId = Guid.NewGuid().ToString();
// Store the serialized logout request parameters in the distributed cache.
var stream = new MemoryStream();
using (var writer = new BsonWriter(stream)) {
writer.CloseOutput = false;
var serializer = JsonSerializer.CreateDefault();
serializer.Serialize(writer, context.Request);
}
services.Logger.LogInformation("The user '{Username}' was successfully logged out.",
services.Users.GetUserName(principal));
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId;
// Delete the ASP.NET Core Identity cookies.
await services.SignIn.SignOutAsync();
}
await services.Options.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions {
AbsoluteExpiration = context.Options.SystemClock.UtcNow + TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10)
});
services.Logger.LogDebug("The logout request was silently processed without requiring user confirmation.");
// Create a new logout request containing only the request_id parameter.
var address = QueryHelpers.AddQueryString(
uri: context.HttpContext.Request.PathBase + context.HttpContext.Request.Path,
name: OpenIdConnectConstants.Parameters.RequestId, value: context.Request.RequestId);
// Redirect the user agent back to the client application.
await context.HttpContext.Authentication.SignOutAsync(context.Options.AuthenticationScheme);
context.HttpContext.Response.Redirect(address);
// Mark the response as handled
// to skip the rest of the pipeline.
context.HandleResponse();
// Mark the response as handled
// to skip the rest of the pipeline.
context.HandleResponse();
return;
}
}
public override Task ApplyLogoutResponse([NotNull] ApplyLogoutResponseContext context) {
public override async Task ApplyLogoutResponse([NotNull] ApplyLogoutResponseContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
// Remove the logout request from the distributed cache.
if (!string.IsNullOrEmpty(context.Request.RequestId)) {
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId;
// Note: the ApplyLogoutResponse event is called for both successful
// and errored logout responses but discrimination is not necessary here,
// as the logout request must be removed from the distributed cache in both cases.
await services.Options.Cache.RemoveAsync(key);
}
if (!context.Options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Response.Error) &&
string.IsNullOrEmpty(context.Response.PostLogoutRedirectUri)) {
// Determine if the status code pages middleware has been enabled for this request.
@ -125,8 +217,6 @@ namespace OpenIddict.Infrastructure {
context.HandleResponse();
}
}
return Task.FromResult(0);
}
}
}

3
src/OpenIddict.Core/OpenIddictConstants.cs

@ -16,7 +16,8 @@ namespace OpenIddict {
}
public static class Environment {
public const string Request = "openiddict-request:";
public const string AuthorizationRequest = "openiddict-authorization-request:";
public const string LogoutRequest = "openiddict-logout-request:";
}
public static class Scopes {

Loading…
Cancel
Save