From 31abb05f4dbace99a39c5728d0a2600999e78ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 11 Oct 2017 23:23:26 +0200 Subject: [PATCH] Introduce timestamp properties and update the Entity Framework stores to avoid swalling DbUpdateConcurrencyException --- .../Managers/OpenIddictApplicationManager.cs | 53 ++++- .../OpenIddictAuthorizationManager.cs | 53 ++++- .../Managers/OpenIddictScopeManager.cs | 56 ++++- .../Managers/OpenIddictTokenManager.cs | 68 +++++- .../OpenIddictExtensions.cs | 24 ++ .../Stores/OpenIddictApplicationStore.cs | 19 +- .../Stores/OpenIddictAuthorizationStore.cs | 21 +- .../Stores/OpenIddictScopeStore.cs | 19 +- .../Stores/OpenIddictTokenStore.cs | 19 +- .../OpenIddictExtensions.cs | 20 ++ .../Stores/OpenIddictApplicationStore.cs | 18 +- .../Stores/OpenIddictAuthorizationStore.cs | 18 +- .../Stores/OpenIddictScopeStore.cs | 18 +- .../Stores/OpenIddictTokenStore.cs | 18 +- .../OpenIddictApplication.cs | 6 + .../OpenIddictAuthorization.cs | 6 + src/OpenIddict.Models/OpenIddictScope.cs | 6 + src/OpenIddict.Models/OpenIddictToken.cs | 6 + src/OpenIddict/OpenIddictProvider.Exchange.cs | 8 +- src/OpenIddict/OpenIddictProvider.Helpers.cs | 124 +++++++++- .../OpenIddictProvider.Revocation.cs | 18 +- src/OpenIddict/OpenIddictProvider.Signin.cs | 45 ++-- .../OpenIddictProviderTests.Signin.cs | 222 ++++++++++++++++++ 23 files changed, 687 insertions(+), 178 deletions(-) diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 1dfc0918..156680b6 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -136,7 +136,18 @@ namespace OpenIddict.Core } await ValidateAsync(application, cancellationToken); - return await Store.CreateAsync(application, cancellationToken); + + try + { + return await Store.CreateAsync(application, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to create a new application."); + + throw; + } } /// @@ -182,7 +193,18 @@ namespace OpenIddict.Core } await ValidateAsync(descriptor, cancellationToken); - return await Store.CreateAsync(descriptor, cancellationToken); + + try + { + return await Store.CreateAsync(descriptor, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to create a new application."); + + throw; + } } /// @@ -193,14 +215,24 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation. /// - public virtual Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public virtual async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { throw new ArgumentNullException(nameof(application)); } - return Store.DeleteAsync(application, cancellationToken); + try + { + await Store.DeleteAsync(application, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to delete an existing application."); + + throw; + } } /// @@ -513,7 +545,18 @@ namespace OpenIddict.Core } await ValidateAsync(application, cancellationToken); - await Store.UpdateAsync(application, cancellationToken); + + try + { + await Store.UpdateAsync(application, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to update an existing application."); + + throw; + } } /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 4ae8eca2..c3fbc1d1 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -93,7 +93,18 @@ namespace OpenIddict.Core } await ValidateAsync(authorization, cancellationToken); - return await Store.CreateAsync(authorization, cancellationToken); + + try + { + return await Store.CreateAsync(authorization, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to create a new authorization."); + + throw; + } } /// @@ -119,7 +130,18 @@ namespace OpenIddict.Core } await ValidateAsync(descriptor, cancellationToken); - return await Store.CreateAsync(descriptor, cancellationToken); + + try + { + return await Store.CreateAsync(descriptor, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to create a new authorization."); + + throw; + } } /// @@ -130,14 +152,24 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation. /// - public virtual Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { throw new ArgumentNullException(nameof(authorization)); } - return Store.DeleteAsync(authorization, cancellationToken); + try + { + await Store.DeleteAsync(authorization, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to delete an existing authorization."); + + throw; + } } /// @@ -344,7 +376,18 @@ namespace OpenIddict.Core } await ValidateAsync(authorization, cancellationToken); - await Store.UpdateAsync(authorization, cancellationToken); + + try + { + await Store.UpdateAsync(authorization, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to update an existing authorization."); + + throw; + } } /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs index cab5dc68..763879b5 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs @@ -79,14 +79,24 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation, whose result returns the scope. /// - public virtual Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public virtual async Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { throw new ArgumentNullException(nameof(scope)); } - return Store.CreateAsync(scope, cancellationToken); + try + { + return await Store.CreateAsync(scope, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to create a new scope."); + + throw; + } } /// @@ -97,14 +107,24 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation, whose result returns the scope. /// - public virtual Task CreateAsync([NotNull] OpenIddictScopeDescriptor descriptor, CancellationToken cancellationToken) + public virtual async Task CreateAsync([NotNull] OpenIddictScopeDescriptor descriptor, CancellationToken cancellationToken) { if (descriptor == null) { throw new ArgumentNullException(nameof(descriptor)); } - return Store.CreateAsync(descriptor, cancellationToken); + try + { + return await Store.CreateAsync(descriptor, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to create a new scope."); + + throw; + } } /// @@ -115,14 +135,24 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation. /// - public virtual Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public virtual async Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { throw new ArgumentNullException(nameof(scope)); } - return Store.DeleteAsync(scope, cancellationToken); + try + { + await Store.DeleteAsync(scope, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to delete an existing scope."); + + throw; + } } /// @@ -188,14 +218,24 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation. /// - public virtual Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { throw new ArgumentNullException(nameof(scope)); } - return Store.UpdateAsync(scope, cancellationToken); + try + { + await Store.UpdateAsync(scope, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to update an existing scope."); + + throw; + } } } } \ No newline at end of file diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 010761d7..0d9f852d 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -88,7 +88,18 @@ namespace OpenIddict.Core } await ValidateAsync(token, cancellationToken); - return await Store.CreateAsync(token, cancellationToken); + + try + { + return await Store.CreateAsync(token, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to create a new token."); + + throw; + } } /// @@ -107,7 +118,46 @@ namespace OpenIddict.Core } await ValidateAsync(descriptor, cancellationToken); - return await Store.CreateAsync(descriptor, cancellationToken); + + try + { + return await Store.CreateAsync(descriptor, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to create a new token."); + + throw; + } + } + + /// + /// Removes an existing token. + /// + /// The token to delete. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + try + { + await Store.DeleteAsync(token, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to delete an existing token."); + + throw; + } } /// @@ -530,14 +580,24 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation. /// - public virtual Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { throw new ArgumentNullException(nameof(token)); } - return Store.UpdateAsync(token, cancellationToken); + try + { + await Store.UpdateAsync(token, cancellationToken); + } + + catch (Exception exception) + { + Logger.LogError(exception, "An exception occurred while trying to update an existing token."); + + throw; + } } /// diff --git a/src/OpenIddict.EntityFramework/OpenIddictExtensions.cs b/src/OpenIddict.EntityFramework/OpenIddictExtensions.cs index 5289f4af..d0f7115d 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictExtensions.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictExtensions.cs @@ -192,6 +192,12 @@ namespace Microsoft.Extensions.DependencyInjection .HasMaxLength(450) .HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute())); + builder.Entity() + .Property(application => application.Timestamp) + .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed) + .IsConcurrencyToken() + .IsRowVersion(); + builder.Entity() .Property(application => application.Type) .IsRequired(); @@ -221,6 +227,12 @@ namespace Microsoft.Extensions.DependencyInjection .Property(authorization => authorization.Subject) .IsRequired(); + builder.Entity() + .Property(authorization => authorization.Timestamp) + .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed) + .IsConcurrencyToken() + .IsRowVersion(); + builder.Entity() .Property(authorization => authorization.Type) .IsRequired(); @@ -241,6 +253,12 @@ namespace Microsoft.Extensions.DependencyInjection .Property(scope => scope.Name) .IsRequired(); + builder.Entity() + .Property(scope => scope.Timestamp) + .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed) + .IsConcurrencyToken() + .IsRowVersion(); + builder.Entity() .ToTable("OpenIddictScopes"); @@ -257,6 +275,12 @@ namespace Microsoft.Extensions.DependencyInjection .Property(token => token.Subject) .IsRequired(); + builder.Entity() + .Property(token => token.Timestamp) + .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed) + .IsConcurrencyToken() + .IsRowVersion(); + builder.Entity() .Property(token => token.Type) .IsRequired(); diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs index 9321d34a..326c1fda 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Immutable; using System.Data.Entity; -using System.Data.Entity.Infrastructure; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -172,7 +171,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public override Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -181,12 +180,7 @@ namespace OpenIddict.EntityFramework Applications.Remove(application); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } /// @@ -256,7 +250,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public override Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -266,12 +260,7 @@ namespace OpenIddict.EntityFramework Applications.Attach(application); Context.Entry(application).State = EntityState.Modified; - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs index f6d199e5..d40f511f 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Immutable; using System.Data.Entity; -using System.Data.Entity.Infrastructure; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -179,7 +178,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public override Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { @@ -187,13 +186,8 @@ namespace OpenIddict.EntityFramework } Authorizations.Remove(authorization); - - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + + return Context.SaveChangesAsync(cancellationToken); } /// @@ -263,18 +257,13 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public override Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { Authorizations.Attach(authorization); Context.Entry(authorization).State = EntityState.Modified; - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs index 9cee4b1c..4732f7b8 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Immutable; using System.Data.Entity; -using System.Data.Entity.Infrastructure; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -141,7 +140,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public override Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { @@ -150,12 +149,7 @@ namespace OpenIddict.EntityFramework Scopes.Remove(scope); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } /// @@ -206,7 +200,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public override Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { @@ -216,12 +210,7 @@ namespace OpenIddict.EntityFramework Scopes.Attach(scope); Context.Entry(scope).State = EntityState.Modified; - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs index 0d44c4b8..ad4c2474 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Immutable; using System.Data.Entity; -using System.Data.Entity.Infrastructure; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -193,7 +192,7 @@ namespace OpenIddict.EntityFramework /// The token to delete. /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. - public override async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) + public override Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -202,12 +201,7 @@ namespace OpenIddict.EntityFramework Tokens.Remove(token); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } /// @@ -359,7 +353,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) + public override Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -369,12 +363,7 @@ namespace OpenIddict.EntityFramework Tokens.Attach(token); Context.Entry(token).State = EntityState.Modified; - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs index 0e061667..8ccd74b3 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs @@ -228,6 +228,11 @@ namespace Microsoft.Extensions.DependencyInjection entity.Property(application => application.ClientId) .IsRequired(required: true); + entity.Property(application => application.Timestamp) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken() + .IsRowVersion(); + entity.Property(application => application.Type) .IsRequired(); @@ -255,6 +260,11 @@ namespace Microsoft.Extensions.DependencyInjection entity.Property(authorization => authorization.Subject) .IsRequired(); + entity.Property(authorization => authorization.Timestamp) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken() + .IsRowVersion(); + entity.Property(authorization => authorization.Type) .IsRequired(); @@ -271,6 +281,11 @@ namespace Microsoft.Extensions.DependencyInjection { entity.HasKey(scope => scope.Id); + entity.Property(scope => scope.Timestamp) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken() + .IsRowVersion(); + entity.Property(scope => scope.Name) .IsRequired(); @@ -288,6 +303,11 @@ namespace Microsoft.Extensions.DependencyInjection entity.Property(token => token.Subject) .IsRequired(); + entity.Property(token => token.Timestamp) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken() + .IsRowVersion(); + entity.Property(token => token.Type) .IsRequired(); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index 9eb55f47..631f0114 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -171,7 +171,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public override Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -180,12 +180,7 @@ namespace OpenIddict.EntityFrameworkCore Context.Remove(application); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } /// @@ -255,7 +250,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public override Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -265,12 +260,7 @@ namespace OpenIddict.EntityFrameworkCore Context.Attach(application); Context.Update(application); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs index bb03776d..b261dd4a 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -178,7 +178,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public override Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { @@ -187,12 +187,7 @@ namespace OpenIddict.EntityFrameworkCore Context.Remove(authorization); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } /// @@ -262,18 +257,13 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public override Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { Context.Attach(authorization); Context.Update(authorization); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs index 8fdbd94a..4520f532 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs @@ -140,7 +140,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public override Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { @@ -149,12 +149,7 @@ namespace OpenIddict.EntityFrameworkCore Context.Remove(scope); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } /// @@ -205,7 +200,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public override Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { @@ -215,12 +210,7 @@ namespace OpenIddict.EntityFrameworkCore Context.Attach(scope); Context.Entry(scope).State = EntityState.Modified; - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs index 00bca764..2dd5186d 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -192,7 +192,7 @@ namespace OpenIddict.EntityFrameworkCore /// The token to delete. /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. - public override async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) + public override Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -201,12 +201,7 @@ namespace OpenIddict.EntityFrameworkCore Context.Remove(token); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } /// @@ -360,7 +355,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) + public override Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -370,12 +365,7 @@ namespace OpenIddict.EntityFrameworkCore Context.Attach(token); Context.Update(token); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file diff --git a/src/OpenIddict.Models/OpenIddictApplication.cs b/src/OpenIddict.Models/OpenIddictApplication.cs index 4ddea077..c24b6fb9 100644 --- a/src/OpenIddict.Models/OpenIddictApplication.cs +++ b/src/OpenIddict.Models/OpenIddictApplication.cs @@ -77,6 +77,12 @@ namespace OpenIddict.Models /// public virtual string RedirectUris { get; set; } + /// + /// Gets or sets the timestamp associated with the current + /// application, which is used as a concurrency token. + /// + public virtual byte[] Timestamp { get; set; } + /// /// Gets the list of the tokens associated with this application. /// diff --git a/src/OpenIddict.Models/OpenIddictAuthorization.cs b/src/OpenIddict.Models/OpenIddictAuthorization.cs index 62b5ac82..76014e6f 100644 --- a/src/OpenIddict.Models/OpenIddictAuthorization.cs +++ b/src/OpenIddict.Models/OpenIddictAuthorization.cs @@ -60,6 +60,12 @@ namespace OpenIddict.Models /// public virtual string Subject { get; set; } + /// + /// Gets or sets the timestamp associated with the current + /// authorization, which is used as a concurrency token. + /// + public virtual byte[] Timestamp { get; set; } + /// /// Gets or sets the list of tokens /// associated with the current authorization. diff --git a/src/OpenIddict.Models/OpenIddictScope.cs b/src/OpenIddict.Models/OpenIddictScope.cs index 5def77a9..5788ae9d 100644 --- a/src/OpenIddict.Models/OpenIddictScope.cs +++ b/src/OpenIddict.Models/OpenIddictScope.cs @@ -25,6 +25,12 @@ namespace OpenIddict.Models /// public class OpenIddictScope where TKey : IEquatable { + /// + /// Gets or sets the timestamp associated with the + /// current scope, which is used as a concurrency token. + /// + public virtual byte[] Timestamp { get; set; } + /// /// Gets or sets the public description /// associated with the current scope. diff --git a/src/OpenIddict.Models/OpenIddictToken.cs b/src/OpenIddict.Models/OpenIddictToken.cs index bf2026e7..b3775fd4 100644 --- a/src/OpenIddict.Models/OpenIddictToken.cs +++ b/src/OpenIddict.Models/OpenIddictToken.cs @@ -85,6 +85,12 @@ namespace OpenIddict.Models /// public virtual string Subject { get; set; } + /// + /// Gets or sets the timestamp associated with the + /// current token, which is used as a concurrency token. + /// + public virtual byte[] Timestamp { get; set; } + /// /// Gets or sets the type of the current token. /// diff --git a/src/OpenIddict/OpenIddictProvider.Exchange.cs b/src/OpenIddict/OpenIddictProvider.Exchange.cs index d52a2500..dfb62d23 100644 --- a/src/OpenIddict/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs @@ -246,8 +246,12 @@ namespace OpenIddict // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. if (await Tokens.IsRedeemedAsync(token, context.HttpContext.RequestAborted)) { - await RevokeAuthorizationAsync(context.Ticket, context.HttpContext); - await RevokeTokensAsync(context.Ticket, context.HttpContext); + // Try to revoke the authorization and the associated tokens. + // If the operation fails, the helpers will automatically log + // and swallow the exception to ensure that a valid error + // response will be returned to the client application. + await TryRevokeAuthorizationAsync(context.Ticket, context.HttpContext); + await TryRevokeTokensAsync(context.Ticket, context.HttpContext); Logger.LogError("The token request was rejected because the authorization code " + "or refresh token '{Identifier}' has already been redeemed.", identifier); diff --git a/src/OpenIddict/OpenIddictProvider.Helpers.cs b/src/OpenIddict/OpenIddictProvider.Helpers.cs index f951490e..34eb6ab0 100644 --- a/src/OpenIddict/OpenIddictProvider.Helpers.cs +++ b/src/OpenIddict/OpenIddictProvider.Helpers.cs @@ -302,31 +302,48 @@ namespace OpenIddict return ticket; } - private async Task RevokeAuthorizationAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) + private async Task TryRevokeAuthorizationAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) { + // Note: if the authorization identifier or the authorization itself + // cannot be found, return true as the authorization doesn't need + // to be revoked if it doesn't exist or is already invalid. var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); if (string.IsNullOrEmpty(identifier)) { - return; + return true; } var authorization = await Authorizations.FindByIdAsync(identifier, context.RequestAborted); if (authorization == null) { - return; + return true; } - await Authorizations.RevokeAsync(authorization, context.RequestAborted); + try + { + await Authorizations.RevokeAsync(authorization, context.RequestAborted); + + Logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", identifier); + + return true; + } - Logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", identifier); + catch (Exception exception) + { + Logger.LogWarning(exception, "An exception occurred while trying to revoke the authorization " + + "associated with the token '{Identifier}'.", identifier); + + return false; + } } - private async Task RevokeTokensAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) + private async Task TryRevokeTokensAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) { + // Note: if the authorization identifier is null, return true as no tokens need to be revoked. var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); if (string.IsNullOrEmpty(identifier)) { - return; + return true; } foreach (var token in await Tokens.FindByAuthorizationIdAsync(identifier, context.RequestAborted)) @@ -337,10 +354,97 @@ namespace OpenIddict continue; } - await Tokens.RevokeAsync(token, context.RequestAborted); + try + { + await Tokens.RevokeAsync(token, context.RequestAborted); + + Logger.LogInformation("The token '{Identifier}' was automatically revoked.", + await Tokens.GetIdAsync(token, context.RequestAborted)); + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An exception occurred while trying to revoke the tokens " + + "associated with the token '{Identifier}'.", + await Tokens.GetIdAsync(token, context.RequestAborted)); + + return false; + } + } + + return true; + } + + private async Task TryRedeemTokenAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) + { + // Note: if the token identifier or the token itself + // cannot be found, return true as the token doesn't need + // to be revoked if it doesn't exist or is already invalid. + var identifier = ticket.GetTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + return true; + } + + var token = await Tokens.FindByIdAsync(identifier, context.RequestAborted); + if (token == null) + { + return true; + } + + try + { + await Tokens.RedeemAsync(token, context.RequestAborted); + + Logger.LogInformation("The token '{Identifier}' was automatically marked as redeemed.", identifier); + + return true; + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An exception occurred while trying to " + + "redeem the token '{Identifier}'.", identifier); + + return false; + } + } + + private async Task TryExtendTokenAsync( + [NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context, [NotNull] OpenIddictOptions options) + { + var identifier = ticket.GetTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + return false; + } + + var token = await Tokens.FindByIdAsync(identifier, context.RequestAborted); + if (token == null) + { + return false; + } + + try + { + // Compute the new expiration date of the refresh token. + var date = options.SystemClock.UtcNow; + date += ticket.GetRefreshTokenLifetime() ?? options.RefreshTokenLifetime; + + await Tokens.ExtendAsync(token, date, context.RequestAborted); + + Logger.LogInformation("The expiration date of the refresh token '{Identifier}' " + + "was automatically updated: {Date}.", identifier, date); + + return true; + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An exception occurred while trying to update the " + + "expiration date of the token '{Identifier}'.", identifier); - Logger.LogInformation("The token '{Identifier}' was automatically revoked.", - await Tokens.GetIdAsync(token, context.RequestAborted)); + return false; } } } diff --git a/src/OpenIddict/OpenIddictProvider.Revocation.cs b/src/OpenIddict/OpenIddictProvider.Revocation.cs index fbb8a0df..c3d1c960 100644 --- a/src/OpenIddict/OpenIddictProvider.Revocation.cs +++ b/src/OpenIddict/OpenIddictProvider.Revocation.cs @@ -4,6 +4,7 @@ * the license and the contributors participating to this project. */ +using System; using System.Diagnostics; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; @@ -190,8 +191,21 @@ namespace OpenIddict return; } - // Revoke the token. - await Tokens.RevokeAsync(token, context.HttpContext.RequestAborted); + // Try to revoke the token. If an exception is thrown, + // log and swallow it to ensure that a valid response + // will be returned to the client application. + try + { + await Tokens.RevokeAsync(token, context.HttpContext.RequestAborted); + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An exception occurred while trying to revoke the authorization " + + "associated with the token '{Identifier}'.", identifier); + + return; + } Logger.LogInformation("The token '{Identifier}' was successfully revoked.", identifier); diff --git a/src/OpenIddict/OpenIddictProvider.Signin.cs b/src/OpenIddict/OpenIddictProvider.Signin.cs index ff7fcb8a..3f8dc9d2 100644 --- a/src/OpenIddict/OpenIddictProvider.Signin.cs +++ b/src/OpenIddict/OpenIddictProvider.Signin.cs @@ -11,7 +11,6 @@ using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; using OpenIddict.Core; namespace OpenIddict @@ -77,22 +76,18 @@ namespace OpenIddict return; } - // Extract the token identifier from the authentication ticket. - var identifier = context.Ticket.GetTokenId(); - Debug.Assert(!string.IsNullOrEmpty(identifier), - "The authentication ticket should contain a ticket identifier."); - // If rolling tokens are enabled or if the request is a grant_type=authorization_code request, // mark the authorization code or the refresh token as redeemed to prevent future reuses. // See https://tools.ietf.org/html/rfc6749#section-6 for more information. if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType()) { - var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); - if (token != null) + if (!await TryRedeemTokenAsync(context.Ticket, context.HttpContext)) { - await Tokens.RedeemAsync(token, context.HttpContext.RequestAborted); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The specified authorization code is no longer valid."); - Logger.LogInformation("The token '{Identifier}' was automatically marked as redeemed.", identifier); + return; } } @@ -100,7 +95,14 @@ namespace OpenIddict // with the authorization if the request is a grant_type=refresh_token request. if (options.UseRollingTokens && context.Request.IsRefreshTokenGrantType()) { - await RevokeTokensAsync(context.Ticket, context.HttpContext); + if (!await TryRevokeTokensAsync(context.Ticket, context.HttpContext)) + { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The specified refresh token is no longer valid."); + + return; + } } // When rolling tokens are disabled, extend the expiration date @@ -108,24 +110,17 @@ namespace OpenIddict // with a new expiration date if sliding expiration was not disabled. else if (options.UseSlidingExpiration && context.Request.IsRefreshTokenGrantType()) { - var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); - if (token != null) + if (!await TryExtendTokenAsync(context.Ticket, context.HttpContext, options)) { - // Compute the new expiration date of the refresh token. - var date = context.Options.SystemClock.UtcNow + - (context.Ticket.GetRefreshTokenLifetime() ?? - context.Options.RefreshTokenLifetime); - - await Tokens.ExtendAsync(token, date, context.HttpContext.RequestAborted); - - Logger.LogInformation("The expiration date of the refresh token '{Identifier}' " + - "was automatically updated: {Date}.", identifier, date); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The specified refresh token is no longer valid."); - context.IncludeRefreshToken = false; + return; } - // If the refresh token entry could not be - // found in the database, generate a new one. + // Prevent the OpenID Connect server from returning a new refresh token. + context.IncludeRefreshToken = false; } } diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs index cb0b1462..aeac45c2 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs @@ -275,6 +275,78 @@ namespace OpenIddict.Tests Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); } + [Fact] + public async Task ProcessSigninResponse_ReturnsErrorResponseWhenRedeemingAuthorizationCodeFails() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.RedeemAsync(token, It.IsAny())) + .ThrowsAsync(new Exception()); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); + } + [Fact] public async Task ProcessSigninResponse_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled() { @@ -293,6 +365,9 @@ namespace OpenIddict.Tests var format = new Mock>(); + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) .Returns(ticket); @@ -329,6 +404,75 @@ namespace OpenIddict.Tests }); // Assert + Assert.NotNull(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_ReturnsErrorResponseWhenRedeemingRefreshTokenFails() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.RedeemAsync(token, It.IsAny())) + .ThrowsAsync(new Exception()); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.UseRollingTokens(); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); } @@ -385,6 +529,8 @@ namespace OpenIddict.Tests }); // Assert + Assert.Null(response.RefreshToken); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Never()); } @@ -408,6 +554,9 @@ namespace OpenIddict.Tests var format = new Mock>(); + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) .Returns(ticket); @@ -450,6 +599,8 @@ namespace OpenIddict.Tests }); // Assert + Assert.NotNull(response.RefreshToken); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Once()); @@ -514,6 +665,8 @@ namespace OpenIddict.Tests }); // Assert + Assert.Null(response.RefreshToken); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Never()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Never()); @@ -651,6 +804,75 @@ namespace OpenIddict.Tests It.IsAny()), Times.Never()); } + [Fact] + public async Task ProcessSigninResponse_ReturnsErrorResponseWhenExtendingLifetimeOfExistingTokenFailed() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.ExtendAsync(token, It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception()); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = TimeSpan.FromDays(10); + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, + new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), + It.IsAny()), Times.Once()); + } + [Fact] public async Task ProcessSigninResponse_AdHocAuthorizationIsAutomaticallyCreated() {