Browse Source

Add log messages to inform users when application claims are not included in introspection responses

pull/1186/head
Kévin Chalet 6 years ago
parent
commit
6cf1f72844
  1. 78
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 6
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  3. 18
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  4. 2
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  5. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  6. 8
      src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
  7. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  8. 8
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  9. 4
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs

78
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -406,6 +406,9 @@ To enable DI support, call 'services.AddQuartz(options => options.UseMicrosof
<data name="ID0083" xml:space="preserve"> <data name="ID0083" xml:space="preserve">
<value>Reference tokens cannot be used when disabling token storage.</value> <value>Reference tokens cannot be used when disabling token storage.</value>
</data> </data>
<data name="ID0084" xml:space="preserve">
<value>The device grant must be allowed when enabling the device endpoint.</value>
</data>
<data name="ID0085" xml:space="preserve"> <data name="ID0085" xml:space="preserve">
<value>At least one encryption key must be registered in the OpenIddict server options. <value>At least one encryption key must be registered in the OpenIddict server options.
Consider registering a certificate using 'services.AddOpenIddict().AddServer().AddEncryptionCertificate()' or 'services.AddOpenIddict().AddServer().AddDevelopmentEncryptionCertificate()' or call 'services.AddOpenIddict().AddServer().AddEphemeralEncryptionKey()' to use an ephemeral key.</value> Consider registering a certificate using 'services.AddOpenIddict().AddServer().AddEncryptionCertificate()' or 'services.AddOpenIddict().AddServer().AddDevelopmentEncryptionCertificate()' or call 'services.AddOpenIddict().AddServer().AddEphemeralEncryptionKey()' to use an ephemeral key.</value>
@ -1116,9 +1119,6 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag
<data name="ID0286" xml:space="preserve"> <data name="ID0286" xml:space="preserve">
<value>The specified principal doesn't contain a valid claims-based identity.</value> <value>The specified principal doesn't contain a valid claims-based identity.</value>
</data> </data>
<data name="ID0287" xml:space="preserve">
<value>The device grant must be allowed when enabling the device endpoint.</value>
</data>
<data name="ID2000" xml:space="preserve"> <data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value> <value>The security token is missing.</value>
</data> </data>
@ -1200,6 +1200,9 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag
<data name="ID2026" xml:space="preserve"> <data name="ID2026" xml:space="preserve">
<value>The specified user code is no longer valid.</value> <value>The specified user code is no longer valid.</value>
</data> </data>
<data name="ID2027" xml:space="preserve">
<value>The client application is not allowed to use the device code flow.</value>
</data>
<data name="ID2028" xml:space="preserve"> <data name="ID2028" xml:space="preserve">
<value>The '{0}' parameter is not supported.</value> <value>The '{0}' parameter is not supported.</value>
</data> </data>
@ -1266,6 +1269,9 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag
<data name="ID2049" xml:space="preserve"> <data name="ID2049" xml:space="preserve">
<value>The client application is not allowed to use the hybrid flow.</value> <value>The client application is not allowed to use the hybrid flow.</value>
</data> </data>
<data name="ID2050" xml:space="preserve">
<value>The client type cannot be null or empty.</value>
</data>
<data name="ID2051" xml:space="preserve"> <data name="ID2051" xml:space="preserve">
<value>This client application is not allowed to use the specified scope.</value> <value>This client application is not allowed to use the specified scope.</value>
</data> </data>
@ -1296,6 +1302,12 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag
<data name="ID2060" xml:space="preserve"> <data name="ID2060" xml:space="preserve">
<value>A scope with the same name already exists.</value> <value>A scope with the same name already exists.</value>
</data> </data>
<data name="ID2061" xml:space="preserve">
<value>Callback URLs cannot be null or empty.</value>
</data>
<data name="ID2062" xml:space="preserve">
<value>Callback URLs must be valid absolute URLs.</value>
</data>
<data name="ID2063" xml:space="preserve"> <data name="ID2063" xml:space="preserve">
<value>This client application is not allowed to use the token endpoint.</value> <value>This client application is not allowed to use the token endpoint.</value>
</data> </data>
@ -1461,18 +1473,6 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag
<data name="ID2117" xml:space="preserve"> <data name="ID2117" xml:space="preserve">
<value>The specified authorization type is not supported by the default token manager.</value> <value>The specified authorization type is not supported by the default token manager.</value>
</data> </data>
<data name="ID2118" xml:space="preserve">
<value>The client type cannot be null or empty.</value>
</data>
<data name="ID2119" xml:space="preserve">
<value>Callback URLs cannot be null or empty.</value>
</data>
<data name="ID2120" xml:space="preserve">
<value>Callback URLs must be valid absolute URLs.</value>
</data>
<data name="ID2121" xml:space="preserve">
<value>The client application is not allowed to use the device code flow.</value>
</data>
<data name="ID4000" xml:space="preserve"> <data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value> <value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data> </data>
@ -1833,9 +1833,15 @@ The principal used to create the token contained the following claims: {Claims}.
<data name="ID6104" xml:space="preserve"> <data name="ID6104" xml:space="preserve">
<value>The introspection request was rejected because the received token was of an unsupported type.</value> <value>The introspection request was rejected because the received token was of an unsupported type.</value>
</data> </data>
<data name="ID6105" xml:space="preserve">
<value>Potentially sensitive application claims were excluded from the introspection response as the client '{ClientId}' was not explicitly listed as an audience.</value>
</data>
<data name="ID6106" xml:space="preserve"> <data name="ID6106" xml:space="preserve">
<value>The introspection request was rejected because the access token was issued to a different client or for another resource server.</value> <value>The introspection request was rejected because the access token was issued to a different client or for another resource server.</value>
</data> </data>
<data name="ID6107" xml:space="preserve">
<value>Potentially sensitive application claims were excluded from the introspection response as the client '{ClientId}' is a public application.</value>
</data>
<data name="ID6108" xml:space="preserve"> <data name="ID6108" xml:space="preserve">
<value>The introspection request was rejected because the refresh token was issued to a different client.</value> <value>The introspection request was rejected because the refresh token was issued to a different client.</value>
</data> </data>
@ -1866,9 +1872,15 @@ The principal used to create the token contained the following claims: {Claims}.
<data name="ID6117" xml:space="preserve"> <data name="ID6117" xml:space="preserve">
<value>The revocation request was rejected because the received token was of an unsupported type.</value> <value>The revocation request was rejected because the received token was of an unsupported type.</value>
</data> </data>
<data name="ID6118" xml:space="preserve">
<value>The device request was rejected because the application '{ClientId}' was not allowed to use the device code flow.</value>
</data>
<data name="ID6119" xml:space="preserve"> <data name="ID6119" xml:space="preserve">
<value>The revocation request was rejected because the access token was issued to a different client or for another resource server.</value> <value>The revocation request was rejected because the access token was issued to a different client or for another resource server.</value>
</data> </data>
<data name="ID6120" xml:space="preserve">
<value>The device request was rejected because the application '{ClientId}' was not allowed to request the '{Scope}' scope.</value>
</data>
<data name="ID6121" xml:space="preserve"> <data name="ID6121" xml:space="preserve">
<value>The revocation request was rejected because the refresh token was issued to a different client.</value> <value>The revocation request was rejected because the refresh token was issued to a different client.</value>
</data> </data>
@ -1987,10 +1999,10 @@ The principal used to create the token contained the following claims: {Claims}.
<value>Client authentication cannot be enforced for public applications.</value> <value>Client authentication cannot be enforced for public applications.</value>
</data> </data>
<data name="ID6160" xml:space="preserve"> <data name="ID6160" xml:space="preserve">
<value>Client authentication failed for {Client} because no client secret was associated with the application.</value> <value>Client authentication failed for {ClientId} because no client secret was associated with the application.</value>
</data> </data>
<data name="ID6161" xml:space="preserve"> <data name="ID6161" xml:space="preserve">
<value>Client authentication failed for {Client}.</value> <value>Client authentication failed for {ClientId}.</value>
</data> </data>
<data name="ID6162" xml:space="preserve"> <data name="ID6162" xml:space="preserve">
<value>Client validation failed because '{RedirectUri}' was not a valid redirect_uri for {Client}.</value> <value>Client validation failed because '{RedirectUri}' was not a valid redirect_uri for {Client}.</value>
@ -2008,45 +2020,39 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6166" xml:space="preserve"> <data name="ID6166" xml:space="preserve">
<value>An exception occurred while trying to revoke the authorization '{Identifier}'.</value> <value>An exception occurred while trying to revoke the authorization '{Identifier}'.</value>
</data> </data>
<data name="ID6171" xml:space="preserve"> <data name="ID6167" xml:space="preserve">
<value>A signing key of type '{Type}' was ignored because its EC curve couldn't be inferred.</value>
</data>
<data name="ID6168" xml:space="preserve">
<value>The token '{Identifier}' was successfully marked as redeemed.</value> <value>The token '{Identifier}' was successfully marked as redeemed.</value>
</data> </data>
<data name="ID6172" xml:space="preserve"> <data name="ID6169" xml:space="preserve">
<value>A concurrency exception occurred while trying to redeem the token '{Identifier}'.</value> <value>A concurrency exception occurred while trying to redeem the token '{Identifier}'.</value>
</data> </data>
<data name="ID6173" xml:space="preserve"> <data name="ID6170" xml:space="preserve">
<value>An exception occurred while trying to redeem the token '{Identifier}'.</value> <value>An exception occurred while trying to redeem the token '{Identifier}'.</value>
</data> </data>
<data name="ID6174" xml:space="preserve"> <data name="ID6171" xml:space="preserve">
<value>The token '{Identifier}' was successfully marked as rejected.</value> <value>The token '{Identifier}' was successfully marked as rejected.</value>
</data> </data>
<data name="ID6175" xml:space="preserve"> <data name="ID6172" xml:space="preserve">
<value>A concurrency exception occurred while trying to reject the token '{Identifier}'.</value> <value>A concurrency exception occurred while trying to reject the token '{Identifier}'.</value>
</data> </data>
<data name="ID6176" xml:space="preserve"> <data name="ID6173" xml:space="preserve">
<value>An exception occurred while trying to reject the token '{Identifier}'.</value> <value>An exception occurred while trying to reject the token '{Identifier}'.</value>
</data> </data>
<data name="ID6177" xml:space="preserve"> <data name="ID6174" xml:space="preserve">
<value>The token '{Identifier}' was successfully revoked.</value> <value>The token '{Identifier}' was successfully revoked.</value>
</data> </data>
<data name="ID6178" xml:space="preserve"> <data name="ID6175" xml:space="preserve">
<value>A concurrency exception occurred while trying to revoke the token '{Identifier}'.</value> <value>A concurrency exception occurred while trying to revoke the token '{Identifier}'.</value>
</data> </data>
<data name="ID6179" xml:space="preserve"> <data name="ID6176" xml:space="preserve">
<value>An exception occurred while trying to revoke the token '{Identifier}'.</value> <value>An exception occurred while trying to revoke the token '{Identifier}'.</value>
</data> </data>
<data name="ID6180" xml:space="preserve"> <data name="ID6177" xml:space="preserve">
<value>A signing key of type '{Type}' was ignored because its EC curve couldn't be inferred.</value>
</data>
<data name="ID6181" xml:space="preserve">
<value>The authorization request was rejected because the application '{ClientId}' was not allowed to use the '{ResponseType}' response type.</value> <value>The authorization request was rejected because the application '{ClientId}' was not allowed to use the '{ResponseType}' response type.</value>
</data> </data>
<data name="ID6182" xml:space="preserve">
<value>The device request was rejected because the application '{ClientId}' was not allowed to use the device code flow.</value>
</data>
<data name="ID6183" xml:space="preserve">
<value>The device request was rejected because the application '{ClientId}' was not allowed to request the '{Scope}' scope.</value>
</data>
<data name="ID8000" xml:space="preserve"> <data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value> <value>https://documentation.openiddict.com/errors/{0}</value>
</data> </data>

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

