Browse Source

Introduce support for application types

pull/1878/head
Kévin Chalet 2 years ago
parent
commit
4af3f8dc6f
  1. 2
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  2. 21
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  3. 23
      src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs
  4. 20
      src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
  5. 6
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  6. 20
      src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs
  7. 136
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  8. 17
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs
  9. 9
      src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkApplicationConfiguration.cs
  10. 29
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs
  11. 17
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs
  12. 9
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreApplicationConfiguration.cs
  13. 29
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs
  14. 21
      src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs
  15. 29
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs
  16. 97
      src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs
  17. 32
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs

2
sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs

@ -179,6 +179,7 @@ namespace OpenIddict.Sandbox.AspNet.Server
{ {
await manager.CreateAsync(new OpenIddictApplicationDescriptor await manager.CreateAsync(new OpenIddictApplicationDescriptor
{ {
ApplicationType = ApplicationTypes.Web,
ClientId = "mvc", ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = ConsentTypes.Explicit, ConsentType = ConsentTypes.Explicit,
@ -215,6 +216,7 @@ namespace OpenIddict.Sandbox.AspNet.Server
{ {
await manager.CreateAsync(new OpenIddictApplicationDescriptor await manager.CreateAsync(new OpenIddictApplicationDescriptor
{ {
ApplicationType = ApplicationTypes.Native,
ClientId = "postman", ClientId = "postman",
ConsentType = ConsentTypes.Systematic, ConsentType = ConsentTypes.Systematic,
DisplayName = "Postman", DisplayName = "Postman",

21
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs

@ -30,6 +30,9 @@ public class Worker : IHostedService
{ {
await manager.CreateAsync(new OpenIddictApplicationDescriptor await manager.CreateAsync(new OpenIddictApplicationDescriptor
{ {
// Note: the application must be registered as a native application to force OpenIddict
// to apply a relaxed redirect_uri validation policy that allows specifying a random port.
ApplicationType = ApplicationTypes.Native,
ClientId = "console", ClientId = "console",
ConsentType = ConsentTypes.Systematic, ConsentType = ConsentTypes.Systematic,
DisplayName = "Console client application", DisplayName = "Console client application",
@ -39,17 +42,9 @@ public class Worker : IHostedService
}, },
RedirectUris = RedirectUris =
{ {
new Uri("http://localhost:49152/callback/login/local"), // Note: the port must not be explicitly specified as it is selected
new Uri("http://localhost:49153/callback/login/local"), // dynamically at runtime by the OpenIddict client system integration.
new Uri("http://localhost:49154/callback/login/local"), new Uri("http://localhost/callback/login/local")
new Uri("http://localhost:49155/callback/login/local"),
new Uri("http://localhost:49156/callback/login/local"),
new Uri("http://localhost:49157/callback/login/local"),
new Uri("http://localhost:49158/callback/login/local"),
new Uri("http://localhost:49159/callback/login/local"),
new Uri("http://localhost:49160/callback/login/local"),
new Uri("http://localhost:49161/callback/login/local"),
new Uri("http://localhost:49162/callback/login/local")
}, },
Permissions = Permissions =
{ {
@ -76,6 +71,7 @@ public class Worker : IHostedService
{ {
await manager.CreateAsync(new OpenIddictApplicationDescriptor await manager.CreateAsync(new OpenIddictApplicationDescriptor
{ {
ApplicationType = ApplicationTypes.Web,
ClientId = "mvc", ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = ConsentTypes.Explicit, ConsentType = ConsentTypes.Explicit,
@ -116,6 +112,7 @@ public class Worker : IHostedService
{ {
await manager.CreateAsync(new OpenIddictApplicationDescriptor await manager.CreateAsync(new OpenIddictApplicationDescriptor
{ {
ApplicationType = ApplicationTypes.Native,
ClientId = "winforms", ClientId = "winforms",
ConsentType = ConsentTypes.Systematic, ConsentType = ConsentTypes.Systematic,
DisplayName = "WinForms client application", DisplayName = "WinForms client application",
@ -150,6 +147,7 @@ public class Worker : IHostedService
{ {
await manager.CreateAsync(new OpenIddictApplicationDescriptor await manager.CreateAsync(new OpenIddictApplicationDescriptor
{ {
ApplicationType = ApplicationTypes.Native,
ClientId = "wpf", ClientId = "wpf",
ConsentType = ConsentTypes.Systematic, ConsentType = ConsentTypes.Systematic,
DisplayName = "WPF client application", DisplayName = "WPF client application",
@ -209,6 +207,7 @@ public class Worker : IHostedService
{ {
await manager.CreateAsync(new OpenIddictApplicationDescriptor await manager.CreateAsync(new OpenIddictApplicationDescriptor
{ {
ApplicationType = ApplicationTypes.Native,
ClientId = "postman", ClientId = "postman",
ConsentType = ConsentTypes.Systematic, ConsentType = ConsentTypes.Systematic,
DisplayName = "Postman", DisplayName = "Postman",

23
src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs

@ -1,4 +1,5 @@
using System.Globalization; using System.ComponentModel;
using System.Globalization;
using System.Text.Json; using System.Text.Json;
namespace OpenIddict.Abstractions; namespace OpenIddict.Abstractions;
@ -8,6 +9,11 @@ namespace OpenIddict.Abstractions;
/// </summary> /// </summary>
public class OpenIddictApplicationDescriptor public class OpenIddictApplicationDescriptor
{ {
/// <summary>
/// Gets or sets the application type associated with the application.
/// </summary>
public string? ApplicationType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the client identifier associated with the application. /// Gets or sets the client identifier associated with the application.
/// </summary> /// </summary>
@ -20,6 +26,11 @@ public class OpenIddictApplicationDescriptor
/// </summary> /// </summary>
public string? ClientSecret { get; set; } public string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets the client type associated with the application.
/// </summary>
public string? ClientType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the consent type associated with the application. /// Gets or sets the consent type associated with the application.
/// </summary> /// </summary>
@ -61,7 +72,13 @@ public class OpenIddictApplicationDescriptor
public HashSet<string> Requirements { get; } = new(StringComparer.Ordinal); public HashSet<string> Requirements { get; } = new(StringComparer.Ordinal);
/// <summary> /// <summary>
/// Gets or sets the application type associated with the application. /// Gets or sets the client type associated with the application.
/// </summary> /// </summary>
public string? Type { get; set; } [EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete($"This property was replaced by {nameof(ClientType)} and will be removed in a future version.", true)]
public string? Type
{
get => ClientType;
set => ClientType = value;
}
} }

20
src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs

@ -130,6 +130,17 @@ public interface IOpenIddictApplicationManager
IAsyncEnumerable<object> FindByRedirectUriAsync( IAsyncEnumerable<object> FindByRedirectUriAsync(
[StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken = default); [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the application type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application type of the application (by default, "web").
/// </returns>
ValueTask<string?> GetApplicationTypeAsync(object application, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Executes the specified query and returns the first element. /// Executes the specified query and returns the first element.
/// </summary> /// </summary>
@ -307,6 +318,15 @@ public interface IOpenIddictApplicationManager
/// </returns> /// </returns>
ValueTask<ImmutableArray<string>> GetRequirementsAsync(object application, CancellationToken cancellationToken = default); ValueTask<ImmutableArray<string>> GetRequirementsAsync(object application, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether a given application has the specified application type.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The expected application type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><see langword="true"/> if the application has the specified application type, <see langword="false"/> otherwise.</returns>
ValueTask<bool> HasApplicationTypeAsync(object application, string type, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Determines whether a given application has the specified client type. /// Determines whether a given application has the specified client type.
/// </summary> /// </summary>

6
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -24,6 +24,12 @@ public static class OpenIddictConstants
public const string RsaSsaPssSha512 = "PS512"; public const string RsaSsaPssSha512 = "PS512";
} }
public static class ApplicationTypes
{
public const string Native = "native";
public const string Web = "web";
}
public static class AuthenticationMethodReferences public static class AuthenticationMethodReferences
{ {
public const string Face = "face"; public const string Face = "face";

20
src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs

@ -95,6 +95,17 @@ public interface IOpenIddictApplicationStore<TApplication> where TApplication :
IAsyncEnumerable<TApplication> FindByRedirectUriAsync( IAsyncEnumerable<TApplication> FindByRedirectUriAsync(
[StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken); [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the application type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application type of the application (by default, "web").
/// </returns>
ValueTask<string?> GetApplicationTypeAsync(TApplication application, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Executes the specified query and returns the first element. /// Executes the specified query and returns the first element.
/// </summary> /// </summary>
@ -277,6 +288,15 @@ public interface IOpenIddictApplicationStore<TApplication> where TApplication :
Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query, Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken); TState state, CancellationToken cancellationToken);
/// <summary>
/// Sets the application type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The application type associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetApplicationTypeAsync(TApplication application, string? type, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Sets the client identifier associated with an application. /// Sets the client identifier associated with an application.
/// </summary> /// </summary>

136
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -407,6 +407,32 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
} }
} }
/// <summary>
/// Retrieves the application type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application type of the application (by default, "web").
/// </returns>
public virtual async ValueTask<string?> GetApplicationTypeAsync(
TApplication application, CancellationToken cancellationToken = default)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
var type = await Store.GetApplicationTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type))
{
return ApplicationTypes.Web;
}
return type;
}
/// <summary> /// <summary>
/// Executes the specified query and returns the first element. /// Executes the specified query and returns the first element.
/// </summary> /// </summary>
@ -744,6 +770,29 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
return Store.GetRequirementsAsync(application, cancellationToken); return Store.GetRequirementsAsync(application, cancellationToken);
} }
/// <summary>
/// Determines whether a given application has the specified application type.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The expected application type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><see langword="true"/> if the application has the specified application type, <see langword="false"/> otherwise.</returns>
public virtual async ValueTask<bool> HasApplicationTypeAsync(
TApplication application, string type, CancellationToken cancellationToken = default)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(type))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0209), nameof(type));
}
return string.Equals(await GetApplicationTypeAsync(application, cancellationToken), type, StringComparison.OrdinalIgnoreCase);
}
/// <summary> /// <summary>
/// Determines whether a given application has the specified client type. /// Determines whether a given application has the specified client type.
/// </summary> /// </summary>
@ -908,9 +957,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
throw new ArgumentNullException(nameof(descriptor)); throw new ArgumentNullException(nameof(descriptor));
} }
await Store.SetApplicationTypeAsync(application, descriptor.ApplicationType, cancellationToken);
await Store.SetClientIdAsync(application, descriptor.ClientId, cancellationToken); await Store.SetClientIdAsync(application, descriptor.ClientId, cancellationToken);
await Store.SetClientSecretAsync(application, descriptor.ClientSecret, cancellationToken); await Store.SetClientSecretAsync(application, descriptor.ClientSecret, cancellationToken);
await Store.SetClientTypeAsync(application, descriptor.Type, cancellationToken); await Store.SetClientTypeAsync(application, descriptor.ClientType, cancellationToken);
await Store.SetConsentTypeAsync(application, descriptor.ConsentType, cancellationToken); await Store.SetConsentTypeAsync(application, descriptor.ConsentType, cancellationToken);
await Store.SetDisplayNameAsync(application, descriptor.DisplayName, cancellationToken); await Store.SetDisplayNameAsync(application, descriptor.DisplayName, cancellationToken);
await Store.SetDisplayNamesAsync(application, descriptor.DisplayNames.ToImmutableDictionary(), cancellationToken); await Store.SetDisplayNamesAsync(application, descriptor.DisplayNames.ToImmutableDictionary(), cancellationToken);
@ -946,11 +996,12 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
descriptor.ApplicationType = await Store.GetApplicationTypeAsync(application, cancellationToken);
descriptor.ClientId = await Store.GetClientIdAsync(application, cancellationToken); descriptor.ClientId = await Store.GetClientIdAsync(application, cancellationToken);
descriptor.ClientSecret = await Store.GetClientSecretAsync(application, cancellationToken); descriptor.ClientSecret = await Store.GetClientSecretAsync(application, cancellationToken);
descriptor.ClientType = await Store.GetClientTypeAsync(application, cancellationToken);
descriptor.ConsentType = await Store.GetConsentTypeAsync(application, cancellationToken); descriptor.ConsentType = await Store.GetConsentTypeAsync(application, cancellationToken);
descriptor.DisplayName = await Store.GetDisplayNameAsync(application, cancellationToken); descriptor.DisplayName = await Store.GetDisplayNameAsync(application, cancellationToken);
descriptor.Type = await Store.GetClientTypeAsync(application, cancellationToken);
descriptor.Permissions.Clear(); descriptor.Permissions.Clear();
descriptor.Permissions.UnionWith(await Store.GetPermissionsAsync(application, cancellationToken)); descriptor.Permissions.UnionWith(await Store.GetPermissionsAsync(application, cancellationToken));
descriptor.Requirements.Clear(); descriptor.Requirements.Clear();
@ -1316,11 +1367,45 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
foreach (var candidate in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken)) foreach (var candidate in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
{ {
// Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison". // Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison",
// unless the application was explicitly registered as a native application. In this case, a second
// pass using the relaxed comparison method is performed if the application is a native application.
if (string.Equals(candidate, uri, StringComparison.Ordinal)) if (string.Equals(candidate, uri, StringComparison.Ordinal))
{ {
return true; return true;
} }
if (await HasApplicationTypeAsync(application, ApplicationTypes.Native, cancellationToken) &&
Uri.TryCreate(uri, UriKind.Absolute, out Uri? left) &&
Uri.TryCreate(candidate, UriKind.Absolute, out Uri? right) &&
// Only apply the relaxed comparison if the URI specified by the client uses a
// non-default port and if the value resolved from the database doesn't specify one.
!left.IsDefaultPort && right.IsDefaultPort &&
// The relaxed policy only applies to loopback URIs.
left.IsLoopback && right.IsLoopback &&
// The relaxed policy only applies to HTTP and HTTPS URIs.
//
// Note: the scheme case is deliberately ignored here as it is always
// normalized to a lowercase value by the Uri.TryCreate() API, which
// would prevent performing a case-sensitive comparison anyway.
((string.Equals(left.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
string.Equals(right.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)) ||
(string.Equals(left.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
string.Equals(right.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))) &&
string.Equals(left.UserInfo, right.UserInfo, StringComparison.Ordinal) &&
// Note: the host case is deliberately ignored here as it is always
// normalized to a lowercase value by the Uri.TryCreate() API, which
// would prevent performing a case-sensitive comparison anyway.
string.Equals(left.Host, right.Host, StringComparison.OrdinalIgnoreCase) &&
string.Equals(left.AbsolutePath, right.AbsolutePath, StringComparison.Ordinal) &&
string.Equals(left.Query, right.Query, StringComparison.Ordinal) &&
string.Equals(left.Fragment, right.Fragment, StringComparison.Ordinal))
{
return true;
}
} }
Logger.LogInformation(SR.GetResourceString(SR.ID6202), uri, await GetClientIdAsync(application, cancellationToken)); Logger.LogInformation(SR.GetResourceString(SR.ID6202), uri, await GetClientIdAsync(application, cancellationToken));
@ -1353,12 +1438,47 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
foreach (var candidate in await Store.GetRedirectUrisAsync(application, cancellationToken)) foreach (var candidate in await Store.GetRedirectUrisAsync(application, cancellationToken))
{ {
// Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison". // Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison",
// unless the application was explicitly registered as a native application. In this case, a second
// pass using the relaxed comparison method is performed if the application is a native application.
//
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information. // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
if (string.Equals(candidate, uri, StringComparison.Ordinal)) if (string.Equals(candidate, uri, StringComparison.Ordinal))
{ {
return true; return true;
} }
if (await HasApplicationTypeAsync(application, ApplicationTypes.Native, cancellationToken) &&
Uri.TryCreate(uri, UriKind.Absolute, out Uri? left) &&
Uri.TryCreate(candidate, UriKind.Absolute, out Uri? right) &&
// Only apply the relaxed comparison if the URI specified by the client uses a
// non-default port and if the value resolved from the database doesn't specify one.
!left.IsDefaultPort && right.IsDefaultPort &&
// The relaxed policy only applies to loopback URIs.
left.IsLoopback && right.IsLoopback &&
// The relaxed policy only applies to HTTP and HTTPS URIs.
//
// Note: the scheme case is deliberately ignored here as it is always
// normalized to a lowercase value by the Uri.TryCreate() API, which
// would prevent performing a case-sensitive comparison anyway.
((string.Equals(left.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
string.Equals(right.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)) ||
(string.Equals(left.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
string.Equals(right.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))) &&
string.Equals(left.UserInfo, right.UserInfo, StringComparison.Ordinal) &&
// Note: the host case is deliberately ignored here as it is always
// normalized to a lowercase value by the Uri.TryCreate() API, which
// would prevent performing a case-sensitive comparison anyway.
string.Equals(left.Host, right.Host, StringComparison.OrdinalIgnoreCase) &&
string.Equals(left.AbsolutePath, right.AbsolutePath, StringComparison.Ordinal) &&
string.Equals(left.Query, right.Query, StringComparison.Ordinal) &&
string.Equals(left.Fragment, right.Fragment, StringComparison.Ordinal))
{
return true;
}
} }
Logger.LogInformation(SR.GetResourceString(SR.ID6162), uri, await GetClientIdAsync(application, cancellationToken)); Logger.LogInformation(SR.GetResourceString(SR.ID6162), uri, await GetClientIdAsync(application, cancellationToken));
@ -1579,6 +1699,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
IAsyncEnumerable<object> IOpenIddictApplicationManager.FindByRedirectUriAsync([StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken) IAsyncEnumerable<object> IOpenIddictApplicationManager.FindByRedirectUriAsync([StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken)
=> FindByRedirectUriAsync(uri, cancellationToken); => FindByRedirectUriAsync(uri, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictApplicationManager.GetApplicationTypeAsync(object application, CancellationToken cancellationToken)
=> GetApplicationTypeAsync((TApplication) application, cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
ValueTask<TResult?> IOpenIddictApplicationManager.GetAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken) where TResult : default ValueTask<TResult?> IOpenIddictApplicationManager.GetAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken) where TResult : default
=> GetAsync(query, cancellationToken); => GetAsync(query, cancellationToken);
@ -1639,6 +1763,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ValueTask<ImmutableArray<string>> IOpenIddictApplicationManager.GetRequirementsAsync(object application, CancellationToken cancellationToken) ValueTask<ImmutableArray<string>> IOpenIddictApplicationManager.GetRequirementsAsync(object application, CancellationToken cancellationToken)
=> GetRequirementsAsync((TApplication) application, cancellationToken); => GetRequirementsAsync((TApplication) application, cancellationToken);
/// <inheritdoc/>
ValueTask<bool> IOpenIddictApplicationManager.HasApplicationTypeAsync(object application, string type, CancellationToken cancellationToken)
=> HasApplicationTypeAsync((TApplication) application, type, cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
ValueTask<bool> IOpenIddictApplicationManager.HasClientTypeAsync(object application, string type, CancellationToken cancellationToken) ValueTask<bool> IOpenIddictApplicationManager.HasClientTypeAsync(object application, string type, CancellationToken cancellationToken)
=> HasClientTypeAsync((TApplication) application, type, cancellationToken); => HasClientTypeAsync((TApplication) application, type, cancellationToken);

17
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs

@ -24,12 +24,17 @@ public class OpenIddictEntityFrameworkApplication : OpenIddictEntityFrameworkApp
/// <summary> /// <summary>
/// Represents an OpenIddict application. /// Represents an OpenIddict application.
/// </summary> /// </summary>
[DebuggerDisplay("Id = {Id.ToString(),nq} ; ClientId = {ClientId,nq} ; Type = {Type,nq}")] [DebuggerDisplay("Id = {Id.ToString(),nq} ; ClientId = {ClientId,nq} ; ClientType = {ClientType,nq}")]
public class OpenIddictEntityFrameworkApplication<TKey, TAuthorization, TToken> public class OpenIddictEntityFrameworkApplication<TKey, TAuthorization, TToken>
where TKey : notnull, IEquatable<TKey> where TKey : notnull, IEquatable<TKey>
where TAuthorization : class where TAuthorization : class
where TToken : class where TToken : class
{ {
/// <summary>
/// Gets or sets the application type associated with the current application.
/// </summary>
public virtual string? ApplicationType { get; set; }
/// <summary> /// <summary>
/// Gets the list of the authorizations associated with this application. /// Gets the list of the authorizations associated with this application.
/// </summary> /// </summary>
@ -47,6 +52,11 @@ public class OpenIddictEntityFrameworkApplication<TKey, TAuthorization, TToken>
/// </summary> /// </summary>
public virtual string? ClientSecret { get; set; } public virtual string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets the client type associated with the current application.
/// </summary>
public virtual string? ClientType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the concurrency token. /// Gets or sets the concurrency token.
/// </summary> /// </summary>
@ -114,9 +124,4 @@ public class OpenIddictEntityFrameworkApplication<TKey, TAuthorization, TToken>
/// Gets the list of the tokens associated with this application. /// Gets the list of the tokens associated with this application.
/// </summary> /// </summary>
public virtual ICollection<TToken> Tokens { get; } = new HashSet<TToken>(); public virtual ICollection<TToken> Tokens { get; } = new HashSet<TToken>();
/// <summary>
/// Gets or sets the application type associated with the current application.
/// </summary>
public virtual string? Type { get; set; }
} }

9
src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkApplicationConfiguration.cs

@ -34,6 +34,9 @@ public sealed class OpenIddictEntityFrameworkApplicationConfiguration<TApplicati
HasKey(application => application.Id); HasKey(application => application.Id);
Property(application => application.ApplicationType)
.HasMaxLength(50);
Property(application => application.ClientId) Property(application => application.ClientId)
.HasMaxLength(100) .HasMaxLength(100)
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute .HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute
@ -41,6 +44,9 @@ public sealed class OpenIddictEntityFrameworkApplicationConfiguration<TApplicati
IsUnique = true IsUnique = true
})); }));
Property(application => application.ClientType)
.HasMaxLength(50);
Property(application => application.ConcurrencyToken) Property(application => application.ConcurrencyToken)
.HasMaxLength(50) .HasMaxLength(50)
.IsConcurrencyToken(); .IsConcurrencyToken();
@ -48,9 +54,6 @@ public sealed class OpenIddictEntityFrameworkApplicationConfiguration<TApplicati
Property(application => application.ConsentType) Property(application => application.ConsentType)
.HasMaxLength(50); .HasMaxLength(50);
Property(application => application.Type)
.HasMaxLength(50);
HasMany(application => application.Authorizations) HasMany(application => application.Authorizations)
.WithOptional(authorization => authorization.Application!) .WithOptional(authorization => authorization.Application!)
.Map(association => .Map(association =>

29
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs

@ -304,6 +304,17 @@ public class OpenIddictEntityFrameworkApplicationStore<TApplication, TAuthorizat
} }
} }
/// <inheritdoc/>
public virtual ValueTask<string?> GetApplicationTypeAsync(TApplication application, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
return new(application.ApplicationType);
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual async ValueTask<TResult?> GetAsync<TState, TResult>( public virtual async ValueTask<TResult?> GetAsync<TState, TResult>(
Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query, Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
@ -347,7 +358,7 @@ public class OpenIddictEntityFrameworkApplicationStore<TApplication, TAuthorizat
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
return new(application.Type); return new(application.ClientType);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -670,6 +681,20 @@ public class OpenIddictEntityFrameworkApplicationStore<TApplication, TAuthorizat
return query(Applications, state).AsAsyncEnumerable(cancellationToken); return query(Applications, state).AsAsyncEnumerable(cancellationToken);
} }
/// <inheritdoc/>
public virtual ValueTask SetApplicationTypeAsync(TApplication application,
string? type, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
application.ApplicationType = type;
return default;
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual ValueTask SetClientIdAsync(TApplication application, string? identifier, CancellationToken cancellationToken) public virtual ValueTask SetClientIdAsync(TApplication application, string? identifier, CancellationToken cancellationToken)
{ {
@ -704,7 +729,7 @@ public class OpenIddictEntityFrameworkApplicationStore<TApplication, TAuthorizat
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
application.Type = type; application.ClientType = type;
return default; return default;
} }

17
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs

@ -32,12 +32,17 @@ public class OpenIddictEntityFrameworkCoreApplication<TKey> : OpenIddictEntityFr
/// <summary> /// <summary>
/// Represents an OpenIddict application. /// Represents an OpenIddict application.
/// </summary> /// </summary>
[DebuggerDisplay("Id = {Id.ToString(),nq} ; ClientId = {ClientId,nq} ; Type = {Type,nq}")] [DebuggerDisplay("Id = {Id.ToString(),nq} ; ClientId = {ClientId,nq} ; ClientType = {ClientType,nq}")]
public class OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TToken> public class OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TToken>
where TKey : notnull, IEquatable<TKey> where TKey : notnull, IEquatable<TKey>
where TAuthorization : class where TAuthorization : class
where TToken : class where TToken : class
{ {
/// <summary>
/// Gets or sets the application type associated with the current application.
/// </summary>
public virtual string? ApplicationType { get; set; }
/// <summary> /// <summary>
/// Gets the list of the authorizations associated with this application. /// Gets the list of the authorizations associated with this application.
/// </summary> /// </summary>
@ -55,6 +60,11 @@ public class OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TTok
/// </summary> /// </summary>
public virtual string? ClientSecret { get; set; } public virtual string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets the client type associated with the current application.
/// </summary>
public virtual string? ClientType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the concurrency token. /// Gets or sets the concurrency token.
/// </summary> /// </summary>
@ -122,9 +132,4 @@ public class OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TTok
/// Gets the list of the tokens associated with this application. /// Gets the list of the tokens associated with this application.
/// </summary> /// </summary>
public virtual ICollection<TToken> Tokens { get; } = new HashSet<TToken>(); public virtual ICollection<TToken> Tokens { get; } = new HashSet<TToken>();
/// <summary>
/// Gets or sets the application type associated with the current application.
/// </summary>
public virtual string? Type { get; set; }
} }

9
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreApplicationConfiguration.cs

@ -37,6 +37,9 @@ public sealed class OpenIddictEntityFrameworkCoreApplicationConfiguration<TAppli
builder.HasKey(application => application.Id); builder.HasKey(application => application.Id);
builder.Property(application => application.ApplicationType)
.HasMaxLength(50);
// Warning: the non-generic overlord is deliberately used to work around // Warning: the non-generic overlord is deliberately used to work around
// a breaking change introduced in Entity Framework Core 3.x (where a // a breaking change introduced in Entity Framework Core 3.x (where a
// generic entity type builder is now returned by the HasIndex() method). // generic entity type builder is now returned by the HasIndex() method).
@ -46,6 +49,9 @@ public sealed class OpenIddictEntityFrameworkCoreApplicationConfiguration<TAppli
builder.Property(application => application.ClientId) builder.Property(application => application.ClientId)
.HasMaxLength(100); .HasMaxLength(100);
builder.Property(application => application.ClientType)
.HasMaxLength(50);
builder.Property(application => application.ConcurrencyToken) builder.Property(application => application.ConcurrencyToken)
.HasMaxLength(50) .HasMaxLength(50)
.IsConcurrencyToken(); .IsConcurrencyToken();
@ -56,9 +62,6 @@ public sealed class OpenIddictEntityFrameworkCoreApplicationConfiguration<TAppli
builder.Property(application => application.Id) builder.Property(application => application.Id)
.ValueGeneratedOnAdd(); .ValueGeneratedOnAdd();
builder.Property(application => application.Type)
.HasMaxLength(50);
builder.HasMany(application => application.Authorizations) builder.HasMany(application => application.Authorizations)
.WithOne(authorization => authorization.Application!) .WithOne(authorization => authorization.Application!)
.HasForeignKey(nameof(OpenIddictEntityFrameworkCoreAuthorization.Application) + .HasForeignKey(nameof(OpenIddictEntityFrameworkCoreAuthorization.Application) +

29
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs

@ -346,6 +346,17 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<TApplication, TAuthor
} }
} }
/// <inheritdoc/>
public virtual ValueTask<string?> GetApplicationTypeAsync(TApplication application, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
return new(application.ApplicationType);
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual async ValueTask<TResult?> GetAsync<TState, TResult>( public virtual async ValueTask<TResult?> GetAsync<TState, TResult>(
Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query, Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
@ -389,7 +400,7 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<TApplication, TAuthor
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
return new(application.Type); return new(application.ClientType);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -711,6 +722,20 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<TApplication, TAuthor
return query(Applications.AsTracking(), state).AsAsyncEnumerable(cancellationToken); return query(Applications.AsTracking(), state).AsAsyncEnumerable(cancellationToken);
} }
/// <inheritdoc/>
public virtual ValueTask SetApplicationTypeAsync(TApplication application,
string? type, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
application.ApplicationType = type;
return default;
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual ValueTask SetClientIdAsync(TApplication application, string? identifier, CancellationToken cancellationToken) public virtual ValueTask SetClientIdAsync(TApplication application, string? identifier, CancellationToken cancellationToken)
{ {
@ -745,7 +770,7 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<TApplication, TAuthor
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
application.Type = type; application.ClientType = type;
return default; return default;
} }

21
src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs

@ -12,9 +12,15 @@ namespace OpenIddict.MongoDb.Models;
/// <summary> /// <summary>
/// Represents an OpenIddict application. /// Represents an OpenIddict application.
/// </summary> /// </summary>
[DebuggerDisplay("Id = {Id.ToString(),nq} ; ClientId = {ClientId,nq} ; Type = {Type,nq}")] [DebuggerDisplay("Id = {Id.ToString(),nq} ; ClientId = {ClientId,nq} ; ClientType = {ClientType,nq}")]
public class OpenIddictMongoDbApplication public class OpenIddictMongoDbApplication
{ {
/// <summary>
/// Gets or sets the application type associated with the current application.
/// </summary>
[BsonElement("application_type"), BsonIgnoreIfNull]
public virtual string? ApplicationType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the client identifier associated with the current application. /// Gets or sets the client identifier associated with the current application.
/// </summary> /// </summary>
@ -29,6 +35,12 @@ public class OpenIddictMongoDbApplication
[BsonElement("client_secret"), BsonIgnoreIfNull] [BsonElement("client_secret"), BsonIgnoreIfNull]
public virtual string? ClientSecret { get; set; } public virtual string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets the client type associated with the current application.
/// </summary>
[BsonElement("client_type"), BsonIgnoreIfNull]
public virtual string? ClientType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the concurrency token. /// Gets or sets the concurrency token.
/// </summary> /// </summary>
@ -89,11 +101,4 @@ public class OpenIddictMongoDbApplication
/// </summary> /// </summary>
[BsonElement("requirements"), BsonIgnoreIfNull] [BsonElement("requirements"), BsonIgnoreIfNull]
public virtual IReadOnlyList<string>? Requirements { get; set; } = ImmutableList.Create<string>(); public virtual IReadOnlyList<string>? Requirements { get; set; } = ImmutableList.Create<string>();
/// <summary>
/// Gets or sets the application type
/// associated with the current application.
/// </summary>
[BsonElement("type"), BsonIgnoreIfNull]
public virtual string? Type { get; set; }
} }

29
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs

@ -184,6 +184,17 @@ public class OpenIddictMongoDbApplicationStore<TApplication> : IOpenIddictApplic
} }
} }
/// <inheritdoc/>
public virtual ValueTask<string?> GetApplicationTypeAsync(TApplication application, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
return new(application.ApplicationType);
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual async ValueTask<TResult?> GetAsync<TState, TResult>( public virtual async ValueTask<TResult?> GetAsync<TState, TResult>(
Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query, Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
@ -230,7 +241,7 @@ public class OpenIddictMongoDbApplicationStore<TApplication> : IOpenIddictApplic
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
return new(application.Type); return new(application.ClientType);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -439,6 +450,20 @@ public class OpenIddictMongoDbApplicationStore<TApplication> : IOpenIddictApplic
} }
} }
/// <inheritdoc/>
public virtual ValueTask SetApplicationTypeAsync(TApplication application,
string? type, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
application.ApplicationType = type;
return default;
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual ValueTask SetClientIdAsync(TApplication application, public virtual ValueTask SetClientIdAsync(TApplication application,
string? identifier, CancellationToken cancellationToken) string? identifier, CancellationToken cancellationToken)
@ -476,7 +501,7 @@ public class OpenIddictMongoDbApplicationStore<TApplication> : IOpenIddictApplic
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
application.Type = type; application.ClientType = type;
return default; return default;
} }

97
src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs

@ -394,6 +394,10 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateLogoutRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateLogoutRequestContext>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
// Note: support for the client_id parameter was only added in the second draft of the
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout specification
// and is optional. As such, the client identifier is only validated if it was specified.
.AddFilter<RequireClientIdParameter>()
.UseScopedHandler<ValidateClientId>() .UseScopedHandler<ValidateClientId>()
.SetOrder(ValidatePostLogoutRedirectUriParameter.Descriptor.Order + 1_000) .SetOrder(ValidatePostLogoutRedirectUriParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
@ -407,13 +411,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// Note: support for the client_id parameter was only added in the second draft of the Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout specification
// and is optional. As such, the client identifier is only validated if it was specified.
if (string.IsNullOrEmpty(context.ClientId))
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId); var application = await _applicationManager.FindByClientIdAsync(context.ClientId);
if (application is null) if (application is null)
@ -521,13 +519,51 @@ public static partial class OpenIddictServerHandlers
await foreach (var application in _applicationManager.FindByPostLogoutRedirectUriAsync(uri)) await foreach (var application in _applicationManager.FindByPostLogoutRedirectUriAsync(uri))
{ {
if (context.Options.IgnoreEndpointPermissions || if (!context.Options.IgnoreEndpointPermissions &&
await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Logout)) !await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Logout))
{
continue;
}
if (await _applicationManager.ValidatePostLogoutRedirectUriAsync(application, uri))
{ {
return true; return true;
} }
} }
// If the specified URI is an HTTP/HTTPS URI, points to the local host and doesn't use the
// default port, make a second pass to determine whether a native application allowed to use
// a relaxed post_logout_redirect_uri comparison policy has the specified URI attached.
if (Uri.TryCreate(uri, UriKind.Absolute, out Uri? value) &&
// Only apply the relaxed comparison if the URI specified by the client uses a non-default port.
!value.IsDefaultPort &&
// The relaxed policy only applies to loopback URIs.
value.IsLoopback &&
// The relaxed policy only applies to HTTP and HTTPS URIs.
//
// Note: the scheme case is deliberately ignored here as it is always
// normalized to a lowercase value by the Uri.TryCreate() API, which
// would prevent performing a case-sensitive comparison anyway.
(string.Equals(value.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
string.Equals(value.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)))
{
await foreach (var application in _applicationManager.FindByPostLogoutRedirectUriAsync(
uri: new UriBuilder(value) { Port = -1 }.Uri.AbsoluteUri))
{
if (!context.Options.IgnoreEndpointPermissions &&
!await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Logout))
{
continue;
}
if (await _applicationManager.HasApplicationTypeAsync(application, ApplicationTypes.Native) &&
await _applicationManager.ValidatePostLogoutRedirectUriAsync(application, uri))
{
return true;
}
}
}
return false; return false;
} }
} }
@ -553,6 +589,13 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateLogoutRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateLogoutRequestContext>()
.AddFilter<RequireEndpointPermissionsEnabled>() .AddFilter<RequireEndpointPermissionsEnabled>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
// Note: support for the client_id parameter was only added in the second draft of the
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout specification
// and is optional. As such, the client permissions are only validated if it was specified.
//
// Note: if only post_logout_redirect_uri was specified, client permissions are expected to be
// enforced by the ValidateClientPostLogoutRedirectUri handler when finding matching clients.
.AddFilter<RequireClientIdParameter>()
.UseScopedHandler<ValidateEndpointPermissions>() .UseScopedHandler<ValidateEndpointPermissions>()
.SetOrder(ValidateClientPostLogoutRedirectUri.Descriptor.Order + 1_000) .SetOrder(ValidateClientPostLogoutRedirectUri.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
@ -566,15 +609,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// Note: support for the client_id parameter was only added in the second draft of the Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout specification
// and is optional. As such, the client permissions are only validated if it was specified.
// If only post_logout_redirect_uri was specified, client permissions are expected to be
// enforced by the ValidateClientPostLogoutRedirectUri handler when finding matching clients.
if (string.IsNullOrEmpty(context.ClientId))
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
@ -757,14 +792,28 @@ public static partial class OpenIddictServerHandlers
async ValueTask<bool> ValidateAuthorizedParty(ClaimsPrincipal principal, async ValueTask<bool> ValidateAuthorizedParty(ClaimsPrincipal principal,
[StringSyntax(StringSyntaxAttribute.Uri)] string uri) [StringSyntax(StringSyntaxAttribute.Uri)] string uri)
{ {
// To be considered valid, one of the clients matching the specified post_logout_redirect_uri // To be considered valid, the specified post_logout_redirect_uri must
// must be listed either as an audience or as a presenter in the identity token hint. // be considered valid for one of the listed audiences/presenters.
await foreach (var application in _applicationManager.FindByPostLogoutRedirectUriAsync(uri)) var identifiers = new HashSet<string>(StringComparer.Ordinal);
identifiers.UnionWith(principal.GetAudiences());
identifiers.UnionWith(principal.GetPresenters());
foreach (var identifier in identifiers)
{ {
var identifier = await _applicationManager.GetClientIdAsync(application); var application = await _applicationManager.FindByClientIdAsync(identifier);
if (!string.IsNullOrEmpty(identifier) && (principal.HasAudience(identifier) || if (application is null)
principal.HasPresenter(identifier))) {
continue;
}
if (!context.Options.IgnoreEndpointPermissions &&
!await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Logout))
{
continue;
}
if (await _applicationManager.ValidatePostLogoutRedirectUriAsync(application, uri))
{ {
return true; return true;
} }

32
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs

@ -364,6 +364,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasPermissionAsync(applications[2], Permissions.Endpoints.Logout, It.IsAny<CancellationToken>())) mock.Setup(manager => manager.HasPermissionAsync(applications[2], Permissions.Endpoints.Logout, It.IsAny<CancellationToken>()))
.ReturnsAsync(false); .ReturnsAsync(false);
mock.Setup(manager => manager.ValidatePostLogoutRedirectUriAsync(applications[1], "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}); });
await using var server = await CreateServerAsync(options => await using var server = await CreateServerAsync(options =>
@ -561,19 +564,34 @@ public abstract partial class OpenIddictServerIntegrationTests
public async Task ValidateLogoutRequest_IdentityTokenHintCausesAnErrorWhenInferredCallerIsNotAuthorized() public async Task ValidateLogoutRequest_IdentityTokenHintCausesAnErrorWhenInferredCallerIsNotAuthorized()
{ {
// Arrange // Arrange
var application = new OpenIddictApplication(); var applications = new[]
{
new OpenIddictApplication(),
new OpenIddictApplication()
};
var manager = CreateApplicationManager(mock => var manager = CreateApplicationManager(mock =>
{ {
mock.Setup(manager => manager.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>())) mock.Setup(manager => manager.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.Returns(new[] { application }.ToAsyncEnumerable()); .Returns(new[] { applications[0] }.ToAsyncEnumerable());
mock.Setup(manager => manager.HasPermissionAsync(application, mock.Setup(manager => manager.HasPermissionAsync(applications[0], Permissions.Endpoints.Logout, It.IsAny<CancellationToken>()))
Permissions.Endpoints.Logout, It.IsAny<CancellationToken>())) .ReturnsAsync(true);
mock.Setup(manager => manager.ValidatePostLogoutRedirectUriAsync(applications[0], "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
mock.Setup(manager => manager.GetClientIdAsync(application, It.IsAny<CancellationToken>())) mock.Setup(manager => manager.GetClientIdAsync(applications[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("Fabrikam"); .ReturnsAsync("Fabrikam");
mock.Setup(manager => manager.FindByClientIdAsync("Contoso", It.IsAny<CancellationToken>()))
.ReturnsAsync(applications[1]);
mock.Setup(manager => manager.HasPermissionAsync(applications[1], Permissions.Endpoints.Logout, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.ValidatePostLogoutRedirectUriAsync(applications[1], "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
}); });
await using var server = await CreateServerAsync(options => await using var server = await CreateServerAsync(options =>
@ -615,7 +633,9 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.GetResourceString(SR.ID2141), response.ErrorDescription); Assert.Equal(SR.GetResourceString(SR.ID2141), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2141), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2141), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()), Times.AtLeast(2)); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Contoso", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.ValidatePostLogoutRedirectUriAsync(applications[0], "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.ValidatePostLogoutRedirectUriAsync(applications[1], "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact] [Fact]

Loading…
Cancel
Save