Browse Source

Revamp the claims extensions and update the client and server hosts to preserve the authentication properties using a special private claim

pull/1462/head
Kévin Chalet 4 years ago
parent
commit
ddc5c9a69c
  1. 42
      sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs
  2. 42
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthenticationController.cs
  3. 66
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs
  4. 10
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
  5. 18
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthenticationController.cs
  6. 60
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs
  7. 1
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  8. 2
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  9. 1542
      src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
  10. 43
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
  11. 8
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
  12. 1
      src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConstants.cs
  13. 30
      src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs
  14. 43
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs
  15. 8
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
  16. 38
      src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs
  17. 2
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  18. 33
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  19. 11
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  20. 1
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs
  21. 30
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs
  22. 33
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  23. 11
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  24. 1
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs
  25. 30
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs
  26. 38
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
  27. 4440
      test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs
  28. 3
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

42
sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs

@ -7,6 +7,7 @@ using System.Web;
using System.Web.Mvc;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using OpenIddict.Abstractions;
using OpenIddict.Client.Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants;
@ -54,7 +55,7 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
{
// Note: the OWIN host requires appending the #string suffix to indicate
// that the "identity_provider" property is a public string parameter.
properties.Dictionary["identity_provider#string"] = "github";
properties.Dictionary[Parameters.IdentityProvider + PropertyTypes.String] = "github";
}
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
@ -112,19 +113,16 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
var claims = new List<Claim>(result.Identity.Claims
.Select(claim => claim switch
{
// Map the standard "sub" claim to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier,
// which is the default claim type used by ASP.NET and is required by the antiforgery components.
{ Type: Claims.Subject }
// Map the standard "sub" and custom "id" claims to ClaimTypes.NameIdentifier, which is
// the default claim type used by ASP.NET and is required by the antiforgery components.
{ Type: Claims.Subject } or
{ Type: "id", Issuer: "https://github.com/" or "https://twitter.com/" }
=> new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
// Map the standard "name" claim to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name.
// Map the standard "name" claim to ClaimTypes.Name.
{ Type: Claims.Name }
=> new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
// Applications can map non-standard claims issued by specific issuers to a standard equivalent.
{ Type: "id", Issuer: "https://github.com/" or "https://twitter.com/" }
=> new Claim(Claims.Subject, claim.Value, claim.ValueType, claim.Issuer),
_ => claim
})
.Where(claim => claim switch
@ -141,27 +139,31 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
// The antiforgery components require both the nameidentifier and identityprovider claims
// so the latter is manually added using the issuer identity resolved from the remote server.
claims.Add(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", claims[0].Issuer));
claims.Add(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",
result.Identity.GetClaim(Claims.AuthorizationServer)));
var identity = new ClaimsIdentity(claims,
authenticationType: CookieAuthenticationDefaults.AuthenticationType,
nameType: ClaimTypes.Name,
roleType: ClaimTypes.Role);
var properties = new AuthenticationProperties
{
RedirectUri = result.Properties.RedirectUri
};
// Build the authentication properties based on the properties that were added when the challenge was triggered.
var properties = new AuthenticationProperties(result.Properties.Dictionary
.Where(item => item switch
{
// Preserve the redirect URL.
{ Key: ".redirect" } => true,
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
properties.Dictionary[Tokens.BackchannelAccessToken] = GetProperty(result.Properties, Tokens.BackchannelAccessToken);
properties.Dictionary[Tokens.RefreshToken] = GetProperty(result.Properties, Tokens.RefreshToken);
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
{ Key: Tokens.BackchannelAccessToken or Tokens.RefreshToken } => true,
// Don't add the other properties to the external cookie.
_ => false
})
.ToDictionary(pair => pair.Key, pair => pair.Value));
context.Authentication.SignIn(properties, identity);
return Redirect(properties.RedirectUri);
static string GetProperty(AuthenticationProperties properties, string name)
=> properties.Dictionary.TryGetValue(name, out var value) ? value : string.Empty;
}
[AcceptVerbs("GET", "POST"), Route("~/logout")]

42
sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthenticationController.cs

@ -7,7 +7,9 @@ using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.Owin.Security;
using OpenIddict.Abstractions;
using OpenIddict.Client.Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants;
namespace OpenIddict.Sandbox.AspNet.Server.Controllers
@ -64,18 +66,22 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
var claims = new List<Claim>(result.Identity.Claims
.Select(claim => claim switch
{
// Note: when using external authentication providers with ASP.NET Core Identity,
// the "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" claim
// - which is not configurable in Identity - MUST be used to store the user identifier.
// Map the standard "sub" and custom "id" claims to ClaimTypes.NameIdentifier, which is
// the default claim type used by ASP.NET and is required by the antiforgery components.
{ Type: Claims.Subject } or
{ Type: "id", Issuer: "https://github.com/" }
=> new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
// Map the standard "name" claim to ClaimTypes.Name.
{ Type: Claims.Name }
=> new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
_ => claim
})
.Where(claim => claim switch
{
// Preserve the "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" claim.
{ Type: ClaimTypes.NameIdentifier } => true,
// Preserve the nameidentifier and name claims.
{ Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,
@ -86,27 +92,31 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
// The antiforgery components require both the nameidentifier and identityprovider claims
// so the latter is manually added using the issuer identity resolved from the remote server.
claims.Add(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", claims[0].Issuer));
claims.Add(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",
result.Identity.GetClaim(Claims.AuthorizationServer)));
var identity = new ClaimsIdentity(claims,
authenticationType: DefaultAuthenticationTypes.ExternalCookie,
nameType: ClaimTypes.Name,
roleType: ClaimTypes.Role);
var properties = new AuthenticationProperties
{
RedirectUri = result.Properties.RedirectUri
};
// Build the authentication properties based on the properties that were added when the challenge was triggered.
var properties = new AuthenticationProperties(result.Properties.Dictionary
.Where(item => item switch
{
// Preserve the redirect URL.
{ Key: ".redirect" } => true,
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
properties.Dictionary[Tokens.BackchannelAccessToken] = GetProperty(result.Properties, Tokens.BackchannelAccessToken);
properties.Dictionary[Tokens.RefreshToken] = GetProperty(result.Properties, Tokens.RefreshToken);
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
{ Key: Tokens.BackchannelAccessToken or Tokens.RefreshToken } => true,
// Don't add the other properties to the external cookie.
_ => false
})
.ToDictionary(pair => pair.Key, pair => pair.Value));
context.Authentication.SignIn(properties, identity);
return Redirect(properties.RedirectUri);
static string GetProperty(AuthenticationProperties properties, string name)
=> properties.Dictionary.TryGetValue(name, out var value) ? value : string.Empty;
}
}
}

66
sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs

@ -146,13 +146,11 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
identity.AddClaim(new Claim(Claims.Subject, identity.FindFirstValue(ClaimTypes.NameIdentifier)));
identity.AddClaim(new Claim(Claims.Name, identity.FindFirstValue(ClaimTypes.Name)));
var principal = new ClaimsPrincipal(identity);
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(request.GetScopes());
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
identity.SetScopes(request.GetScopes());
identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
// Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes.
@ -160,21 +158,17 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
if (authorization == null)
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
principal: new ClaimsPrincipal(identity),
subject : user.Id,
client : await _applicationManager.GetIdAsync(application),
type : AuthorizationTypes.Permanent,
scopes : principal.GetScopes());
scopes : identity.GetScopes());
}
principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
identity.SetDestinations(GetDestinations);
context.Authentication.SignIn(new AuthenticationProperties(), (ClaimsIdentity) principal.Identity);
context.Authentication.SignIn(new AuthenticationProperties(), identity);
return new EmptyResult();
@ -267,13 +261,11 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
identity.AddClaim(new Claim(Claims.Subject, identity.FindFirstValue(ClaimTypes.NameIdentifier)));
identity.AddClaim(new Claim(Claims.Name, identity.FindFirstValue(ClaimTypes.Name)));
var principal = new ClaimsPrincipal(identity);
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(request.GetScopes());
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
identity.SetScopes(request.GetScopes());
identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
// Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes.
@ -281,22 +273,18 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
if (authorization == null)
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
principal: new ClaimsPrincipal(identity),
subject : user.Id,
client : await _applicationManager.GetIdAsync(application),
type : AuthorizationTypes.Permanent,
scopes : principal.GetScopes());
scopes : identity.GetScopes());
}
principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
identity.SetDestinations(GetDestinations);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
context.Authentication.SignIn(new AuthenticationProperties(), (ClaimsIdentity) principal.Identity);
context.Authentication.SignIn(new AuthenticationProperties(), identity);
return new EmptyResult();
}
@ -351,11 +339,11 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/device code/refresh token.
var principal = new ClaimsPrincipal((await context.Authentication.AuthenticateAsync(OpenIddictServerOwinDefaults.AuthenticationType)).Identity);
// Retrieve the claims identity stored in the authorization code/device code/refresh token.
var result = await context.Authentication.AuthenticateAsync(OpenIddictServerOwinDefaults.AuthenticationType);
// Retrieve the user profile corresponding to the authorization code/refresh token.
var user = await context.GetUserManager<ApplicationUserManager>().FindByIdAsync(principal.GetClaim(Claims.Subject));
var user = await context.GetUserManager<ApplicationUserManager>().FindByIdAsync(result.Identity.GetClaim(Claims.Subject));
if (user == null)
{
context.Authentication.Challenge(
@ -383,16 +371,8 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
return new EmptyResult();
}
var identity = new ClaimsIdentity(OpenIddictServerOwinDefaults.AuthenticationType);
identity.AddClaims((await context.Get<ApplicationSignInManager>().CreateUserIdentityAsync(user)).Claims);
identity.AddClaim(new Claim(Claims.Subject, identity.FindFirstValue(ClaimTypes.NameIdentifier)));
identity.AddClaim(new Claim(Claims.Name, identity.FindFirstValue(ClaimTypes.Name)));
foreach (var claim in identity.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
var identity = new ClaimsIdentity(result.Identity.Claims, OpenIddictServerOwinDefaults.AuthenticationType);
identity.SetDestinations(GetDestinations);
// Ask OpenIddict to issue the appropriate access/identity tokens.
context.Authentication.SignIn(new AuthenticationProperties(), identity);
@ -403,7 +383,7 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
throw new InvalidOperationException("The specified grant type is not supported.");
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
private static IEnumerable<string> GetDestinations(Claim claim)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
@ -414,7 +394,7 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
if (claim.Subject.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
@ -422,7 +402,7 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
if (claim.Subject.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
@ -430,7 +410,7 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
if (claim.Subject.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;

10
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs

@ -45,7 +45,7 @@ public class AuthenticationController : Controller
// the user is directly redirected to GitHub (in this case, no login page is shown).
if (provider is "local-github")
{
properties.Parameters["identity_provider"] = "github";
properties.Parameters[Parameters.IdentityProvider] = "github";
}
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
@ -123,13 +123,11 @@ public class AuthenticationController : Controller
nameType: Claims.Name,
roleType: Claims.Role);
var properties = new AuthenticationProperties
{
RedirectUri = result.Properties.RedirectUri
};
// Build the authentication properties based on the properties that were added when the challenge was triggered.
var properties = new AuthenticationProperties(result.Properties.Items);
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
// To make cookies less heavy, tokens that are not used can be filtered out before creating the cookie.
// To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.
properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
{
// Preserve the access and refresh tokens returned in the token response, if available.

18
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthenticationController.cs

@ -57,8 +57,8 @@ public class AuthenticationController : Controller
.Select(claim => claim switch
{
// Note: when using external authentication providers with ASP.NET Core Identity,
// the "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" claim
// - which is not configurable in Identity - MUST be used to store the user identifier.
// the ClaimTypes.NameIdentifier claim - which is not configurable in Identity -
// MUST be used to store the user identifier.
{ Type: "id", Issuer: "https://github.com/" }
=> new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
@ -66,7 +66,7 @@ public class AuthenticationController : Controller
})
.Where(claim => claim switch
{
// Preserve the "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" claim.
// Preserve the ClaimTypes.NameIdentifier claim.
{ Type: ClaimTypes.NameIdentifier } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
@ -84,17 +84,11 @@ public class AuthenticationController : Controller
nameType: ClaimTypes.NameIdentifier,
roleType: ClaimTypes.Role);
var properties = new AuthenticationProperties
{
RedirectUri = result.Properties.RedirectUri
};
// Store the identity of the external provider in the authentication properties to allow
// ASP.NET Core Identity to resolve it when returning the external login confirmation form.
properties.Items["LoginProvider"] = claims[0].Issuer;
// Build the authentication properties based on the properties that were added when the challenge was triggered.
var properties = new AuthenticationProperties(result.Properties.Items);
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
// To make cookies less heavy, tokens that are not used can be filtered out before creating the cookie.
// To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.
properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
{
// Preserve the access and refresh tokens returned in the token response, if available.

60
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs

@ -112,20 +112,16 @@ public class AuthorizationController : Controller
}));
}
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
[OpenIddictClientAspNetCoreConstants.Properties.Issuer] = issuer
})
{
// Once the callback is handled, redirect the user agent to the ASP.NET Identity
// page responsible for showing the external login confirmation form if necessary.
RedirectUri = Url.Action("ExternalLoginCallback", "Account", new
var properties = _signInManager.ConfigureExternalAuthenticationProperties(
provider: issuer,
redirectUrl: Url.Action("ExternalLoginCallback", "Account", new
{
ReturnUrl = Request.PathBase + Request.Path + QueryString.Create(parameters)
})
};
}));
// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
properties.SetString(OpenIddictClientAspNetCoreConstants.Properties.Issuer, issuer);
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
@ -196,11 +192,7 @@ public class AuthorizationController : Controller
}
principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
principal.SetDestinations(GetDestinations);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
@ -286,11 +278,7 @@ public class AuthorizationController : Controller
}
principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
principal.SetDestinations(GetDestinations);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
@ -362,11 +350,7 @@ public class AuthorizationController : Controller
// For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(result.Principal.GetScopes());
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
principal.SetDestinations(GetDestinations);
var properties = new AuthenticationProperties
{
@ -470,11 +454,7 @@ public class AuthorizationController : Controller
// For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(request.GetScopes());
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
principal.SetDestinations(GetDestinations);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
@ -488,7 +468,8 @@ public class AuthorizationController : Controller
// Retrieve the user profile corresponding to the authorization code/refresh token.
// Note: if you want to automatically invalidate the authorization code/refresh token
// when the user password/roles change, use the following line instead:
// var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
//
// var user = await _signInManager.ValidateSecurityStampAsync(info.Principal);
var user = await _userManager.GetUserAsync(principal);
if (user is null)
{
@ -513,10 +494,7 @@ public class AuthorizationController : Controller
}));
}
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
principal.SetDestinations(GetDestinations);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
@ -526,7 +504,7 @@ public class AuthorizationController : Controller
}
#endregion
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
private IEnumerable<string> GetDestinations(Claim claim)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
@ -537,7 +515,7 @@ public class AuthorizationController : Controller
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
if (claim.Subject.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
@ -545,7 +523,7 @@ public class AuthorizationController : Controller
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
if (claim.Subject.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
@ -553,7 +531,7 @@ public class AuthorizationController : Controller
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
if (claim.Subject.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;

1
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -127,6 +127,7 @@ public static class OpenIddictConstants
public const string DeviceCodeLifetime = "oi_dvc_lft";
public const string ExpirationDate = "oi_exp_dt";
public const string GrantType = "oi_grt_typ";
public const string HostProperties = "oi_hst_props";
public const string IdentityTokenLifetime = "oi_idt_lft";
public const string Issuer = "oi_iss";
public const string Nonce = "oi_nce";

2
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -773,7 +773,7 @@ To register the validation services, use 'services.AddOpenIddict().AddValidation
<value>The claim type cannot be null or empty.</value>
</data>
<data name="ID0185" xml:space="preserve">
<value>The claim value cannot be null or empty.</value>
<value>The claim value is not a supported JSON node.</value>
</data>
<data name="ID0186" xml:space="preserve">
<value>The audience cannot be null or empty.</value>

1542
src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs

File diff suppressed because it is too large

43
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs

@ -6,6 +6,7 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
@ -157,15 +158,18 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<OpenIddic
return AuthenticateResult.NoResult();
}
var properties = new AuthenticationProperties
{
ExpiresUtc = principal.GetExpirationDate(),
IssuedUtc = principal.GetCreationDate(),
// Attach the identity of the authorization to the returned principal to allow resolving it even if no other
// claim was added to the principal (e.g when no id_token was returned and no userinfo endpoint is available).
principal.SetClaim(Claims.AuthorizationServer, context.StateTokenPrincipal?.GetClaim(Claims.AuthorizationServer));
// Restore the return URL using the "target_link_uri" that was stored
// in the state token when the challenge operation started, if available.
RedirectUri = context.StateTokenPrincipal?.GetClaim(Claims.TargetLinkUri)
};
// Restore or create a new authentication properties collection and populate it.
var properties = CreateProperties(context.StateTokenPrincipal);
properties.ExpiresUtc = principal.GetExpirationDate();
properties.IssuedUtc = principal.GetCreationDate();
// Restore the return URL using the "target_link_uri" that was stored
// in the state token when the challenge operation started, if available.
properties.RedirectUri = context.StateTokenPrincipal?.GetClaim(Claims.TargetLinkUri);
List<AuthenticationToken>? tokens = null;
@ -338,6 +342,29 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<OpenIddic
return new ClaimsPrincipal(identity);
}
static AuthenticationProperties CreateProperties(ClaimsPrincipal? principal)
{
// Note: the principal may be null if no value was extracted from the corresponding token.
if (principal is not null)
{
var value = principal.GetClaim(Claims.Private.HostProperties);
if (!string.IsNullOrEmpty(value))
{
var dictionary = new Dictionary<string, string?>(comparer: StringComparer.Ordinal);
using var document = JsonDocument.Parse(value);
foreach (var property in document.RootElement.EnumerateObject())
{
dictionary[property.Name] = property.Value.GetString();
}
return new AuthenticationProperties(dictionary);
}
}
return new AuthenticationProperties();
}
}
}

