diff --git a/Directory.Packages.props b/Directory.Packages.props index f807e47d456..9e7925236de 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -58,12 +58,12 @@ - - - - - - + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdServerConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdServerConfiguration.cs index 3349c976b37..b0169cb5d1e 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdServerConfiguration.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdServerConfiguration.cs @@ -83,7 +83,7 @@ public void Configure(OpenIddictServerOptions options) if (settings.LogoutEndpointPath.HasValue) { - options.LogoutEndpointUris.Add(new Uri( + options.EndSessionEndpointUris.Add(new Uri( settings.LogoutEndpointPath.ToUriComponent()[1..], UriKind.Relative)); } @@ -95,7 +95,7 @@ public void Configure(OpenIddictServerOptions options) if (settings.UserinfoEndpointPath.HasValue) { - options.UserinfoEndpointUris.Add(new Uri( + options.UserInfoEndpointUris.Add(new Uri( settings.UserinfoEndpointPath.ToUriComponent()[1..], UriKind.Relative)); } @@ -195,18 +195,18 @@ public void Configure(OpenIddictServerDataProtectionOptions options) public void Configure(string name, OpenIddictServerAspNetCoreOptions options) { - // Note: the OpenID module handles the authorization, logout, token and userinfo requests + // Note: the OpenID module handles the authorization, end session, token and userinfo requests // in its dedicated ASP.NET Core MVC controller, which requires enabling the pass-through mode. options.EnableAuthorizationEndpointPassthrough = true; - options.EnableLogoutEndpointPassthrough = true; + options.EnableEndSessionEndpointPassthrough = true; options.EnableTokenEndpointPassthrough = true; - options.EnableUserinfoEndpointPassthrough = true; + options.EnableUserInfoEndpointPassthrough = true; - // Note: caching is enabled for both authorization and logout requests to allow sending - // large POST authorization and logout requests, but can be programmatically disabled, as the - // authorization and logout views support flowing the entire payload and not just the request_id. + // Note: caching is enabled for both authorization and end session requests to allow sending + // large POST authorization and end session requests, but can be programmatically disabled, as the + // authorization and end session views support flowing the entire payload and not just the request_id. options.EnableAuthorizationRequestCaching = true; - options.EnableLogoutRequestCaching = true; + options.EnableEndSessionRequestCaching = true; // Note: error pass-through is enabled to allow the actions of the MVC authorization controller // to handle the errors returned by the interactive endpoints without relying on the generic diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/AccessController.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/AccessController.cs index ad0a9f9995e..da3a1da323a 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/AccessController.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/AccessController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using OrchardCore.Environment.Shell; @@ -64,7 +65,7 @@ public async Task Authorize() // Retrieve the claims stored in the authentication cookie. // If they can't be extracted, redirect the user to the login page. var result = await HttpContext.AuthenticateAsync(); - if (result == null || !result.Succeeded || request.HasPrompt(Prompts.Login)) + if (result == null || !result.Succeeded || request.HasPromptValue(PromptValues.Login)) { return RedirectToLoginPage(request); } @@ -99,7 +100,7 @@ public async Task Authorize() case ConsentTypes.Implicit: case ConsentTypes.External when authorizations.Count > 0: - case ConsentTypes.Explicit when authorizations.Count > 0 && !request.HasPrompt(Prompts.Consent): + case ConsentTypes.Explicit when authorizations.Count > 0 && !request.HasPromptValue(PromptValues.Consent): var identity = new ClaimsIdentity(result.Principal.Claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); identity.AddClaim(new Claim(OpenIdConstants.Claims.EntityType, OpenIdConstants.EntityTypes.User)); @@ -123,7 +124,7 @@ public async Task Authorize() return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - case ConsentTypes.Explicit when request.HasPrompt(Prompts.None): + case ConsentTypes.Explicit when request.HasPromptValue(PromptValues.None): return Forbid(new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, @@ -144,7 +145,7 @@ IActionResult RedirectToLoginPage(OpenIddictRequest request) { // If the client application requested promptless authentication, // return an error indicating that the user is not logged in. - if (request.HasPrompt(Prompts.None)) + if (request.HasPromptValue(PromptValues.None)) { return Forbid(new AuthenticationProperties(new Dictionary { @@ -155,9 +156,15 @@ IActionResult RedirectToLoginPage(OpenIddictRequest request) string GetRedirectUrl() { - // Override the prompt parameter to prevent infinite authentication/authorization loops. - var parameters = Request.Query.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - parameters[Parameters.Prompt] = "continue"; + // To avoid endless login -> authorization redirects, the prompt=login flag + // is removed from the authorization request payload before redirecting the user. + var prompt = string.Join(" ", request.GetPromptValues().Remove(PromptValues.Login)); + + var parameters = Request.HasFormContentType ? + Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() : + Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList(); + + parameters.Add(new(Parameters.Prompt, new StringValues(prompt))); return Request.PathBase + Request.Path + QueryString.Create(parameters); } diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/ApplicationController.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/ApplicationController.cs index 0121144cbd6..2a92ee03145 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/ApplicationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/ApplicationController.cs @@ -232,7 +232,7 @@ await HasPermissionAsync(OpenIddictConstants.Permissions.ResponseTypes.Token)), AllowPasswordFlow = await HasPermissionAsync(OpenIddictConstants.Permissions.GrantTypes.Password), AllowRefreshTokenFlow = await HasPermissionAsync(OpenIddictConstants.Permissions.GrantTypes.RefreshToken), - AllowLogoutEndpoint = await HasPermissionAsync(OpenIddictConstants.Permissions.Endpoints.Logout), + AllowLogoutEndpoint = await HasPermissionAsync(OpenIddictConstants.Permissions.Endpoints.EndSession), AllowIntrospectionEndpoint = await HasPermissionAsync(OpenIddictConstants.Permissions.Endpoints.Introspection), AllowRevocationEndpoint = await HasPermissionAsync(OpenIddictConstants.Permissions.Endpoints.Revocation), ClientId = await _applicationManager.GetClientIdAsync(application), diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/OpenIdApplicationSettings.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/OpenIdApplicationSettings.cs index 8b50941c773..fbc6a439aa5 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/OpenIdApplicationSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/OpenIdApplicationSettings.cs @@ -57,11 +57,11 @@ public static async Task UpdateDescriptorFromSettings(this IOpenIdApplicationMan if (model.AllowLogoutEndpoint) { - descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Logout); + descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.EndSession); } else { - descriptor.Permissions.Remove(OpenIddictConstants.Permissions.Endpoints.Logout); + descriptor.Permissions.Remove(OpenIddictConstants.Permissions.Endpoints.EndSession); } if (model.AllowAuthorizationCodeFlow || model.AllowHybridFlow) diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/Application/Create.cshtml b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/Application/Create.cshtml index 271d9bf756c..e2ff714c632 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/Application/Create.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/Application/Create.cshtml @@ -119,17 +119,17 @@
- +
- +
- @T["Space delimited list of logout redirect URIs."] + @T["Space delimited list of post-logout redirect URIs."]
diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/Application/Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/Application/Edit.cshtml index 3b0e85c00ea..36521408b0f 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/Application/Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/Application/Edit.cshtml @@ -131,17 +131,17 @@
- +
- +
- @T["Space delimited list of logout redirect URIs."] + @T["Space delimited list of post-logout redirect URIs."]
diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdServerSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdServerSettings.Edit.cshtml index e76114eef71..e4ed393b8ef 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdServerSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdServerSettings.Edit.cshtml @@ -25,7 +25,7 @@
- + @T["Enables the endpoint:"] /connect/logout
diff --git a/src/OrchardCore/OrchardCore.OpenId.Core/YesSql/Stores/OpenIdAuthorizationStore.cs b/src/OrchardCore/OrchardCore.OpenId.Core/YesSql/Stores/OpenIdAuthorizationStore.cs index d48974c4f7d..fe89d3ff0c4 100644 --- a/src/OrchardCore/OrchardCore.OpenId.Core/YesSql/Stores/OpenIdAuthorizationStore.cs +++ b/src/OrchardCore/OrchardCore.OpenId.Core/YesSql/Stores/OpenIdAuthorizationStore.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Globalization; +using System.Linq.Expressions; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -10,6 +11,7 @@ using OrchardCore.OpenId.YesSql.Models; using YesSql; using YesSql.Services; +using static OpenIddict.Abstractions.OpenIddictConstants; namespace OrchardCore.OpenId.YesSql.Stores; @@ -59,60 +61,53 @@ public virtual async ValueTask DeleteAsync(TAuthorization authorization, Cancell } /// - public virtual IAsyncEnumerable FindAsync( - string subject, string client, CancellationToken cancellationToken) + public virtual async IAsyncEnumerable FindAsync( + string subject, string client, string status, string type, + ImmutableArray? scopes, [EnumeratorCancellation] CancellationToken cancellationToken) { - ArgumentException.ThrowIfNullOrEmpty(subject); - ArgumentException.ThrowIfNullOrEmpty(client); + Expression> query = index => true; - cancellationToken.ThrowIfCancellationRequested(); + if (!string.IsNullOrEmpty(subject)) + { + Expression> filter = index => index.Subject == subject; - return _session.Query( - index => index.ApplicationId == client && index.Subject == subject, - collection: OpenIdCollection).ToAsyncEnumerable(); - } + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } - /// - public virtual IAsyncEnumerable FindAsync( - string subject, string client, string status, CancellationToken cancellationToken) - { - ArgumentException.ThrowIfNullOrEmpty(subject); - ArgumentException.ThrowIfNullOrEmpty(client); - ArgumentException.ThrowIfNullOrEmpty(status); + if (!string.IsNullOrEmpty(client)) + { + Expression> filter = index => index.ApplicationId == client; - cancellationToken.ThrowIfCancellationRequested(); + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } - return _session.Query( - index => index.ApplicationId == client && index.Subject == subject && index.Status == status, - collection: OpenIdCollection).ToAsyncEnumerable(); - } + if (!string.IsNullOrEmpty(status)) + { + Expression> filter = index => index.Status == status; - /// - public virtual IAsyncEnumerable FindAsync( - string subject, string client, - string status, string type, CancellationToken cancellationToken) - { - ArgumentException.ThrowIfNullOrEmpty(subject); - ArgumentException.ThrowIfNullOrEmpty(client); - ArgumentException.ThrowIfNullOrEmpty(status); - ArgumentException.ThrowIfNullOrEmpty(type); + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } + + if (!string.IsNullOrEmpty(type)) + { + Expression> filter = index => index.Type == type; + + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } cancellationToken.ThrowIfCancellationRequested(); - return _session.Query( - index => index.ApplicationId == client && index.Subject == subject && - index.Status == status && index.Type == type, - collection: OpenIdCollection).ToAsyncEnumerable(); - } + var authorizations = _session.Query( + query, collection: OpenIdCollection).ToAsyncEnumerable(); - /// - public virtual async IAsyncEnumerable FindAsync( - string subject, string client, string status, string type, - ImmutableArray scopes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var authorization in FindAsync(subject, client, status, type, cancellationToken)) + await foreach (var authorization in authorizations) { - if (new HashSet(await GetScopesAsync(authorization, cancellationToken), StringComparer.Ordinal).IsSupersetOf(scopes)) + if (scopes is null || new HashSet(await GetScopesAsync(authorization, cancellationToken), + StringComparer.Ordinal).IsSupersetOf(scopes.Value)) { yield return authorization; } @@ -340,6 +335,138 @@ public virtual async ValueTask PruneAsync(DateTimeOffset threshold, Cancel return result; } + /// + public virtual async ValueTask RevokeAsync( + string subject, string client, string status, string type, CancellationToken cancellationToken) + { + Expression> query = index => true; + + if (!string.IsNullOrEmpty(subject)) + { + Expression> filter = index => index.Subject == subject; + + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } + + if (!string.IsNullOrEmpty(client)) + { + Expression> filter = index => index.ApplicationId == client; + + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } + + if (!string.IsNullOrEmpty(status)) + { + Expression> filter = index => index.Status == status; + + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } + + if (!string.IsNullOrEmpty(type)) + { + Expression> filter = index => index.Type == type; + + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } + + // Note: YesSql doesn't support set-based updates, which prevents updating entities + // in a single command without having to retrieve and materialize them first. + // To work around this limitation, entities are manually listed and updated. + + cancellationToken.ThrowIfCancellationRequested(); + + var authorizations = (await _session.Query( + query, collection: OpenIdCollection).ListAsync()).ToList(); + + if (authorizations.Count is 0) + { + return 0; + } + + foreach (var authorization in authorizations) + { + cancellationToken.ThrowIfCancellationRequested(); + + authorization.Status = Statuses.Revoked; + + await _session.SaveAsync(authorization, checkConcurrency: false, collection: OpenIdCollection); + } + + await _session.SaveChangesAsync(); + + return authorizations.Count; + } + + /// + public virtual async ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + // Note: YesSql doesn't support set-based updates, which prevents updating entities + // in a single command without having to retrieve and materialize them first. + // To work around this limitation, entities are manually listed and updated. + + cancellationToken.ThrowIfCancellationRequested(); + + var authorizations = (await _session.Query( + token => token.ApplicationId == identifier, collection: OpenIdCollection).ListAsync()).ToList(); + + if (authorizations.Count is 0) + { + return 0; + } + + foreach (var authorization in authorizations) + { + cancellationToken.ThrowIfCancellationRequested(); + + authorization.Status = Statuses.Revoked; + + await _session.SaveAsync(authorization, checkConcurrency: false, collection: OpenIdCollection); + } + + await _session.SaveChangesAsync(); + + return authorizations.Count; + } + + /// + public virtual async ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(subject); + + // Note: YesSql doesn't support set-based updates, which prevents updating entities + // in a single command without having to retrieve and materialize them first. + // To work around this limitation, entities are manually listed and updated. + + cancellationToken.ThrowIfCancellationRequested(); + + var authorizations = (await _session.Query( + token => token.Subject == subject, collection: OpenIdCollection).ListAsync()).ToList(); + + if (authorizations.Count is 0) + { + return 0; + } + + foreach (var authorization in authorizations) + { + cancellationToken.ThrowIfCancellationRequested(); + + authorization.Status = Statuses.Revoked; + + await _session.SaveAsync(authorization, checkConcurrency: false, collection: OpenIdCollection); + } + + await _session.SaveChangesAsync(); + + return authorizations.Count; + } + /// public virtual ValueTask SetApplicationIdAsync(TAuthorization authorization, string identifier, CancellationToken cancellationToken) diff --git a/src/OrchardCore/OrchardCore.OpenId.Core/YesSql/Stores/OpenIdTokenStore.cs b/src/OrchardCore/OrchardCore.OpenId.Core/YesSql/Stores/OpenIdTokenStore.cs index eaa3c46165d..6b11e304935 100644 --- a/src/OrchardCore/OrchardCore.OpenId.Core/YesSql/Stores/OpenIdTokenStore.cs +++ b/src/OrchardCore/OrchardCore.OpenId.Core/YesSql/Stores/OpenIdTokenStore.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Globalization; +using System.Linq.Expressions; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; @@ -60,45 +61,45 @@ public virtual async ValueTask DeleteAsync(TToken token, CancellationToken cance /// public virtual IAsyncEnumerable FindAsync( - string subject, string client, CancellationToken cancellationToken) + string subject, string client, string status, string type, CancellationToken cancellationToken) { - ArgumentException.ThrowIfNullOrEmpty(subject); - ArgumentException.ThrowIfNullOrEmpty(client); + Expression> query = index => true; - cancellationToken.ThrowIfCancellationRequested(); + if (!string.IsNullOrEmpty(subject)) + { + Expression> filter = index => index.Subject == subject; - return _session.Query( - index => index.ApplicationId == client && index.Subject == subject, collection: OpenIdCollection).ToAsyncEnumerable(); - } + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } - /// - public virtual IAsyncEnumerable FindAsync( - string subject, string client, string status, CancellationToken cancellationToken) - { - ArgumentException.ThrowIfNullOrEmpty(subject); - ArgumentException.ThrowIfNullOrEmpty(client); - ArgumentException.ThrowIfNullOrEmpty(status); + if (!string.IsNullOrEmpty(client)) + { + Expression> filter = index => index.ApplicationId == client; - cancellationToken.ThrowIfCancellationRequested(); + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } - return _session.Query( - index => index.ApplicationId == client && index.Subject == subject && index.Status == status, collection: OpenIdCollection).ToAsyncEnumerable(); - } + if (!string.IsNullOrEmpty(status)) + { + Expression> filter = index => index.Status == status; - /// - public virtual IAsyncEnumerable FindAsync( - string subject, string client, string status, string type, CancellationToken cancellationToken) - { - ArgumentException.ThrowIfNullOrEmpty(subject); - ArgumentException.ThrowIfNullOrEmpty(client); - ArgumentException.ThrowIfNullOrEmpty(status); - ArgumentException.ThrowIfNullOrEmpty(type); + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } + + if (!string.IsNullOrEmpty(type)) + { + Expression> filter = index => index.Type == type; + + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } cancellationToken.ThrowIfCancellationRequested(); - return _session.Query( - index => index.ApplicationId == client && index.Subject == subject && - index.Status == status && index.Type == type, collection: OpenIdCollection).ToAsyncEnumerable(); + return _session.Query(query, collection: OpenIdCollection).ToAsyncEnumerable(); } /// @@ -375,9 +376,108 @@ public virtual async ValueTask PruneAsync(DateTimeOffset threshold, Cancel return result; } + /// + public virtual async ValueTask RevokeAsync( + string subject, string client, string status, string type, CancellationToken cancellationToken) + { + Expression> query = index => true; + + if (!string.IsNullOrEmpty(subject)) + { + Expression> filter = index => index.Subject == subject; + + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } + + if (!string.IsNullOrEmpty(client)) + { + Expression> filter = index => index.ApplicationId == client; + + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } + + if (!string.IsNullOrEmpty(status)) + { + Expression> filter = index => index.Status == status; + + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } + + if (!string.IsNullOrEmpty(type)) + { + Expression> filter = index => index.Type == type; + + query = Expression.Lambda>( + Expression.AndAlso(query.Body, filter.Body), query.Parameters[0]); + } + + // Note: YesSql doesn't support set-based updates, which prevents updating entities + // in a single command without having to retrieve and materialize them first. + // To work around this limitation, entities are manually listed and updated. + + cancellationToken.ThrowIfCancellationRequested(); + + var tokens = (await _session.Query(query, collection: OpenIdCollection).ListAsync()).ToList(); + if (tokens.Count is 0) + { + return 0; + } + + foreach (var token in tokens) + { + cancellationToken.ThrowIfCancellationRequested(); + + token.Status = Statuses.Revoked; + + await _session.SaveAsync(token, checkConcurrency: false, collection: OpenIdCollection); + } + + await _session.SaveChangesAsync(); + + return tokens.Count; + } + + /// + public virtual async ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(identifier); + + // Note: YesSql doesn't support set-based updates, which prevents updating entities + // in a single command without having to retrieve and materialize them first. + // To work around this limitation, entities are manually listed and updated. + + cancellationToken.ThrowIfCancellationRequested(); + + var tokens = (await _session.Query( + token => token.ApplicationId == identifier, collection: OpenIdCollection).ListAsync()).ToList(); + + if (tokens.Count is 0) + { + return 0; + } + + foreach (var token in tokens) + { + cancellationToken.ThrowIfCancellationRequested(); + + token.Status = Statuses.Revoked; + + await _session.SaveAsync(token, checkConcurrency: false, collection: OpenIdCollection); + } + + await _session.SaveChangesAsync(); + + return tokens.Count; + } + /// public virtual async ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) { + ArgumentException.ThrowIfNullOrEmpty(identifier); + // Note: YesSql doesn't support set-based updates, which prevents updating entities // in a single command without having to retrieve and materialize them first. // To work around this limitation, entities are manually listed and updated. @@ -394,6 +494,40 @@ public virtual async ValueTask RevokeByAuthorizationIdAsync(string identif foreach (var token in tokens) { + cancellationToken.ThrowIfCancellationRequested(); + + token.Status = Statuses.Revoked; + + await _session.SaveAsync(token, checkConcurrency: false, collection: OpenIdCollection); + } + + await _session.SaveChangesAsync(); + + return tokens.Count; + } + + public virtual async ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(subject); + + // Note: YesSql doesn't support set-based updates, which prevents updating entities + // in a single command without having to retrieve and materialize them first. + // To work around this limitation, entities are manually listed and updated. + + cancellationToken.ThrowIfCancellationRequested(); + + var tokens = (await _session.Query( + token => token.Subject == subject, collection: OpenIdCollection).ListAsync()).ToList(); + + if (tokens.Count is 0) + { + return 0; + } + + foreach (var token in tokens) + { + cancellationToken.ThrowIfCancellationRequested(); + token.Status = Statuses.Revoked; await _session.SaveAsync(token, checkConcurrency: false, collection: OpenIdCollection); diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.OpenId/OpenIdApplicationStepTestsData.cs b/test/OrchardCore.Tests/Modules/OrchardCore.OpenId/OpenIdApplicationStepTestsData.cs index ecca1e5d06f..d1d8e9d9685 100644 --- a/test/OrchardCore.Tests/Modules/OrchardCore.OpenId/OpenIdApplicationStepTestsData.cs +++ b/test/OrchardCore.Tests/Modules/OrchardCore.OpenId/OpenIdApplicationStepTestsData.cs @@ -24,7 +24,7 @@ public OpenIdApplicationStepTestsData() OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, OpenIddictConstants.Permissions.GrantTypes.RefreshToken, OpenIddictConstants.Permissions.Endpoints.Authorization, - OpenIddictConstants.Permissions.Endpoints.Logout, + OpenIddictConstants.Permissions.Endpoints.EndSession, OpenIddictConstants.Permissions.Endpoints.Token, OpenIddictConstants.Permissions.ResponseTypes.Code, });