Browse Source

Allow custom properties marked as public to be returned as authorization/logout/token response parameters

pull/495/head
Kévin Chalet 8 years ago
parent
commit
d7589c229e
  1. 8
      src/OpenIddict.Core/OpenIddictConstants.cs
  2. 118
      src/OpenIddict/OpenIddictProvider.Helpers.cs
  3. 141
      src/OpenIddict/OpenIddictProvider.Signin.cs
  4. 170
      src/OpenIddict/OpenIddictProvider.cs
  5. 1012
      test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs
  6. 1240
      test/OpenIddict.Tests/OpenIddictProviderTests.cs

8
src/OpenIddict.Core/OpenIddictConstants.cs

@ -43,6 +43,14 @@ namespace OpenIddict.Core
public const string AuthorizationId = ".authorization_id";
}
public static class PropertyTypes
{
public const string Boolean = "#public_boolean";
public const string Integer = "#public_integer";
public const string Json = "#public_json";
public const string String = "#public_string";
}
public static class Separators
{
public const string Space = " ";

118
src/OpenIddict/OpenIddictProvider.Helpers.cs

@ -5,7 +5,10 @@
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Security.Cryptography;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
@ -16,6 +19,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
using OpenIddict.Core;
namespace OpenIddict
@ -446,5 +450,119 @@ namespace OpenIddict
return false;
}
}
private IEnumerable<(string property, string parameter, OpenIdConnectParameter value)> GetParameters(
OpenIdConnectRequest request, AuthenticationProperties properties)
{
Debug.Assert(properties != null, "The authentication properties shouldn't be null.");
Debug.Assert(request != null, "The request shouldn't be null.");
Debug.Assert(request.IsAuthorizationRequest() || request.IsLogoutRequest() || request.IsTokenRequest(),
"The request should be an authorization, logout or token request.");
foreach (var property in properties.Items)
{
if (string.IsNullOrEmpty(property.Key))
{
continue;
}
if (string.IsNullOrEmpty(property.Value))
{
continue;
}
if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Boolean))
{
var name = property.Key.Substring(
/* index: */ 0,
/* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Boolean));
bool value;
try
{
value = bool.Parse(property.Value);
}
catch (Exception exception)
{
Logger.LogWarning(exception, "An error occurred while parsing the public property " +
"'{Name}' from the authentication ticket.", name);
continue;
}
yield return (property.Key, name, value);
}
else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Integer))
{
var name = property.Key.Substring(
/* index: */ 0,
/* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Integer));
long value;
try
{
value = long.Parse(property.Value, CultureInfo.InvariantCulture);
}
catch (Exception exception)
{
Logger.LogWarning(exception, "An error occurred while parsing the public property " +
"'{Name}' from the authentication ticket.", name);
continue;
}
yield return (property.Key, name, value);
}
else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Json))
{
var name = property.Key.Substring(
/* index: */ 0,
/* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Json));
if (request.IsAuthorizationRequest() || request.IsLogoutRequest())
{
Logger.LogWarning("The JSON property '{Name}' was excluded as it was not " +
"compatible with the OpenID Connect response type.", name);
continue;
}
JToken value;
try
{
value = JToken.Parse(property.Value);
}
catch (Exception exception)
{
Logger.LogWarning(exception, "An error occurred while deserializing the public JSON " +
"property '{Name}' from the authentication ticket.", name);
continue;
}
yield return (property.Key, name, value);
}
else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.String))
{
var name = property.Key.Substring(
/* index: */ 0,
/* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.String));
yield return (property.Key, name, property.Value);
}
continue;
}
}
}
}

141
src/OpenIddict/OpenIddictProvider.Signin.cs

@ -1,141 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Diagnostics;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using OpenIddict.Core;
namespace OpenIddict
{
public partial class OpenIddictProvider<TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
where TApplication : class where TAuthorization : class where TScope : class where TToken : class
{
public override async Task ProcessSigninResponse([NotNull] ProcessSigninResponseContext context)
{
var options = (OpenIddictOptions) context.Options;
if (context.Request.IsTokenRequest() && (context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsRefreshTokenGrantType()))
{
// Note: when handling a grant_type=authorization_code or refresh_token request,
// the OpenID Connect server middleware allows creating authentication tickets
// that are completely disconnected from the original code or refresh token ticket.
// This scenario is deliberately not supported in OpenIddict and all the tickets
// must be linked. To ensure the properties are flowed from the authorization code
// or the refresh token to the new ticket, they are manually restored if necessary.
// Retrieve the original authentication ticket from the request properties.
var ticket = context.Request.GetProperty<AuthenticationTicket>(
OpenIddictConstants.Properties.AuthenticationTicket);
Debug.Assert(ticket != null, "The authentication ticket shouldn't be null.");
// If the properties instances of the two authentication tickets differ,
// restore the missing properties in the new authentication ticket.
if (!ReferenceEquals(ticket.Properties, context.Ticket.Properties))
{
foreach (var property in ticket.Properties.Items)
{
// Don't override the properties that have been
// manually set on the new authentication ticket.
if (context.Ticket.HasProperty(property.Key))
{
continue;
}
context.Ticket.AddProperty(property.Key, property.Value);
}
// Always include the "openid" scope when the developer doesn't explicitly call SetScopes.
// Note: the application is allowed to specify a different "scopes": in this case,
// don't replace the "scopes" property stored in the authentication ticket.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OpenId) && !context.Ticket.HasScope())
{
context.Ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId);
}
context.IncludeIdentityToken = context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId);
}
context.IncludeRefreshToken = context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess);
// Always include a refresh token for grant_type=refresh_token requests if
// rolling tokens are enabled and if the offline_access scope was specified.
if (context.Request.IsRefreshTokenGrantType())
{
context.IncludeRefreshToken &= options.UseRollingTokens;
}
// If token revocation was explicitly disabled,
// none of the following security routines apply.
if (options.DisableTokenRevocation)
{
return;
}
// If rolling tokens are enabled or if the request is a grant_type=authorization_code request,
// mark the authorization code or the refresh token as redeemed to prevent future reuses.
// See https://tools.ietf.org/html/rfc6749#section-6 for more information.
if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType())
{
if (!await TryRedeemTokenAsync(context.Ticket, context.HttpContext))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified authorization code is no longer valid.");
return;
}
}
// When rolling tokens are enabled, revoke all the previously issued tokens associated
// with the authorization if the request is a grant_type=refresh_token request.
if (options.UseRollingTokens && context.Request.IsRefreshTokenGrantType())
{
if (!await TryRevokeTokensAsync(context.Ticket, context.HttpContext))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified refresh token is no longer valid.");
return;
}
}
// When rolling tokens are disabled, extend the expiration date
// of the existing token instead of returning a new refresh token
// with a new expiration date if sliding expiration was not disabled.
else if (options.UseSlidingExpiration && context.Request.IsRefreshTokenGrantType())
{
if (!await TryExtendTokenAsync(context.Ticket, context.HttpContext, options))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified refresh token is no longer valid.");
return;
}
// Prevent the OpenID Connect server from returning a new refresh token.
context.IncludeRefreshToken = false;
}
}
// If no authorization was explicitly attached to the authentication ticket,
// create an ad hoc authorization if an authorization code or a refresh token
// is going to be returned to the client application as part of the response.
if (!context.Ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId) &&
(context.IncludeAuthorizationCode || context.IncludeRefreshToken))
{
await CreateAuthorizationAsync(context.Ticket, options, context.HttpContext, context.Request);
}
}
}
}

170
src/OpenIddict/OpenIddictProvider.cs

@ -5,8 +5,14 @@
*/
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using OpenIddict.Core;
@ -60,5 +66,169 @@ namespace OpenIddict
/// Gets the tokens manager.
/// </summary>
public OpenIddictTokenManager<TToken> Tokens { get; }
public override Task ProcessChallengeResponse([NotNull] ProcessChallengeResponseContext context)
{
Debug.Assert(context.Request.IsAuthorizationRequest() ||
context.Request.IsTokenRequest(),
"The request should be an authorization or token request.");
// Add the custom properties that are marked as public
// as authorization or token response properties.
var parameters = GetParameters(context.Request, context.Properties);
foreach (var (property, parameter, value) in parameters)
{
context.Response.AddParameter(parameter, value);
}
return Task.CompletedTask;
}
public override async Task ProcessSigninResponse([NotNull] ProcessSigninResponseContext context)
{
var options = (OpenIddictOptions) context.Options;
Debug.Assert(context.Request.IsAuthorizationRequest() ||
context.Request.IsTokenRequest(),
"The request should be an authorization or token request.");
if (context.Request.IsTokenRequest() && (context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsRefreshTokenGrantType()))
{
// Note: when handling a grant_type=authorization_code or refresh_token request,
// the OpenID Connect server middleware allows creating authentication tickets
// that are completely disconnected from the original code or refresh token ticket.
// This scenario is deliberately not supported in OpenIddict and all the tickets
// must be linked. To ensure the properties are flowed from the authorization code
// or the refresh token to the new ticket, they are manually restored if necessary.
if (!context.Ticket.Properties.HasProperty(OpenIdConnectConstants.Properties.TokenId))
{
// Retrieve the original authentication ticket from the request properties.
var ticket = context.Request.GetProperty<AuthenticationTicket>(
OpenIddictConstants.Properties.AuthenticationTicket);
Debug.Assert(ticket != null, "The authentication ticket shouldn't be null.");
foreach (var property in ticket.Properties.Items)
{
// Don't override the properties that have been
// manually set on the new authentication ticket.
if (context.Ticket.HasProperty(property.Key))
{
continue;
}
context.Ticket.AddProperty(property.Key, property.Value);
}
// Always include the "openid" scope when the developer doesn't explicitly call SetScopes.
// Note: the application is allowed to specify a different "scopes": in this case,
// don't replace the "scopes" property stored in the authentication ticket.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OpenId) && !context.Ticket.HasScope())
{
context.Ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId);
}
context.IncludeIdentityToken = context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId);
}
context.IncludeRefreshToken = context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess);
// Always include a refresh token for grant_type=refresh_token requests if
// rolling tokens are enabled and if the offline_access scope was specified.
if (context.Request.IsRefreshTokenGrantType())
{
context.IncludeRefreshToken &= options.UseRollingTokens;
}
// If token revocation was explicitly disabled,
// none of the following security routines apply.
if (options.DisableTokenRevocation)
{
return;
}
// If rolling tokens are enabled or if the request is a grant_type=authorization_code request,
// mark the authorization code or the refresh token as redeemed to prevent future reuses.
// See https://tools.ietf.org/html/rfc6749#section-6 for more information.
if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType())
{
if (!await TryRedeemTokenAsync(context.Ticket, context.HttpContext))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified authorization code is no longer valid.");
return;
}
}
// When rolling tokens are enabled, revoke all the previously issued tokens associated
// with the authorization if the request is a grant_type=refresh_token request.
if (options.UseRollingTokens && context.Request.IsRefreshTokenGrantType())
{
if (!await TryRevokeTokensAsync(context.Ticket, context.HttpContext))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified refresh token is no longer valid.");
return;
}
}
// When rolling tokens are disabled, extend the expiration date
// of the existing token instead of returning a new refresh token
// with a new expiration date if sliding expiration was not disabled.
else if (options.UseSlidingExpiration && context.Request.IsRefreshTokenGrantType())
{
if (!await TryExtendTokenAsync(context.Ticket, context.HttpContext, options))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified refresh token is no longer valid.");
return;
}
// Prevent the OpenID Connect server from returning a new refresh token.
context.IncludeRefreshToken = false;
}
}
// If no authorization was explicitly attached to the authentication ticket,
// create an ad hoc authorization if an authorization code or a refresh token
// is going to be returned to the client application as part of the response.
if (!context.Ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId) &&
(context.IncludeAuthorizationCode || context.IncludeRefreshToken))
{
await CreateAuthorizationAsync(context.Ticket, options, context.HttpContext, context.Request);
}
// Add the custom properties that are marked as public as authorization or
// token response properties and remove them from the authentication ticket
// so they are not persisted in the authorization code/access/refresh token.
// Note: make sure the foreach statement iterates on a copy of the ticket
// as the property collection is modified when the property is removed.
var parameters = GetParameters(context.Request, context.Ticket.Properties);
foreach (var (property, parameter, value) in parameters.ToArray())
{
context.Response.AddParameter(parameter, value);
context.Ticket.RemoveProperty(property);
}
}
public override Task ProcessSignoutResponse([NotNull] ProcessSignoutResponseContext context)
{
Debug.Assert(context.Request.IsLogoutRequest(), "The request should be a logout request.");
// Add the custom properties that are marked as public as logout response properties.
var parameters = GetParameters(context.Request, context.Properties);
foreach (var (property, parameter, value) in parameters)
{
context.Response.AddParameter(parameter, value);
}
return Task.CompletedTask;
}
}
}

1012
test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs

File diff suppressed because it is too large

1240
test/OpenIddict.Tests/OpenIddictProviderTests.cs

File diff suppressed because it is too large
Loading…
Cancel
Save