8
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs

@ -412,6 +412,8 @@ public static partial class OpenIddictClientAspNetCoreHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is null)
{
@ -436,6 +438,12 @@ public static partial class OpenIddictClientAspNetCoreHandlers
context.TargetLinkUri = properties.RedirectUri;
}
// Preserve the host properties in the principal.
if (properties.Items.Count is not 0)
{
context.Principal.SetClaim(Claims.Private.HostProperties, properties.Items);
}
foreach (var parameter in properties.Parameters)
{
context.Parameters[parameter.Key] = parameter.Value switch

1
src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConstants.cs

@ -13,6 +13,7 @@ public static class OpenIddictClientDataProtectionConstants
public const string Audiences = ".audiences";
public const string CodeVerifier = ".code_verifier";
public const string Expires = ".expires";
public const string HostProperties = ".host_properties";
public const string InternalTokenId = ".internal_token_id";
public const string Issued = ".issued";
public const string Nonce = ".nonce";

30
src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs

@ -29,10 +29,12 @@ public class OpenIddictClientDataProtectionFormatter : IOpenIddictClientDataProt
// can be reused, well-known properties are manually mapped to their claims equivalents.
return principal
.SetAudiences(GetArrayProperty(properties, Properties.Audiences))
.SetPresenters(GetArrayProperty(properties, Properties.Presenters))
.SetResources(GetArrayProperty(properties, Properties.Resources))
.SetScopes(GetArrayProperty(properties, Properties.Scopes))
.SetClaims(Claims.Private.Audience, GetJsonProperty(properties, Properties.Audiences))
.SetClaims(Claims.Private.Presenter, GetJsonProperty(properties, Properties.Presenters))
.SetClaims(Claims.Private.Resource, GetJsonProperty(properties, Properties.Resources))
.SetClaims(Claims.Private.Scope, GetJsonProperty(properties, Properties.Scopes))
.SetClaim(Claims.Private.HostProperties, GetJsonProperty(properties, Properties.HostProperties))
.SetClaim(Claims.Private.CodeVerifier, GetProperty(properties, Properties.CodeVerifier))
.SetClaim(Claims.Private.CreationDate, GetProperty(properties, Properties.Issued))
@ -158,28 +160,15 @@ public class OpenIddictClientDataProtectionFormatter : IOpenIddictClientDataProt
static string? GetProperty(IReadOnlyDictionary<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? value : null;
static ImmutableArray<string> GetArrayProperty(IReadOnlyDictionary<string, string> properties, string name)
static JsonElement GetJsonProperty(IReadOnlyDictionary<string, string> properties, string name)
{
if (properties.TryGetValue(name, out var value))
{
using var document = JsonDocument.Parse(value);
var builder = ImmutableArray.CreateBuilder<string>(document.RootElement.GetArrayLength());
foreach (var element in document.RootElement.EnumerateArray())
{
var item = element.GetString();
if (string.IsNullOrEmpty(item))
{
continue;
}
builder.Add(item);
}
return builder.ToImmutable();
return document.RootElement.Clone();
}
return ImmutableArray.Create<string>();
return default;
}
}
@ -209,6 +198,7 @@ public class OpenIddictClientDataProtectionFormatter : IOpenIddictClientDataProt
SetProperty(properties, Properties.InternalTokenId, principal.GetTokenId());
SetProperty(properties, Properties.CodeVerifier, principal.GetClaim(Claims.Private.CodeVerifier));
SetProperty(properties, Properties.HostProperties, principal.GetClaim(Claims.Private.HostProperties));
SetProperty(properties, Properties.Nonce, principal.GetClaim(Claims.Private.Nonce));
SetProperty(properties, Properties.OriginalRedirectUri, principal.GetClaim(Claims.Private.RedirectUri));

43
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs

@ -5,6 +5,7 @@
*/
using System.Security.Claims;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Security.Infrastructure;
using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants;
@ -173,15 +174,18 @@ public class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddictClien
return null;
}
var properties = new AuthenticationProperties
{
ExpiresUtc = principal.GetExpirationDate(),
IssuedUtc = principal.GetCreationDate(),
// Attach the identity of the authorization to the returned principal to allow resolving it even if no other
// claim was added to the principal (e.g when no id_token was returned and no userinfo endpoint is available).
principal.SetClaim(Claims.AuthorizationServer, context.StateTokenPrincipal?.GetClaim(Claims.AuthorizationServer));
// Restore the return URL using the "target_link_uri" that was stored
// in the state token when the challenge operation started, if available.
RedirectUri = context.StateTokenPrincipal?.GetClaim(Claims.TargetLinkUri)
};
// Restore or create a new authentication properties collection and populate it.
var properties = CreateProperties(context.StateTokenPrincipal);
properties.ExpiresUtc = principal.GetExpirationDate();
properties.IssuedUtc = principal.GetCreationDate();
// Restore the return URL using the "target_link_uri" that was stored
// in the state token when the challenge operation started, if available.
properties.RedirectUri = context.StateTokenPrincipal?.GetClaim(Claims.TargetLinkUri);
// Attach the tokens to allow any OWIN component (e.g a controller)
// to retrieve them (e.g to make an API request to another application).
@ -266,6 +270,29 @@ public class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddictClien
return new ClaimsPrincipal(identity);
}
static AuthenticationProperties CreateProperties(ClaimsPrincipal? principal)
{
// Note: the principal may be null if no value was extracted from the corresponding token.
if (principal is not null)
{
var value = principal.GetClaim(Claims.Private.HostProperties);
if (!string.IsNullOrEmpty(value))
{
var dictionary = new Dictionary<string, string?>(comparer: StringComparer.Ordinal);
using var document = JsonDocument.Parse(value);
foreach (var property in document.RootElement.EnumerateObject())
{
dictionary[property.Name] = property.Value.GetString();
}
return new AuthenticationProperties(dictionary);
}
}
return new AuthenticationProperties();
}
}
}