@ -1179,7 +1179,7 @@ namespace OpenIddict.Core
var type = await Store.GetClientTypeAsync(application, cancellationToken); var type = await Store.GetClientTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type)) if (string.IsNullOrEmpty(type))
{ {
yield return new ValidationResult(SR.GetResourceString(SR.ID2118)); yield return new ValidationResult(SR.GetResourceString(SR.ID2050));
} }
else else
@ -1214,7 +1214,7 @@ namespace OpenIddict.Core
// Ensure the address is not null or empty. // Ensure the address is not null or empty.
if (string.IsNullOrEmpty(address)) if (string.IsNullOrEmpty(address))
{ {
yield return new ValidationResult(SR.GetResourceString(SR.ID2119)); yield return new ValidationResult(SR.GetResourceString(SR.ID2061));
break; break;
} }
@ -1222,7 +1222,7 @@ namespace OpenIddict.Core
// Ensure the address is a valid absolute URL. // Ensure the address is a valid absolute URL.
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString()) if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString())
{ {
yield return new ValidationResult(SR.GetResourceString(SR.ID2120)); yield return new ValidationResult(SR.GetResourceString(SR.ID2062));
break; break;
} }

18
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -1061,21 +1061,21 @@ namespace OpenIddict.Core
{ {
await UpdateAsync(token, cancellationToken); await UpdateAsync(token, cancellationToken);
Logger.LogInformation(SR.GetResourceString(SR.ID6171), await Store.GetIdAsync(token, cancellationToken)); Logger.LogInformation(SR.GetResourceString(SR.ID6168), await Store.GetIdAsync(token, cancellationToken));
return true; return true;
} }
catch (ConcurrencyException exception) catch (ConcurrencyException exception)
{ {
Logger.LogDebug(exception, SR.GetResourceString(SR.ID6172), await Store.GetIdAsync(token, cancellationToken)); Logger.LogDebug(exception, SR.GetResourceString(SR.ID6169), await Store.GetIdAsync(token, cancellationToken));
return false; return false;
} }
catch (Exception exception) catch (Exception exception)
{ {
Logger.LogWarning(exception, SR.GetResourceString(SR.ID6173), await Store.GetIdAsync(token, cancellationToken)); Logger.LogWarning(exception, SR.GetResourceString(SR.ID6170), await Store.GetIdAsync(token, cancellationToken));
return false; return false;
} }
@ -1100,21 +1100,21 @@ namespace OpenIddict.Core
{ {
await UpdateAsync(token, cancellationToken); await UpdateAsync(token, cancellationToken);
Logger.LogInformation(SR.GetResourceString(SR.ID6174), await Store.GetIdAsync(token, cancellationToken)); Logger.LogInformation(SR.GetResourceString(SR.ID6171), await Store.GetIdAsync(token, cancellationToken));
return true; return true;
} }
catch (ConcurrencyException exception) catch (ConcurrencyException exception)
{ {
Logger.LogDebug(exception, SR.GetResourceString(SR.ID6175), await Store.GetIdAsync(token, cancellationToken)); Logger.LogDebug(exception, SR.GetResourceString(SR.ID6172), await Store.GetIdAsync(token, cancellationToken));
return false; return false;
} }
catch (Exception exception) catch (Exception exception)
{ {
Logger.LogWarning(exception, SR.GetResourceString(SR.ID6176), await Store.GetIdAsync(token, cancellationToken)); Logger.LogWarning(exception, SR.GetResourceString(SR.ID6173), await Store.GetIdAsync(token, cancellationToken));
return false; return false;
} }
@ -1139,21 +1139,21 @@ namespace OpenIddict.Core
{ {
await UpdateAsync(token, cancellationToken); await UpdateAsync(token, cancellationToken);
Logger.LogInformation(SR.GetResourceString(SR.ID6177), await Store.GetIdAsync(token, cancellationToken)); Logger.LogInformation(SR.GetResourceString(SR.ID6174), await Store.GetIdAsync(token, cancellationToken));
return true; return true;
} }
catch (ConcurrencyException exception) catch (ConcurrencyException exception)
{ {
Logger.LogDebug(exception, SR.GetResourceString(SR.ID6178), await Store.GetIdAsync(token, cancellationToken)); Logger.LogDebug(exception, SR.GetResourceString(SR.ID6175), await Store.GetIdAsync(token, cancellationToken));
return false; return false;
} }
catch (Exception exception) catch (Exception exception)
{ {
Logger.LogWarning(exception, SR.GetResourceString(SR.ID6179), await Store.GetIdAsync(token, cancellationToken)); Logger.LogWarning(exception, SR.GetResourceString(SR.ID6176), await Store.GetIdAsync(token, cancellationToken));
return false; return false;
} }

