From 38711c14f1f72cb5b9a177a68f48be879d06ddd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 4 Oct 2024 16:02:00 +0200 Subject: [PATCH 1/4] Bump the .NET dependencies --- Directory.Packages.props | 232 +++++++++++++++++++-------------------- global.json | 2 +- 2 files changed, 117 insertions(+), 117 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d82ae6655..0edcc7f99 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -227,40 +227,40 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - + - - - - - - + + + + + + - + - - - - + + + + - + - + - + diff --git a/global.json b/global.json index 4889ac8a1..935533eb2 100644 --- a/global.json +++ b/global.json @@ -10,7 +10,7 @@ "runtimes": { "aspnetcore": [ - "6.0.32", + "6.0.33", "8.0.8" ] } From fcc0ddd99cdcebdae22d71008d83643b69c8d08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 4 Oct 2024 17:05:14 +0200 Subject: [PATCH 2/4] Introduce new RevokeByApplicationIdAsync()/RevokeBySubjectAsync() APIs in the authorization/token managers/stores --- .../IOpenIddictAuthorizationManager.cs | 16 +++ .../Managers/IOpenIddictTokenManager.cs | 16 +++ .../Stores/IOpenIddictAuthorizationStore.cs | 16 +++ .../Stores/IOpenIddictTokenStore.cs | 16 +++ .../OpenIddictAuthorizationManager.cs | 40 ++++++ .../Managers/OpenIddictTokenManager.cs | 40 ++++++ ...IddictEntityFrameworkAuthorizationStore.cs | 92 ++++++++++++++ .../OpenIddictEntityFrameworkTokenStore.cs | 92 ++++++++++++++ ...ctEntityFrameworkCoreAuthorizationStore.cs | 118 ++++++++++++++++++ ...OpenIddictEntityFrameworkCoreTokenStore.cs | 118 ++++++++++++++++++ .../OpenIddictMongoDbAuthorizationStore.cs | 36 ++++++ .../Stores/OpenIddictMongoDbTokenStore.cs | 36 ++++++ 12 files changed, 636 insertions(+) diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs index 23bacf250..6b9e61c1e 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs @@ -394,6 +394,22 @@ IAsyncEnumerable ListAsync( /// The number of authorizations that were removed. ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default); + /// + /// Revokes all the authorizations associated with the specified application identifier. + /// + /// The application identifier associated with the authorizations. + /// The that can be used to abort the operation. + /// The number of authorizations associated with the specified application that were marked as revoked. + ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken = default); + + /// + /// Revokes all the authorizations associated with the specified subject. + /// + /// The subject associated with the authorizations. + /// The that can be used to abort the operation. + /// The number of authorizations associated with the specified subject that were marked as revoked. + ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken = default); + /// /// Tries to revoke an authorization. /// diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index 1866a21ce..f8e4c0ba7 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -409,6 +409,14 @@ IAsyncEnumerable ListAsync( /// The number of tokens that were removed. ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default); + /// + /// Revokes all the tokens associated with the specified application identifier. + /// + /// The application identifier associated with the tokens. + /// The that can be used to abort the operation. + /// The number of tokens associated with the specified application that were marked as revoked. + ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken = default); + /// /// Revokes all the tokens associated with the specified authorization identifier. /// @@ -417,6 +425,14 @@ IAsyncEnumerable ListAsync( /// The number of tokens associated with the specified authorization that were marked as revoked. ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken = default); + /// + /// Revokes all the tokens associated with the specified subject. + /// + /// The subject associated with the tokens. + /// The that can be used to abort the operation. + /// The number of tokens associated with the specified subject that were marked as revoked. + ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken = default); + /// /// Tries to redeem a token. /// diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs index bb5e122bf..01121dacb 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs @@ -279,6 +279,22 @@ IAsyncEnumerable ListAsync( /// The number of authorizations that were removed. ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken); + /// + /// Revokes all the authorizations associated with the specified application identifier. + /// + /// The application identifier associated with the authorizations. + /// The that can be used to abort the operation. + /// The number of authorizations associated with the specified application that were marked as revoked. + ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken = default); + + /// + /// Revokes all the authorizations associated with the specified subject. + /// + /// The subject associated with the authorizations. + /// The that can be used to abort the operation. + /// The number of authorizations associated with the specified subject that were marked as revoked. + ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken = default); + /// /// Sets the application identifier associated with an authorization. /// diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs index dfb6af732..af65b61fe 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs @@ -326,6 +326,14 @@ IAsyncEnumerable ListAsync( /// The number of tokens that were removed. ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken); + /// + /// Revokes all the tokens associated with the specified application identifier. + /// + /// The application identifier associated with the tokens. + /// The that can be used to abort the operation. + /// The number of tokens associated with the specified application that were marked as revoked. + ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken = default); + /// /// Revokes all the tokens associated with the specified authorization identifier. /// @@ -334,6 +342,14 @@ IAsyncEnumerable ListAsync( /// The number of tokens associated with the specified authorization that were marked as revoked. ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken); + /// + /// Revokes all the tokens associated with the specified subject. + /// + /// The subject associated with the tokens. + /// The that can be used to abort the operation. + /// The number of tokens associated with the specified subject that were marked as revoked. + ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken = default); + /// /// Sets the application identifier associated with a token. /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 0ef781714..54f3aad90 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -1028,6 +1028,38 @@ public virtual async ValueTask PopulateAsync( public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default) => Store.PruneAsync(threshold, cancellationToken); + /// + /// Revokes all the authorizations associated with the specified application identifier. + /// + /// The application identifier associated with the authorizations. + /// The that can be used to abort the operation. + /// The number of authorizations associated with the specified application that were marked as revoked. + public virtual ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier)); + } + + return Store.RevokeByApplicationIdAsync(identifier, cancellationToken); + } + + /// + /// Revokes all the authorizations associated with the specified subject. + /// + /// The subject associated with the authorizations. + /// The that can be used to abort the operation. + /// The number of authorizations associated with the specified subject that were marked as revoked. + public virtual ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(subject)); + } + + return Store.RevokeBySubjectAsync(subject, cancellationToken); + } + /// /// Tries to revoke an authorization. /// @@ -1337,6 +1369,14 @@ ValueTask IOpenIddictAuthorizationManager.PopulateAsync(object authorization, Op ValueTask IOpenIddictAuthorizationManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) => PruneAsync(threshold, cancellationToken); + /// + ValueTask IOpenIddictAuthorizationManager.RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + => RevokeByApplicationIdAsync(identifier, cancellationToken); + + /// + ValueTask IOpenIddictAuthorizationManager.RevokeBySubjectAsync(string subject, CancellationToken cancellationToken) + => RevokeBySubjectAsync(subject, cancellationToken); + /// ValueTask IOpenIddictAuthorizationManager.TryRevokeAsync(object authorization, CancellationToken cancellationToken) => TryRevokeAsync((TAuthorization) authorization, cancellationToken); diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index aba9da05b..d74382988 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -1055,6 +1055,22 @@ public virtual async ValueTask PopulateAsync( public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default) => Store.PruneAsync(threshold, cancellationToken); + /// + /// Revokes all the tokens associated with the specified application identifier. + /// + /// The application identifier associated with the tokens. + /// The that can be used to abort the operation. + /// The number of tokens associated with the specified application that were marked as revoked. + public virtual ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier)); + } + + return Store.RevokeByApplicationIdAsync(identifier, cancellationToken); + } + /// /// Revokes all the tokens associated with the specified authorization identifier. /// @@ -1071,6 +1087,22 @@ public virtual ValueTask RevokeByAuthorizationIdAsync(string identifier, C return Store.RevokeByAuthorizationIdAsync(identifier, cancellationToken); } + /// + /// Revokes all the tokens associated with the specified subject. + /// + /// The subject associated with the tokens. + /// The that can be used to abort the operation. + /// The number of tokens associated with the specified subject that were marked as revoked. + public virtual ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(subject)); + } + + return Store.RevokeBySubjectAsync(subject, cancellationToken); + } + /// /// Tries to redeem a token. /// @@ -1501,10 +1533,18 @@ ValueTask IOpenIddictTokenManager.PopulateAsync(object token, OpenIddictTokenDes ValueTask IOpenIddictTokenManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken) => PruneAsync(threshold, cancellationToken); + /// + ValueTask IOpenIddictTokenManager.RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + => RevokeByApplicationIdAsync(identifier, cancellationToken); + /// ValueTask IOpenIddictTokenManager.RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) => RevokeByAuthorizationIdAsync(identifier, cancellationToken); + /// + ValueTask IOpenIddictTokenManager.RevokeBySubjectAsync(string subject, CancellationToken cancellationToken) + => RevokeBySubjectAsync(subject, cancellationToken); + /// ValueTask IOpenIddictTokenManager.TryRedeemAsync(object token, CancellationToken cancellationToken) => TryRedeemAsync((TToken) token, cancellationToken); diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs index e427d7ded..d6020f77b 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs @@ -658,6 +658,98 @@ orderby authorization.Id return result; } + /// + public virtual async ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier)); + } + + var key = ConvertIdentifierFromString(identifier); + + List? exceptions = null; + + var result = 0L; + + foreach (var authorization in await (from authorization in Authorizations + where authorization.Application!.Id!.Equals(key) + select authorization).ToListAsync(cancellationToken)) + { + authorization.Status = Statuses.Revoked; + + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + Context.Entry(authorization).State = EntityState.Unchanged; + + exceptions ??= []; + exceptions.Add(exception); + + continue; + } + + result++; + } + + if (exceptions is not null) + { + throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + } + + return result; + } + + /// + public virtual async ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(subject)); + } + + List? exceptions = null; + + var result = 0L; + + foreach (var authorization in await (from authorization in Authorizations + where authorization.Subject == subject + select authorization).ToListAsync(cancellationToken)) + { + authorization.Status = Statuses.Revoked; + + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + Context.Entry(authorization).State = EntityState.Unchanged; + + exceptions ??= []; + exceptions.Add(exception); + + continue; + } + + result++; + } + + if (exceptions is not null) + { + throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + } + + return result; + } + /// public virtual async ValueTask SetApplicationIdAsync(TAuthorization authorization, string? identifier, CancellationToken cancellationToken) diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs index c446b1a9d..d790bf44b 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs @@ -659,6 +659,53 @@ orderby token.Id return result; } + /// + public virtual async ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier)); + } + + var key = ConvertIdentifierFromString(identifier); + + List? exceptions = null; + + var result = 0L; + + foreach (var token in await (from token in Tokens + where token.Application!.Id!.Equals(key) + select token).ToListAsync(cancellationToken)) + { + token.Status = Statuses.Revoked; + + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + Context.Entry(token).State = EntityState.Unchanged; + + exceptions ??= []; + exceptions.Add(exception); + + continue; + } + + result++; + } + + if (exceptions is not null) + { + throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + } + + return result; + } + /// public virtual async ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) { @@ -706,6 +753,51 @@ public virtual async ValueTask RevokeByAuthorizationIdAsync(string identif return result; } + /// + public virtual async ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(subject)); + } + + List? exceptions = null; + + var result = 0L; + + foreach (var token in await (from token in Tokens + where token.Subject == subject + select token).ToListAsync(cancellationToken)) + { + token.Status = Statuses.Revoked; + + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + Context.Entry(token).State = EntityState.Unchanged; + + exceptions ??= []; + exceptions.Add(exception); + + continue; + } + + result++; + } + + if (exceptions is not null) + { + throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + } + + return result; + } + /// public virtual async ValueTask SetApplicationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) { diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs index b2151a02f..21115ab02 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs @@ -799,6 +799,124 @@ orderby authorization.Id return result; } + /// + public virtual async ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier)); + } + + var key = ConvertIdentifierFromString(identifier); + +#if SUPPORTS_BULK_DBSET_OPERATIONS + if (!Options.CurrentValue.DisableBulkOperations) + { + return await ( + from authorization in Authorizations + where authorization.Application!.Id!.Equals(key) + select authorization).ExecuteUpdateAsync(entity => entity.SetProperty( + authorization => authorization.Status, Statuses.Revoked), cancellationToken); + + // Note: calling DbContext.SaveChangesAsync() is not necessary + // with bulk update operations as they are executed immediately. + } +#endif + List? exceptions = null; + + var result = 0L; + + foreach (var authorization in await (from authorization in Authorizations + where authorization.Application!.Id!.Equals(key) + select authorization).ToListAsync(cancellationToken)) + { + authorization.Status = Statuses.Revoked; + + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + Context.Entry(authorization).State = EntityState.Unchanged; + + exceptions ??= []; + exceptions.Add(exception); + + continue; + } + + result++; + } + + if (exceptions is not null) + { + throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + } + + return result; + } + + /// + public virtual async ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(subject)); + } + +#if SUPPORTS_BULK_DBSET_OPERATIONS + if (!Options.CurrentValue.DisableBulkOperations) + { + return await ( + from authorization in Authorizations + where authorization.Subject == subject + select authorization).ExecuteUpdateAsync(entity => entity.SetProperty( + authorization => authorization.Status, Statuses.Revoked), cancellationToken); + + // Note: calling DbContext.SaveChangesAsync() is not necessary + // with bulk update operations as they are executed immediately. + } +#endif + List? exceptions = null; + + var result = 0L; + + foreach (var authorization in await (from authorization in Authorizations + where authorization.Subject == subject + select authorization).ToListAsync(cancellationToken)) + { + authorization.Status = Statuses.Revoked; + + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + Context.Entry(authorization).State = EntityState.Unchanged; + + exceptions ??= []; + exceptions.Add(exception); + + continue; + } + + result++; + } + + if (exceptions is not null) + { + throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + } + + return result; + } + /// public virtual async ValueTask SetApplicationIdAsync(TAuthorization authorization, string? identifier, CancellationToken cancellationToken) diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs index defa48ffa..09d499d07 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs @@ -748,6 +748,66 @@ orderby token.Id return result; } + /// + public virtual async ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier)); + } + + var key = ConvertIdentifierFromString(identifier); + +#if SUPPORTS_BULK_DBSET_OPERATIONS + if (!Options.CurrentValue.DisableBulkOperations) + { + return await ( + from token in Tokens + where token.Application!.Id!.Equals(key) + select token).ExecuteUpdateAsync(entity => entity.SetProperty( + token => token.Status, Statuses.Revoked), cancellationToken); + + // Note: calling DbContext.SaveChangesAsync() is not necessary + // with bulk update operations as they are executed immediately. + } +#endif + List? exceptions = null; + + var result = 0L; + + foreach (var token in await (from token in Tokens + where token.Application!.Id!.Equals(key) + select token).ToListAsync(cancellationToken)) + { + token.Status = Statuses.Revoked; + + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + Context.Entry(token).State = EntityState.Unchanged; + + exceptions ??= []; + exceptions.Add(exception); + + continue; + } + + result++; + } + + if (exceptions is not null) + { + throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + } + + return result; + } + /// public virtual async ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) { @@ -808,6 +868,64 @@ from token in Tokens return result; } + /// + public virtual async ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(subject)); + } + +#if SUPPORTS_BULK_DBSET_OPERATIONS + if (!Options.CurrentValue.DisableBulkOperations) + { + return await ( + from token in Tokens + where token.Subject == subject + select token).ExecuteUpdateAsync(entity => entity.SetProperty( + token => token.Status, Statuses.Revoked), cancellationToken); + + // Note: calling DbContext.SaveChangesAsync() is not necessary + // with bulk update operations as they are executed immediately. + } +#endif + List? exceptions = null; + + var result = 0L; + + foreach (var token in await (from token in Tokens + where token.Subject == subject + select token).ToListAsync(cancellationToken)) + { + token.Status = Statuses.Revoked; + + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing. + Context.Entry(token).State = EntityState.Unchanged; + + exceptions ??= []; + exceptions.Add(exception); + + continue; + } + + result++; + } + + if (exceptions is not null) + { + throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions); + } + + return result; + } + /// public virtual async ValueTask SetApplicationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) { diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs index b57e126b7..d760c1ae2 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs @@ -549,6 +549,42 @@ where authorization.CreationDate < threshold.UtcDateTime return result; } + /// + public virtual async ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return (await collection.UpdateManyAsync( + filter : authorization => authorization.ApplicationId == ObjectId.Parse(identifier), + update : Builders.Update.Set(authorization => authorization.Status, Statuses.Revoked), + options : null, + cancellationToken: cancellationToken)).MatchedCount; + } + + /// + public virtual async ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(subject)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return (await collection.UpdateManyAsync( + filter : authorization => authorization.Subject == subject, + update : Builders.Update.Set(authorization => authorization.Status, Statuses.Revoked), + options : null, + cancellationToken: cancellationToken)).MatchedCount; + } + /// public virtual ValueTask SetApplicationIdAsync(TAuthorization authorization, string? identifier, CancellationToken cancellationToken) diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs index 70858b3b4..797d7f785 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs @@ -586,6 +586,24 @@ where token.CreationDate < threshold.UtcDateTime return result; } + /// + public virtual async ValueTask RevokeByApplicationIdAsync(string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return (await collection.UpdateManyAsync( + filter : token => token.ApplicationId == ObjectId.Parse(identifier), + update : Builders.Update.Set(token => token.Status, Statuses.Revoked), + options : null, + cancellationToken: cancellationToken)).MatchedCount; + } + /// public virtual async ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken) { @@ -604,6 +622,24 @@ public virtual async ValueTask RevokeByAuthorizationIdAsync(string identif cancellationToken: cancellationToken)).MatchedCount; } + /// + public virtual async ValueTask RevokeBySubjectAsync(string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(subject)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return (await collection.UpdateManyAsync( + filter : token => token.Subject == subject, + update : Builders.Update.Set(token => token.Status, Statuses.Revoked), + options : null, + cancellationToken: cancellationToken)).MatchedCount; + } + /// public virtual ValueTask SetApplicationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) { From e1f729ba0b55040ca4deeb4bcba09054bb8f04c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 4 Oct 2024 19:14:01 +0200 Subject: [PATCH 3/4] Validate prompt values specified in authorization requests and update the configuration endpoint to return "prompt_values_supported" --- .editorconfig | 1 + .../Controllers/AuthorizationController.cs | 6 +- .../Controllers/AuthorizationController.cs | 12 ++-- .../OpenIddictConstants.cs | 4 +- .../OpenIddictResources.resx | 14 ++--- .../OpenIddictServerBuilder.cs | 25 +++++++- .../OpenIddictServerEvents.Discovery.cs | 5 ++ ...OpenIddictServerHandlers.Authentication.cs | 29 ++++++++- .../OpenIddictServerHandlers.Discovery.cs | 33 ++++++++++- .../OpenIddictServerOptions.cs | 13 ++++ .../Primitives/OpenIddictExtensionsTests.cs | 4 +- ...ctServerIntegrationTests.Authentication.cs | 21 ++++--- ...nIddictServerIntegrationTests.Discovery.cs | 24 ++++++++ .../OpenIddictServerBuilderTests.cs | 59 ++++++++++++++++--- 14 files changed, 208 insertions(+), 42 deletions(-) diff --git a/.editorconfig b/.editorconfig index 402c9ed2a..27ad13512 100644 --- a/.editorconfig +++ b/.editorconfig @@ -97,6 +97,7 @@ csharp_using_directive_placement = outside_namespace dotnet_code_quality_unused_parameters = all dotnet_diagnostic.CA1510.severity = none dotnet_diagnostic.CA2254.severity = none +dotnet_diagnostic.IDE0002.severity = none dotnet_diagnostic.IDE0305.severity = none dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs index ed0880da1..5b2bd22a0 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs @@ -139,7 +139,7 @@ public async Task Authorize() // return an authorization response without displaying the consent form. case ConsentTypes.Implicit: case ConsentTypes.External when authorizations.Count is not 0: - case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(Prompts.Consent): + case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(PromptValues.Consent): // Create the claims-based identity that will be used by OpenIddict to generate tokens. var identity = new ClaimsIdentity( authenticationType: OpenIddictServerOwinDefaults.AuthenticationType, @@ -178,8 +178,8 @@ public async Task Authorize() // At this point, no authorization was found in the database and an error must be returned // if the client application specified prompt=none in the authorization request. - case ConsentTypes.Explicit when request.HasPrompt(Prompts.None): - case ConsentTypes.Systematic when request.HasPrompt(Prompts.None): + case ConsentTypes.Explicit when request.HasPrompt(PromptValues.None): + case ConsentTypes.Systematic when request.HasPrompt(PromptValues.None): context.Authentication.Challenge( authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType, properties: new AuthenticationProperties(new Dictionary diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs index 418042314..eafea9965 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs @@ -71,13 +71,13 @@ public async Task Authorize() // For scenarios where the default authentication handler configured in the ASP.NET Core // authentication options shouldn't be used, a specific scheme can be specified here. var result = await HttpContext.AuthenticateAsync(); - if (result == null || !result.Succeeded || request.HasPrompt(Prompts.Login) || + if (result == null || !result.Succeeded || request.HasPrompt(PromptValues.Login) || (request.MaxAge != null && result.Properties?.IssuedUtc != null && DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) { // 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.HasPrompt(PromptValues.None)) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, @@ -90,7 +90,7 @@ public async Task Authorize() // 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.GetPrompts().Remove(Prompts.Login)); + var prompt = string.Join(" ", request.GetPrompts().Remove(PromptValues.Login)); var parameters = Request.HasFormContentType ? Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() : @@ -173,7 +173,7 @@ public async Task Authorize() // return an authorization response without displaying the consent form. case ConsentTypes.Implicit: case ConsentTypes.External when authorizations.Count is not 0: - case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(Prompts.Consent): + case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(PromptValues.Consent): // Create the claims-based identity that will be used by OpenIddict to generate tokens. var identity = new ClaimsIdentity( authenticationType: TokenValidationParameters.DefaultAuthenticationType, @@ -210,8 +210,8 @@ public async Task Authorize() // At this point, no authorization was found in the database and an error must be returned // if the client application specified prompt=none in the authorization request. - case ConsentTypes.Explicit when request.HasPrompt(Prompts.None): - case ConsentTypes.Systematic when request.HasPrompt(Prompts.None): + case ConsentTypes.Explicit when request.HasPrompt(PromptValues.None): + case ConsentTypes.Systematic when request.HasPrompt(PromptValues.None): return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index d53e17e40..ed71631b8 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -295,6 +295,7 @@ public static class Metadata public const string MtlsEndpointAliases = "mtls_endpoint_aliases"; public const string OpPolicyUri = "op_policy_uri"; public const string OpTosUri = "op_tos_uri"; + public const string PromptValuesSupported = "prompt_values_supported"; public const string RequestObjectEncryptionAlgValuesSupported = "request_object_encryption_alg_values_supported"; public const string RequestObjectEncryptionEncValuesSupported = "request_object_encryption_enc_values_supported"; public const string RequestObjectSigningAlgValuesSupported = "request_object_signing_alg_values_supported"; @@ -430,9 +431,10 @@ public static class Scopes } } - public static class Prompts + public static class PromptValues { public const string Consent = "consent"; + public const string Create = "create"; public const string Login = "login"; public const string None = "none"; public const string SelectAccount = "select_account"; diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 3fa78acae..0ab3e2479 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -365,12 +365,6 @@ Consider using 'options.AddSigningCredentials(SigningCredentials)' instead. Endpoint URIs must be valid URIs. - - Claims cannot be null or empty. - - - Scopes cannot be null or empty. - The security token handler cannot be null. @@ -1704,6 +1698,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId The specified client authentication method/token binding methods combination is not valid. + + The '{0}' parameter cannot contain null or empty values. + The security token is missing. @@ -2379,7 +2376,7 @@ The principal used to create the token contained the following claims: {Claims}. The authorization request was rejected because the '{Scope}' scope was missing. - The authorization request was rejected because an invalid prompt parameter was specified. + The authorization request was rejected because an invalid prompt combination was specified. The authorization request was rejected because the specified code challenge method was not supported. @@ -2892,6 +2889,9 @@ This may indicate that the hashed entry is corrupted or malformed. An error was returned by ASWebAuthenticationSession while trying to start a sign-out operation. + + The authorization request was rejected because an unsupported prompt parameter was specified. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 2e70a6be5..ab6c73c8f 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -1634,12 +1634,33 @@ public OpenIddictServerBuilder RegisterClaims(params string[] claims) if (Array.Exists(claims, string.IsNullOrEmpty)) { - throw new ArgumentException(SR.GetResourceString(SR.ID0073), nameof(claims)); + throw new ArgumentException(SR.FormatID0457(nameof(claims)), nameof(claims)); } return Configure(options => options.Claims.UnionWith(claims)); } + /// + /// Registers the specified prompt values as supported scopes so + /// they can be returned as part of the discovery document. + /// + /// The supported prompt values. + /// The instance. + public OpenIddictServerBuilder RegisterPromptValues(params string[] values) + { + if (values is null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (Array.Exists(values, string.IsNullOrEmpty)) + { + throw new ArgumentException(SR.FormatID0457(nameof(values)), nameof(values)); + } + + return Configure(options => options.PromptValues.UnionWith(values)); + } + /// /// Registers the specified scopes as supported scopes so /// they can be returned as part of the discovery document. @@ -1655,7 +1676,7 @@ public OpenIddictServerBuilder RegisterScopes(params string[] scopes) if (Array.Exists(scopes, string.IsNullOrEmpty)) { - throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes)); + throw new ArgumentException(SR.FormatID0457(nameof(scopes)), nameof(scopes)); } return Configure(options => options.Scopes.UnionWith(scopes)); diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs index bcc16fb34..7370a7c54 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs @@ -166,6 +166,11 @@ public OpenIddictRequest Request /// public HashSet IntrospectionEndpointAuthenticationMethods { get; } = new(StringComparer.Ordinal); + /// + /// Gets the list of prompt values supported by the authorization server. + /// + public HashSet PromptValues { get; } = new(StringComparer.Ordinal); + /// /// Gets the list of response modes /// supported by the authorization server. diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index 76d9de706..2dd3dcae2 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -875,10 +875,33 @@ public ValueTask HandleAsync(ValidateAuthorizationRequestContext context) throw new ArgumentNullException(nameof(context)); } + if (string.IsNullOrEmpty(context.Request.Prompt)) + { + return default; + } + + // Reject requests specifying an unsupported prompt value. + // See https://openid.net/specs/openid-connect-prompt-create-1_0.html#section-4.1 for more information. + foreach (var value in context.Request.GetPrompts().ToHashSet(StringComparer.Ordinal)) + { + if (!context.Options.PromptValues.Contains(value)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6233)); + + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2032(Parameters.Prompt), + uri: SR.FormatID8000(SR.ID2032)); + + return default; + } + } + // Reject requests specifying prompt=none with consent/login or select_account. - if (context.Request.HasPrompt(Prompts.None) && (context.Request.HasPrompt(Prompts.Consent) || - context.Request.HasPrompt(Prompts.Login) || - context.Request.HasPrompt(Prompts.SelectAccount))) + // See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information. + if (context.Request.HasPrompt(PromptValues.None) && (context.Request.HasPrompt(PromptValues.Consent) || + context.Request.HasPrompt(PromptValues.Login) || + context.Request.HasPrompt(PromptValues.SelectAccount))) { context.Logger.LogInformation(SR.GetResourceString(SR.ID6040)); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs index c6a34a716..c6cea2485 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs @@ -41,6 +41,7 @@ public static class Discovery AttachScopes.Descriptor, AttachClaims.Descriptor, AttachSubjectTypes.Descriptor, + AttachPromptValues.Descriptor, AttachSigningAlgorithms.Descriptor, AttachAdditionalMetadata.Descriptor, @@ -250,6 +251,7 @@ public async ValueTask HandleAsync(ProcessRequestContext context) [Metadata.IdTokenSigningAlgValuesSupported] = notification.IdTokenSigningAlgorithms.ToArray(), [Metadata.CodeChallengeMethodsSupported] = notification.CodeChallengeMethods.ToArray(), [Metadata.SubjectTypesSupported] = notification.SubjectTypes.ToArray(), + [Metadata.PromptValuesSupported] = notification.PromptValues.ToArray(), [Metadata.TokenEndpointAuthMethodsSupported] = notification.TokenEndpointAuthenticationMethods.ToArray(), [Metadata.IntrospectionEndpointAuthMethodsSupported] = notification.IntrospectionEndpointAuthenticationMethods.ToArray(), [Metadata.RevocationEndpointAuthMethodsSupported] = notification.RevocationEndpointAuthenticationMethods.ToArray(), @@ -673,6 +675,35 @@ public ValueTask HandleAsync(HandleConfigurationRequestContext context) } } + /// + /// Contains the logic responsible for attaching the supported prompt values to the provider discovery document. + /// + public sealed class AttachPromptValues : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachSubjectTypes.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.PromptValues.UnionWith(context.Options.PromptValues); + + return default; + } + } + /// /// Contains the logic responsible for attaching the supported signing algorithms to the provider discovery document. /// @@ -684,7 +715,7 @@ public sealed class AttachSigningAlgorithms : IOpenIddictServerHandler() .UseSingletonHandler() - .SetOrder(AttachSubjectTypes.Descriptor.Order + 1_000) + .SetOrder(AttachPromptValues.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index a4a90bde9..0077fb546 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -375,6 +375,19 @@ public sealed class OpenIddictServerOptions /// public HashSet GrantTypes { get; } = new(StringComparer.Ordinal); + /// + /// Gets the OpenID Connect prompt values enabled for this application. + /// + public HashSet PromptValues { get; } = new(StringComparer.Ordinal) + { + // By default, only include the mandatory values defined in the core OpenID Connect specification. + // See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information. + OpenIddictConstants.PromptValues.Consent, + OpenIddictConstants.PromptValues.Login, + OpenIddictConstants.PromptValues.None, + OpenIddictConstants.PromptValues.SelectAccount + }; + /// /// Gets or sets a boolean indicating whether PKCE must be used by client applications /// when requesting an authorization code (e.g when using the code or hybrid flows). diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs index c4a7c25d2..59527d6b0 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs @@ -225,7 +225,7 @@ public void HasPrompt_ThrowsAnExceptionForNullRequest() // Act and assert var exception = Assert.Throws(() => { - request.HasPrompt(Prompts.Consent); + request.HasPrompt(PromptValues.Consent); }); Assert.Equal("request", exception.ParamName); @@ -277,7 +277,7 @@ public void HasPrompt_ReturnsExpectedResult(string prompt, bool result) }; // Act and assert - Assert.Equal(result, request.HasPrompt(Prompts.Consent)); + Assert.Equal(result, request.HasPrompt(PromptValues.Consent)); } [Fact] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs index 3c6db6975..14392037c 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs @@ -383,14 +383,17 @@ public async Task ValidateAuthorizationRequest_MissingOpenIdScopeCausesAnErrorFo Assert.Equal(SR.FormatID8000(SR.ID2034), response.ErrorUri); } - [Theory] - [InlineData("none consent")] - [InlineData("none login")] - [InlineData("none select_account")] - public async Task ValidateAuthorizationRequest_InvalidPromptCausesAnError(string prompt) + [Fact] + public async Task ValidateAuthorizationRequest_UnsupportedPromptCausesAnError() { // Arrange - await using var server = await CreateServerAsync(options => options.EnableDegradedMode()); + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.PromptValues.Remove(PromptValues.SelectAccount)); + }); + await using var client = await server.CreateClientAsync(); // Act @@ -398,7 +401,7 @@ public async Task ValidateAuthorizationRequest_InvalidPromptCausesAnError(string { ClientId = "Fabrikam", Nonce = "n-0S6_WzA2Mj", - Prompt = prompt, + Prompt = PromptValues.SelectAccount, RedirectUri = "http://www.fabrikam.com/path", ResponseType = "code id_token token", Scope = Scopes.OpenId @@ -406,8 +409,8 @@ public async Task ValidateAuthorizationRequest_InvalidPromptCausesAnError(string // Assert Assert.Equal(Errors.InvalidRequest, response.Error); - Assert.Equal(SR.FormatID2052(Parameters.Prompt), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri); + Assert.Equal(SR.FormatID2032(Parameters.Prompt), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri); } [Theory] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs index 7c2b47108..85ba749c5 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs @@ -811,6 +811,30 @@ public async Task HandleConfigurationRequest_SupportedSubjectTypesAreCorrectlyRe Assert.Equal("custom", Assert.Single(types)); } + [Fact] + public async Task HandleConfigurationRequest_SupportedPromptValuesAreCorrectlyReturned() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.Configure(options => options.PromptValues.Remove(PromptValues.Consent)); + options.Configure(options => options.PromptValues.Remove(PromptValues.Login)); + options.Configure(options => options.PromptValues.Remove(PromptValues.None)); + options.Configure(options => options.PromptValues.Remove(PromptValues.SelectAccount)); + options.Configure(options => options.PromptValues.Add("custom")); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.GetAsync("/.well-known/openid-configuration"); + var types = (string[]?) response[Metadata.PromptValuesSupported]; + + // Assert + Assert.NotNull(types); + Assert.Equal("custom", Assert.Single(types)); + } + [Theory] [InlineData(Algorithms.RsaSha256)] [InlineData(Algorithms.RsaSha384)] diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index a92261432..edb4bc2a8 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -2016,18 +2016,62 @@ public void RegisterClaims_ThrowsAnExceptionForNullClaims() [Theory] [InlineData(null)] [InlineData("")] - public void RegisterClaims_ThrowsAnExceptionForClaim(string claim) + public void RegisterClaims_ThrowsAnExceptionForNullOrEmptyClaim(string claim) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] claims = [claim]; // Act and assert - var exception = Assert.Throws(() => builder.RegisterClaims(claims)); + var exception = Assert.Throws(() => builder.RegisterClaims([claim])); Assert.Equal("claims", exception.ParamName); - Assert.Contains("Claims cannot be null or empty.", exception.Message); + Assert.Contains(SR.FormatID0457("claims"), exception.Message); + } + + [Fact] + public void RegisterPromptValues_PromptValuesAreAdded() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.RegisterPromptValues("custom_value_1", "custom_value_2"); + + var options = GetOptions(services); + + // Assert + Assert.Contains("custom_value_1", options.PromptValues); + Assert.Contains("custom_value_2", options.PromptValues); + } + + [Fact] + public void RegisterPromptValues_ThrowsAnExceptionForNullPromptValues() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.RegisterPromptValues(values: null!)); + Assert.Equal("values", exception.ParamName); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void RegisterPromptValues_ThrowsAnExceptionForNullOrEmptyValue(string value) + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.RegisterPromptValues([value])); + + Assert.Equal("values", exception.ParamName); + Assert.Contains(SR.FormatID0457("values"), exception.Message); } [Fact] @@ -2062,18 +2106,17 @@ public void RegisterScopes_ThrowsAnExceptionForNullScopes() [Theory] [InlineData(null)] [InlineData("")] - public void RegisterScopes_ThrowsAnExceptionForScope(string scope) + public void RegisterScopes_ThrowsAnExceptionForNullOrEmptyScope(string scope) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] scopes = [scope]; // Act and assert - var exception = Assert.Throws(() => builder.RegisterScopes(scopes)); + var exception = Assert.Throws(() => builder.RegisterScopes([scope])); Assert.Equal("scopes", exception.ParamName); - Assert.Contains("Scopes cannot be null or empty.", exception.Message); + Assert.Contains(SR.FormatID0457("scopes"), exception.Message); } [Fact] From 9bcf4b86de635295eaeeed04d2b192b4335bccde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 4 Oct 2024 21:19:15 +0200 Subject: [PATCH 4/4] Update Versions.props to build 6.0.0-preview2 packages --- eng/Versions.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 83970b2b6..63b475993 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -5,9 +5,9 @@ 0 0 $(MajorVersion).$(MinorVersion).$(PatchVersion) - preview1 + preview2 - Preview 1 + Preview 2 false release true