8
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs

@ -432,6 +432,8 @@ public static partial class OpenIddictClientOwinHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is null)
{
@ -456,6 +458,12 @@ public static partial class OpenIddictClientOwinHandlers
context.TargetLinkUri = properties.RedirectUri;
}
// Preserve the host properties in the principal.
if (properties.Dictionary.Count is not 0)
{
context.Principal.SetClaim(Claims.Private.HostProperties, properties.Dictionary);
}
// Note: unlike ASP.NET Core, Owin's AuthenticationProperties doesn't offer a strongly-typed
// dictionary that allows flowing parameters while preserving their original types. To allow
// returning custom parameters, the OWIN host allows using AuthenticationProperties.Dictionary

38
src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs

@ -7,7 +7,6 @@
using System.Collections.Immutable;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.IdentityModel.JsonWebTokens;
namespace OpenIddict.Client;
@ -159,28 +158,11 @@ public static partial class OpenIddictClientHandlers
// Top-level claims represented as arrays are split and mapped to multiple CLR claims
// to match the logic implemented by IdentityModel for JWT token deserialization.
case { ValueKind: JsonValueKind.Array } value:
foreach (var item in value.EnumerateArray())
{
identity.AddClaim(new Claim(
type : parameter.Key,
value : item.ToString()!,
valueType : GetClaimValueType(item),
issuer : issuer,
originalIssuer: issuer,
subject : identity));
}
identity.AddClaims(parameter.Key, value, issuer);
break;
// Note: JsonElement.ToString() returns string.Empty for JsonValueKind.Null and
// JsonValueKind.Undefined, which, unlike null strings, is a valid claim value.
case { ValueKind: _ } value:
identity.AddClaim(new Claim(
type : parameter.Key,
value : value.ToString()!,
valueType : GetClaimValueType(value),
issuer : issuer,
originalIssuer: issuer,
subject : identity));
identity.AddClaim(parameter.Key, value, issuer);
break;
}
}
@ -188,22 +170,6 @@ public static partial class OpenIddictClientHandlers
context.Principal = new ClaimsPrincipal(identity);
return default;
static string GetClaimValueType(JsonElement element) => element.ValueKind switch
{
JsonValueKind.String => ClaimValueTypes.String,
JsonValueKind.True or JsonValueKind.False => ClaimValueTypes.Boolean,
JsonValueKind.Number when element.TryGetInt32(out _) => ClaimValueTypes.Integer32,
JsonValueKind.Number when element.TryGetInt64(out _) => ClaimValueTypes.Integer64,
JsonValueKind.Number when element.TryGetUInt32(out _) => ClaimValueTypes.UInteger32,
JsonValueKind.Number when element.TryGetUInt64(out _) => ClaimValueTypes.UInteger64,
JsonValueKind.Number when element.TryGetDouble(out _) => ClaimValueTypes.Double,
JsonValueKind.Null or JsonValueKind.Undefined => JsonClaimValueTypes.JsonNull,
JsonValueKind.Array => JsonClaimValueTypes.JsonArray,
JsonValueKind.Object or _ => JsonClaimValueTypes.Json
};
}
}
}