2
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -113,7 +113,7 @@ namespace OpenIddict.Server
// Ensure the device grant is allowed when the device endpoint is enabled. // Ensure the device grant is allowed when the device endpoint is enabled.
if (options.DeviceEndpointUris.Count > 0 && !options.GrantTypes.Contains(GrantTypes.DeviceCode)) if (options.DeviceEndpointUris.Count > 0 && !options.GrantTypes.Contains(GrantTypes.DeviceCode))
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0287)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0084));
} }
// Ensure the grant types/response types configuration is consistent. // Ensure the grant types/response types configuration is consistent.

2
src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

@ -1448,7 +1448,7 @@ namespace OpenIddict.Server
// Reject requests that specify a response_type for which no permission was granted. // Reject requests that specify a response_type for which no permission was granted.
if (!await HasPermissionAsync(context.Request.GetResponseTypes())) if (!await HasPermissionAsync(context.Request.GetResponseTypes()))
{ {
context.Logger.LogError(SR.GetResourceString(SR.ID6181), context.ClientId, context.Request.ResponseType); context.Logger.LogError(SR.GetResourceString(SR.ID6177), context.ClientId, context.Request.ResponseType);
context.Reject( context.Reject(
error: Errors.UnauthorizedClient, error: Errors.UnauthorizedClient,

8
src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs

@ -776,12 +776,12 @@ namespace OpenIddict.Server
// Reject the request if the application is not allowed to use the device code grant. // Reject the request if the application is not allowed to use the device code grant.
if (!await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.DeviceCode)) if (!await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.DeviceCode))
{ {
context.Logger.LogError(SR.GetResourceString(SR.ID6182), context.ClientId); context.Logger.LogError(SR.GetResourceString(SR.ID6118), context.ClientId);
context.Reject( context.Reject(
error: Errors.UnauthorizedClient, error: Errors.UnauthorizedClient,
description: SR.GetResourceString(SR.ID2121), description: SR.GetResourceString(SR.ID2027),
uri: SR.FormatID8000(SR.ID2121)); uri: SR.FormatID8000(SR.ID2027));
return; return;
} }
@ -791,7 +791,7 @@ namespace OpenIddict.Server
if (context.Request.HasScope(Scopes.OfflineAccess) && if (context.Request.HasScope(Scopes.OfflineAccess) &&
!await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.RefreshToken)) !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.RefreshToken))
{ {
context.Logger.LogError(SR.GetResourceString(SR.ID6183), context.ClientId, Scopes.OfflineAccess); context.Logger.LogError(SR.GetResourceString(SR.ID6120), context.ClientId, Scopes.OfflineAccess);
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,

2
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -1199,7 +1199,7 @@ namespace OpenIddict.Server
if (string.IsNullOrEmpty(curve)) if (string.IsNullOrEmpty(curve))
{ {
context.Logger.LogWarning(SR.GetResourceString(SR.ID6180), credentials.Key.GetType().Name); context.Logger.LogWarning(SR.GetResourceString(SR.ID6167), credentials.Key.GetType().Name);
continue; continue;
} }

8
src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs

@ -968,6 +968,8 @@ namespace OpenIddict.Server
// application-specific claims contained in the introspected access/identity token. // application-specific claims contained in the introspected access/identity token.
if (!context.Principal.HasAudience(context.Request.ClientId)) if (!context.Principal.HasAudience(context.Request.ClientId))
{ {
context.Logger.LogInformation(SR.GetResourceString(SR.ID6105), context.Request.ClientId);
return; return;
} }
@ -980,6 +982,8 @@ namespace OpenIddict.Server
// Public clients are not allowed to access sensitive claims as authentication cannot be enforced. // Public clients are not allowed to access sensitive claims as authentication cannot be enforced.
if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public))
{ {
context.Logger.LogInformation(SR.GetResourceString(SR.ID6107), context.Request.ClientId);
return; return;
} }
@ -992,8 +996,8 @@ namespace OpenIddict.Server
// Make sure to always update this list when adding new built-in claim properties. // Make sure to always update this list when adding new built-in claim properties.
var type = group.Key; var type = group.Key;
if (type is Claims.Audience or Claims.ExpiresAt or Claims.IssuedAt or if (type is Claims.Audience or Claims.ExpiresAt or Claims.IssuedAt or
Claims.Issuer or Claims.NotBefore or Claims.Scope or Claims.Issuer or Claims.NotBefore or Claims.Scope or
Claims.Subject or Claims.TokenType or Claims.TokenUsage) Claims.Subject or Claims.TokenType or Claims.TokenUsage)
{ {
continue; continue;
} }

4
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs

@ -572,8 +572,8 @@ namespace OpenIddict.Server.IntegrationTests
// Assert // Assert
Assert.Equal(Errors.UnauthorizedClient, response.Error); Assert.Equal(Errors.UnauthorizedClient, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2121), response.ErrorDescription); Assert.Equal(SR.GetResourceString(SR.ID2027), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2121), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2027), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,

Loading…
Cancel
Save