diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index ffb2ad74..c5848836 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(0, 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(0, 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(0, 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(0, 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 2dac1a30..c70f73e6 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(0, 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(0, 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(0, 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(0, 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..14198606 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(0, 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(0, 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(0, 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(0, 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 bdb7f728..244a55da 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(0, 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(0, 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(0, 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(0, 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..379764ce 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs @@ -228,6 +228,10 @@ namespace Microsoft.Extensions.DependencyInjection entity.Property(application => application.ClientId) .IsRequired(required: true); + entity.Property(application => application.Timestamp) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken(); + entity.Property(application => application.Type) .IsRequired(); @@ -255,6 +259,10 @@ namespace Microsoft.Extensions.DependencyInjection entity.Property(authorization => authorization.Subject) .IsRequired(); + entity.Property(authorization => authorization.Timestamp) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken(); + entity.Property(authorization => authorization.Type) .IsRequired(); @@ -271,6 +279,10 @@ namespace Microsoft.Extensions.DependencyInjection { entity.HasKey(scope => scope.Id); + entity.Property(scope => scope.Timestamp) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken(); + entity.Property(scope => scope.Name) .IsRequired(); @@ -288,6 +300,10 @@ namespace Microsoft.Extensions.DependencyInjection entity.Property(token => token.Subject) .IsRequired(); + entity.Property(token => token.Timestamp) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken(); + entity.Property(token => token.Type) .IsRequired(); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index 6f8eb3b1..aab9aa0a 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); } /// @@ -236,7 +231,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) { @@ -246,12 +241,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 b358e0a5..a3d5e8af 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -180,7 +180,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) { @@ -189,12 +189,7 @@ namespace OpenIddict.EntityFrameworkCore Context.Remove(authorization); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } /// @@ -245,18 +240,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 76cf6b70..2254b6b0 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -196,7 +196,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) { @@ -205,12 +205,7 @@ namespace OpenIddict.EntityFrameworkCore Context.Remove(token); - try - { - await Context.SaveChangesAsync(cancellationToken); - } - - catch (DbUpdateConcurrencyException) { } + return Context.SaveChangesAsync(cancellationToken); } /// @@ -347,7 +342,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) { @@ -357,12 +352,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 ba67a925..63e8fa92 100644 --- a/src/OpenIddict/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs @@ -253,8 +253,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 721fcb80..e3c70807 100644 --- a/src/OpenIddict/OpenIddictProvider.Helpers.cs +++ b/src/OpenIddict/OpenIddictProvider.Helpers.cs @@ -314,37 +314,54 @@ namespace OpenIddict return ticket; } - private async Task RevokeAuthorizationAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) + private async Task TryRevokeAuthorizationAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) { var authorizations = context.RequestServices.GetRequiredService>(); var logger = context.RequestServices.GetRequiredService>>(); + // 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; + } + + catch (Exception exception) + { + logger.LogWarning(0, exception, "An exception occurred while trying to revoke the authorization " + + "associated with the token '{Identifier}'.", identifier); - logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", identifier); + return false; + } } - private async Task RevokeTokensAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) + private async Task TryRevokeTokensAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) { var logger = context.RequestServices.GetRequiredService>>(); var tokens = context.RequestServices.GetRequiredService>(); + // 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)) @@ -355,10 +372,103 @@ 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(0, 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) + { + var logger = context.RequestServices.GetRequiredService>>(); + var tokens = context.RequestServices.GetRequiredService>(); + + // 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(0, 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 logger = context.RequestServices.GetRequiredService>>(); + var tokens = context.RequestServices.GetRequiredService>(); + + 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(0, 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 52883a23..a9302cc1 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; @@ -198,8 +199,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(0, 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 472d69c3..3f8dc9d2 100644 --- a/src/OpenIddict/OpenIddictProvider.Signin.cs +++ b/src/OpenIddict/OpenIddictProvider.Signin.cs @@ -11,8 +11,6 @@ using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using OpenIddict.Core; namespace OpenIddict @@ -24,9 +22,6 @@ namespace OpenIddict { var options = (OpenIddictOptions) context.Options; - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - var tokens = context.HttpContext.RequestServices.GetRequiredService>(); - if (context.Request.IsTokenRequest() && (context.Request.IsAuthorizationCodeGrantType() || context.Request.IsRefreshTokenGrantType())) { @@ -81,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; } } @@ -104,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 @@ -112,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 b0ec166b..b4962864 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs @@ -277,6 +277,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() { @@ -295,6 +367,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); @@ -331,6 +406,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()); } @@ -387,6 +531,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()); } @@ -410,6 +556,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); @@ -452,6 +601,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()); @@ -516,6 +667,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()); @@ -653,6 +806,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() {