2
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -10,6 +10,8 @@ using System.Diagnostics;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
#if !SUPPORTS_TIME_CONSTANT_COMPARISONS

33
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs

@ -6,6 +6,7 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants;
@ -181,11 +182,10 @@ public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<OpenIddic
return AuthenticateResult.NoResult();
}
var properties = new AuthenticationProperties
{
ExpiresUtc = principal.GetExpirationDate(),
IssuedUtc = principal.GetCreationDate()
};
// Restore or create a new authentication properties collection and populate it.
var properties = CreateProperties(principal);
properties.ExpiresUtc = principal.GetExpirationDate();
properties.IssuedUtc = principal.GetCreationDate();
List<AuthenticationToken>? tokens = null;
@ -290,6 +290,29 @@ public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<OpenIddic
return AuthenticateResult.Success(new AuthenticationTicket(principal, properties,
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme));
}
static AuthenticationProperties CreateProperties(ClaimsPrincipal? principal)
{
// Note: the principal may be null if no value was extracted from the corresponding token.
if (principal is not null)
{
var value = principal.GetClaim(Claims.Private.HostProperties);
if (!string.IsNullOrEmpty(value))
{
var dictionary = new Dictionary<string, string?>(comparer: StringComparer.Ordinal);
using var document = JsonDocument.Parse(value);
foreach (var property in document.RootElement.EnumerateObject())
{
dictionary[property.Name] = property.Value.GetString();
}
return new AuthenticationProperties(dictionary);
}
}
return new AuthenticationProperties();
}
}
/// <inheritdoc/>

11
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs

@ -7,6 +7,7 @@
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
@ -316,7 +317,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
}
/// <summary>
/// Contains the logic responsible for resolving the additional sign-in parameters stored in the ASP.NET
/// Contains the logic responsible for resolving the additional challenge parameters stored in the ASP.NET
/// Core authentication properties specified by the application that triggered the sign-in operation.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
@ -396,12 +397,20 @@ public static partial class OpenIddictServerAspNetCoreHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is null)
{
return default;
}
// Preserve the host properties in the principal.
if (properties.Items.Count is not 0)
{
context.Principal.SetClaim(Claims.Private.HostProperties, properties.Items);
}
foreach (var parameter in properties.Parameters)
{
context.Parameters[parameter.Key] = parameter.Value switch

1
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs

@ -17,6 +17,7 @@ public static class OpenIddictServerDataProtectionConstants
public const string CodeChallengeMethod = ".code_challenge_method";
public const string DeviceCodeId = ".device_code_id";
public const string DeviceCodeLifetime = ".device_code_lifetime";
public const string HostProperties = ".host_properties";
public const string Expires = ".expires";
public const string IdentityTokenLifetime = ".identity_token_lifetime";
public const string InternalAuthorizationId = ".internal_authorization_id";

30
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs

@ -29,10 +29,12 @@ public class OpenIddictServerDataProtectionFormatter : IOpenIddictServerDataProt
// can be reused, well-known properties are manually mapped to their claims equivalents.
return principal
.SetAudiences(GetArrayProperty(properties, Properties.Audiences))
.SetPresenters(GetArrayProperty(properties, Properties.Presenters))
.SetResources(GetArrayProperty(properties, Properties.Resources))
.SetScopes(GetArrayProperty(properties, Properties.Scopes))
.SetClaims(Claims.Private.Audience, GetJsonProperty(properties, Properties.Audiences))
.SetClaims(Claims.Private.Presenter, GetJsonProperty(properties, Properties.Presenters))
.SetClaims(Claims.Private.Resource, GetJsonProperty(properties, Properties.Resources))
.SetClaims(Claims.Private.Scope, GetJsonProperty(properties, Properties.Scopes))
.SetClaim(Claims.Private.HostProperties, GetJsonProperty(properties, Properties.HostProperties))
.SetClaim(Claims.Private.AccessTokenLifetime, GetProperty(properties, Properties.AccessTokenLifetime))
.SetClaim(Claims.Private.AuthorizationCodeLifetime, GetProperty(properties, Properties.AuthorizationCodeLifetime))
@ -166,28 +168,15 @@ public class OpenIddictServerDataProtectionFormatter : IOpenIddictServerDataProt
static string? GetProperty(IReadOnlyDictionary<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? value : null;
static ImmutableArray<string> GetArrayProperty(IReadOnlyDictionary<string, string> properties, string name)
static JsonElement GetJsonProperty(IReadOnlyDictionary<string, string> properties, string name)
{
if (properties.TryGetValue(name, out var value))
{
using var document = JsonDocument.Parse(value);
var builder = ImmutableArray.CreateBuilder<string>(document.RootElement.GetArrayLength());
foreach (var element in document.RootElement.EnumerateArray())
{
var item = element.GetString();
if (string.IsNullOrEmpty(item))
{
continue;
}
builder.Add(item);
}
return builder.ToImmutable();
return document.RootElement.Clone();
}
return ImmutableArray.Create<string>();
return default;
}
}
@ -221,6 +210,7 @@ public class OpenIddictServerDataProtectionFormatter : IOpenIddictServerDataProt
SetProperty(properties, Properties.CodeChallenge, principal.GetClaim(Claims.Private.CodeChallenge));
SetProperty(properties, Properties.CodeChallengeMethod, principal.GetClaim(Claims.Private.CodeChallengeMethod));
SetProperty(properties, Properties.HostProperties, principal.GetClaim(Claims.Private.HostProperties));
SetProperty(properties, Properties.InternalAuthorizationId, principal.GetAuthorizationId());
SetProperty(properties, Properties.InternalTokenId, principal.GetTokenId());

33
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs

@ -5,6 +5,7 @@
*/
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Owin.Security.Infrastructure;
using static OpenIddict.Server.Owin.OpenIddictServerOwinConstants;
using Properties = OpenIddict.Server.Owin.OpenIddictServerOwinConstants.Properties;
@ -187,11 +188,10 @@ public class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddictServe
return null;
}
var properties = new AuthenticationProperties
{
ExpiresUtc = principal.GetExpirationDate(),
IssuedUtc = principal.GetCreationDate()
};
// Restore or create a new authentication properties collection and populate it.
var properties = CreateProperties(principal);
properties.ExpiresUtc = principal.GetExpirationDate();
properties.IssuedUtc = principal.GetCreationDate();
// Attach the tokens to allow any OWIN component (e.g a controller)
// to retrieve them (e.g to make an API request to another application).
@ -228,6 +228,29 @@ public class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddictServe
return new AuthenticationTicket((ClaimsIdentity) principal.Identity, properties);
}
static AuthenticationProperties CreateProperties(ClaimsPrincipal? principal)
{
// Note: the principal may be null if no value was extracted from the corresponding token.
if (principal is not null)
{
var value = principal.GetClaim(Claims.Private.HostProperties);
if (!string.IsNullOrEmpty(value))
{
var dictionary = new Dictionary<string, string?>(comparer: StringComparer.Ordinal);
using var document = JsonDocument.Parse(value);
foreach (var property in document.RootElement.EnumerateObject())
{
dictionary[property.Name] = property.Value.GetString();
}
return new AuthenticationProperties(dictionary);
}
}
return new AuthenticationProperties();
}
}
/// <inheritdoc/>

11
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
@ -317,7 +318,7 @@ public static partial class OpenIddictServerOwinHandlers
}
/// <summary>
/// Contains the logic responsible for resolving the additional sign-in parameters stored in the
/// Contains the logic responsible for resolving the additional challenge parameters stored in the
/// OWIN authentication properties specified by the application that triggered the sign-in operation.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
@ -416,12 +417,20 @@ public static partial class OpenIddictServerOwinHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is null)
{
return default;
}
// Preserve the host properties in the principal.
if (properties.Dictionary.Count is not 0)
{
context.Principal.SetClaim(Claims.Private.HostProperties, properties.Dictionary);
}
// Note: unlike ASP.NET Core, Owin's AuthenticationProperties doesn't offer a strongly-typed
// dictionary that allows flowing parameters while preserving their original types. To allow
// returning custom parameters, the OWIN host allows using AuthenticationProperties.Dictionary

1
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs

@ -18,6 +18,7 @@ public static class OpenIddictValidationDataProtectionConstants
public const string DeviceCodeId = ".device_code_id";
public const string DeviceCodeLifetime = ".device_code_lifetime";
public const string Expires = ".expires";
public const string HostProperties = ".host_properties";
public const string IdentityTokenLifetime = ".identity_token_lifetime";
public const string InternalAuthorizationId = ".internal_authorization_id";
public const string InternalTokenId = ".internal_token_id";

30
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs

@ -4,7 +4,6 @@
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using System.Security.Claims;
using System.Text.Json;
using Properties = OpenIddict.Validation.DataProtection.OpenIddictValidationDataProtectionConstants.Properties;
@ -27,10 +26,12 @@ public class OpenIddictValidationDataProtectionFormatter : IOpenIddictValidation
// can be reused, well-known properties are manually mapped to their claims equivalents.
return principal
.SetAudiences(GetArrayProperty(properties, Properties.Audiences))
.SetPresenters(GetArrayProperty(properties, Properties.Presenters))
.SetResources(GetArrayProperty(properties, Properties.Resources))
.SetScopes(GetArrayProperty(properties, Properties.Scopes))
.SetClaims(Claims.Private.Audience, GetJsonProperty(properties, Properties.Audiences))
.SetClaims(Claims.Private.Presenter, GetJsonProperty(properties, Properties.Presenters))
.SetClaims(Claims.Private.Resource, GetJsonProperty(properties, Properties.Resources))
.SetClaims(Claims.Private.Scope, GetJsonProperty(properties, Properties.Scopes))
.SetClaim(Claims.Private.HostProperties, GetJsonProperty(properties, Properties.HostProperties))
.SetClaim(Claims.Private.AccessTokenLifetime, GetProperty(properties, Properties.AccessTokenLifetime))
.SetClaim(Claims.Private.AuthorizationCodeLifetime, GetProperty(properties, Properties.AuthorizationCodeLifetime))
@ -164,28 +165,15 @@ public class OpenIddictValidationDataProtectionFormatter : IOpenIddictValidation
static string? GetProperty(IReadOnlyDictionary<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? value : null;
static ImmutableArray<string> GetArrayProperty(IReadOnlyDictionary<string, string> properties, string name)
static JsonElement GetJsonProperty(IReadOnlyDictionary<string, string> properties, string name)
{
if (properties.TryGetValue(name, out var value))
{
using var document = JsonDocument.Parse(value);
var builder = ImmutableArray.CreateBuilder<string>(document.RootElement.GetArrayLength());
foreach (var element in document.RootElement.EnumerateArray())
{
var item = element.GetString();
if (string.IsNullOrEmpty(item))
{
continue;
}
builder.Add(item);
}
return builder.ToImmutable();
return document.RootElement.Clone();
}
return ImmutableArray.Create<string>();
return default;
}
}
}

38
src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs

@ -7,7 +7,6 @@
using System.Collections.Immutable;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.IdentityModel.JsonWebTokens;
namespace OpenIddict.Validation;
@ -411,28 +410,11 @@ public static partial class OpenIddictValidationHandlers
// Top-level claims represented as arrays are split and mapped to multiple CLR claims
// to match the logic implemented by IdentityModel for JWT token deserialization.
case { ValueKind: JsonValueKind.Array } value:
foreach (var item in value.EnumerateArray())
{
identity.AddClaim(new Claim(
type : parameter.Key,
value : item.ToString()!,
valueType : GetClaimValueType(item),
issuer : issuer,
originalIssuer: issuer,
subject : identity));
}
identity.AddClaims(parameter.Key, value, issuer);
break;
// Note: JsonElement.ToString() returns string.Empty for JsonValueKind.Null and
// JsonValueKind.Undefined, which, unlike null strings, is a valid claim value.
case { ValueKind: _ } value:
identity.AddClaim(new Claim(
type : parameter.Key,
value : value.ToString()!,
valueType : GetClaimValueType(value),
issuer : issuer,
originalIssuer: issuer,
subject : identity));
identity.AddClaim(parameter.Key, value, issuer);
break;
}
}
@ -440,22 +422,6 @@ public static partial class OpenIddictValidationHandlers
context.Principal = new ClaimsPrincipal(identity);
return default;
static string GetClaimValueType(JsonElement element) => element.ValueKind switch
{
JsonValueKind.String => ClaimValueTypes.String,
JsonValueKind.True or JsonValueKind.False => ClaimValueTypes.Boolean,
JsonValueKind.Number when element.TryGetInt32(out _) => ClaimValueTypes.Integer32,
JsonValueKind.Number when element.TryGetInt64(out _) => ClaimValueTypes.Integer64,
JsonValueKind.Number when element.TryGetUInt32(out _) => ClaimValueTypes.UInteger32,
JsonValueKind.Number when element.TryGetUInt64(out _) => ClaimValueTypes.UInteger64,
JsonValueKind.Number when element.TryGetDouble(out _) => ClaimValueTypes.Double,
JsonValueKind.Null or JsonValueKind.Undefined => JsonClaimValueTypes.JsonNull,
JsonValueKind.Array => JsonClaimValueTypes.JsonArray,
JsonValueKind.Object or _ => JsonClaimValueTypes.Json
};
}
}
}

4440
test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs

File diff suppressed because it is too large

3
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

@ -862,8 +862,7 @@ public abstract partial class OpenIddictServerIntegrationTests
.SetTokenType(TokenTypeHints.AuthorizationCode)
.SetPresenters("Fabrikam")
.SetClaim(Claims.Subject, "Bob le Bricoleur")
.SetClaim(Claims.Private.CodeChallenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
.SetClaim(Claims.Private.CodeChallengeMethod, null);
.SetClaim(Claims.Private.CodeChallenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
return default;
});

Loading…
Cancel
Save