From c7567f9f74014690ca672d09b8c9748b9d740bc1 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 10 May 2017 13:26:19 -0700 Subject: [PATCH] Add support for exposing identity as a service --- Identity.sln | 212 ++++- build/dependencies.props | 2 + .../DeveloperCertificateMiddleware.cs | 137 +++ .../DeveloperCertificateOptions.cs | 12 + .../IdentityApplicationBuilderExtensions.cs | 19 + ...etCore.Diagnostics.Identity.Service.csproj | 44 + .../Strings.Designer.cs | 146 ++++ .../Strings.resx | 149 ++++ .../DeveloperCertificateErrorPage.Designer.cs | 247 ++++++ .../DeveloperCertificateErrorPage.cshtml | 87 ++ .../Views/DeveloperCertificateViewModel.cs | 13 + .../Views/ErrorPage.css | 78 ++ ...developer-certificate-diagnostics-page.cmd | 1 + .../ApplicationScope.cs | 49 ++ .../AuthorizationGrant.cs | 57 ++ .../AuthorizationRequest.cs | 50 ++ .../AuthorizationResponse.cs | 14 + .../Claims/ClaimsGenerationContext.cs | 16 + .../Claims/ITokenClaimsManager.cs | 12 + .../Claims/ITokenClaimsProvider.cs | 13 + .../ConfigurationContext.cs | 19 + .../IAuthorizationRequestFactory.cs | 13 + .../IAuthorizationResponseFactory.cs | 12 + ...IAuthorizationResponseParameterProvider.cs | 14 + .../IConfigurationManager.cs | 13 + .../IConfigurationMetadataProvider.cs | 15 + .../IIdentityServiceBuilder.cs | 15 + .../IKeySetMetadataProvider.cs | 13 + .../ILogoutRequestFactory.cs | 13 + .../ISigningCredentialsPolicyProvider.cs | 14 + .../ISigningCredentialsSource.cs | 13 + .../ITokenManager.cs | 14 + .../ITokenRequestFactory.cs | 13 + .../ITokenResponseFactory.cs | 13 + .../ITokenResponseParameterProvider.cs | 15 + .../IdentityServiceClaimTypes.cs | 33 + .../IdentityServiceErrorCodes.cs | 10 + .../Issuers/IAccessTokenIssuer.cs | 12 + .../Issuers/IAuthorizationCodeIssuer.cs | 14 + .../Issuers/IIdTokenIssuer.cs | 12 + .../Issuers/IRefreshTokenIssuer.cs | 14 + .../Issuers/ITokenHasher.cs | 10 + .../Issuers/TokenResult.cs | 26 + .../LogoutRequest.cs | 30 + ...tCore.Identity.Service.Abstractions.csproj | 19 + .../PromptValues.cs | 13 + .../ProtocolError.cs | 23 + .../RedirectUriResolutionResult.cs | 36 + .../RequestGrants.cs | 17 + .../ScopeResolutionResult.cs | 37 + .../SigningCredentialsDescriptor.cs | 51 ++ .../TokenGeneratingContext.cs | 134 +++ .../TokenKinds.cs | 10 + .../TokenRequest.cs | 56 ++ .../TokenTypes.cs | 13 + .../Tokens/AccessToken.cs | 33 + .../Tokens/AuthorizationCode.cs | 36 + .../Tokens/IdToken.cs | 35 + .../Tokens/RefreshToken.cs | 34 + .../Tokens/Token.cs | 114 +++ .../IAuthorizationRequestValidator.cs | 12 + .../Validation/IClientIdValidator.cs | 13 + .../Validation/IRedirectUriResolver.cs | 13 + .../Validation/IScopeResolver.cs | 13 + .../Validation/ITimeStampManager.cs | 20 + .../Validation/ITokenRequestValidator.cs | 12 + .../AuthorizationCodeIssuer.cs | 63 ++ .../AuthorizationRequestFactory.cs | 398 +++++++++ .../Claims/DefaultTokenClaimsManager.cs | 28 + .../Claims/DefaultTokenClaimsProvider.cs | 145 +++ .../GrantedTokensTokenClaimsProvider.cs | 53 ++ .../Claims/NonceTokenClaimsProvider.cs | 33 + .../Claims/ScopesTokenClaimsProvider.cs | 86 ++ .../Claims/TimestampsTokenClaimsProvider.cs | 66 ++ .../Claims/TokenHashTokenClaimsProvider.cs | 53 ++ .../CryptographyHelpers.cs | 99 +++ .../DefaultAuthorizationResponseFactory.cs | 35 + ...tAuthorizationResponseParameterProvider.cs | 56 ++ .../DefaultConfigurationManager.cs | 43 + ...DefaultSigningCredentialsPolicyProvider.cs | 88 ++ .../DefaultSigningCredentialsSource.cs | 88 ++ .../DefaultTokenResponseFactory.cs | 31 + .../DefaultTokenResponseParameterProvider.cs | 76 ++ .../FormPostResponseGenerator.cs | 63 ++ .../FragmentResponseGenerator.cs | 62 ++ .../IdentityServiceBuilderExtensions.cs | 72 ++ .../JwtAccessTokenIssuer.cs | 90 ++ .../JwtIdTokenIssuer.cs | 109 +++ .../LogoutRequestFactory.cs | 52 ++ .../DefaultConfigurationMetadataProvider.cs | 45 + .../Metadata/DefaultKeySetMetadataProvider.cs | 85 ++ ...ft.AspNetCore.Identity.Service.Core.csproj | 25 + ...dentityServiceAuthorizationOptionsSetup.cs | 23 + .../Options/IdentityServiceOptions.cs | 35 + .../IdentityServiceOptionsDefaultSetup.cs | 117 +++ .../Options/TokenMapping.cs | 27 + .../Options/TokenOptions.cs | 16 + .../Options/TokenValueCardinality.cs | 12 + .../Options/TokenValueDescriptor.cs | 24 + .../PrincipalExtensions.cs | 18 + .../ProtocolErrorProvider.cs | 154 ++++ .../QueryResponseGenerator.cs | 42 + .../RefreshTokenIssuer.cs | 78 ++ .../RequestParametersHelper.cs | 55 ++ .../AuthorizationCodeConverter.cs | 16 + .../Serialization/RefreshTokenConverter.cs | 16 + .../Serialization/TokenConverter.cs | 144 +++ .../Serialization/TokenDataSerializer.cs | 79 ++ .../TimeStampManager.cs | 48 + .../TokenHasher.cs | 35 + .../TokenManager.cs | 71 ++ .../TokenMapper.cs | 59 ++ .../TokenRequestFactory.cs | 230 +++++ .../ApplicationStore.cs | 684 +++++++++++++++ .../IdentityServiceApplication.cs | 49 ++ .../IdentityServiceApplicationClaim.cs | 22 + .../IdentityServiceBuilderExtensions.cs | 48 + .../IdentityServiceDbContext.cs | 165 ++++ .../IdentityServiceRedirectUri.cs | 13 + .../IdentityServiceScope.cs | 12 + ...dentity.Service.EntityFrameworkCore.csproj | 21 + .../EnableIntegratedWebClientAttribute.cs | 11 + .../IntegratedWebApplicationRedirectFilter.cs | 71 ++ ...IntegratedWebClientConfigurationManager.cs | 92 ++ .../IntegratedWebClientModelConvention.cs | 41 + ...ratedWebClientOpenIdConnectOptionsSetup.cs | 63 ++ .../IntegratedWebClientOptions.cs | 18 + ...tedWebClientServiceCollectionExtensions.cs | 35 + .../IntegratedWebclientMvcOptionsSetup.cs | 26 + ...dentity.Service.IntegratedWebClient.csproj | 23 + .../AuthorizationRequestModelBinder.cs | 53 ++ .../FormPostResult.cs | 35 + .../FragmentResult.cs | 36 + .../IdentityServiceControllerExtensions.cs | 72 ++ .../LogoutRequestModelBinder.cs | 49 ++ ...oft.AspNetCore.Identity.Service.Mvc.csproj | 20 + .../QueryResult.cs | 36 + .../TokenRequestModelBinder.cs | 49 ++ .../UrlHelperExtensions.cs | 26 + .../IdentityServiceResultAssert.cs | 55 ++ .../IdentityServiceSpecificationTestBase.cs | 219 +++++ ...dentity.Service.Specification.Tests.csproj | 31 + .../NuGet.config | 6 + .../ApplicationClaimsPrincipalFactory.cs | 38 + .../ApplicationManager.cs | 509 +++++++++++ .../AuthorizationStatus.cs | 12 + .../AuthorizeResult.cs | 50 ++ .../Claims/PairwiseSubTokenClaimProvider.cs | 54 ++ .../ClientApplicationValidator.cs | 191 ++++ .../IdentityServiceOptionsSetup.cs | 35 + .../IApplicationClaimStore.cs | 18 + .../IApplicationClaimsPrincipalFactory.cs | 13 + .../IApplicationClientSecretStore.cs | 16 + .../IApplicationScopeStore.cs | 18 + .../IApplicationStore.cs | 26 + .../IApplicationValidator.cs | 13 + .../IQueryableApplicationStore.cs | 12 + .../IRedirectUriStore.cs | 22 + .../IdentityServiceBuilder.cs | 21 + .../IdentityServiceError.cs | 15 + .../IdentityServiceResult.cs | 29 + ...ntityServiceServiceCollectionExtensions.cs | 142 +++ .../LogoutResult.cs | 20 + .../LogoutStatus.cs | 11 + ...crosoft.AspNetCore.Identity.Service.csproj | 25 + .../Session.cs | 19 + .../SessionManager.cs | 281 ++++++ ....Identity.Service.Abstractions.Test.csproj | 22 + .../TokenGeneratingContextTest.cs | 63 ++ .../Tokens/AccessTokenTest.cs | 317 +++++++ .../Tokens/AuthorizationCodeTest.cs | 285 ++++++ .../Tokens/IdTokenTest.cs | 313 +++++++ .../Tokens/RefreshTokenTest.cs | 269 ++++++ ...uthorizationCodeExchangeIntegrationTest.cs | 240 +++++ .../AuthorizationCodeIssuerTest.cs | 238 +++++ .../AuthorizationRequestFactoryTest.cs | 826 ++++++++++++++++++ .../AuthorizeIntegrationTest.cs | 218 +++++ .../Claims/DefaultTokenClaimsManagerTest.cs | 44 + .../Claims/DefaultTokenClaimsProviderTest.cs | 213 +++++ .../GrantedTokensTokenClaimsProviderTest.cs | 101 +++ .../Claims/NonceTokenClaimsProviderTest.cs | 147 ++++ .../Claims/ScopeTokenClaimsProviderTest.cs | 212 +++++ .../TimeStampTokenClaimsProviderTest.cs | 85 ++ .../TokenHashTokenClaimsProviderTest.cs | 95 ++ .../CryptoUtilities.cs | 23 + ...DefaultAuthorizationResponseFactoryTest.cs | 43 + ...horizationResponseParameterProviderTest.cs | 135 +++ ...ultSigningCredentialsPolicyProviderTest.cs | 277 ++++++ .../DefaultSigningCredentialsSourceTest.cs | 86 ++ .../DefaultTokenResponseFactoryTest.cs | 43 + ...faultTokenResponseParameterProviderTest.cs | 117 +++ .../FormPostResponseGeneratorTest.cs | 56 ++ .../FragmentResponseGeneratorTest.cs | 50 ++ .../IdentityServiceErrorComparer.cs | 43 + .../JwtAccessTokenIssuerTest.cs | 232 +++++ .../JwtIdTokenIssuerTest.cs | 349 ++++++++ .../JwtRefreshTokenIssuerTest.cs | 226 +++++ ...pNetCore.Identity.Service.Core.Test.csproj | 24 + .../QueryResponseGeneratorTest.cs | 53 ++ .../TokenHasherTest.cs | 31 + .../TokenRequestFactoryTest.cs | 510 +++++++++++ .../InMemoryContext.cs | 78 ++ .../InMemoryEFApplicationStoreTest.cs | 35 + ...e.EntityFrameworkCore.InMemory.Test.csproj | 31 + .../ApplicationStoreTest.cs | 63 ++ ...ty.Service.EntityFrameworkCore.Test.csproj | 39 + .../Utilities/DbUtil.cs | 34 + .../Utilities/ScratchDatabaseFixture.cs | 29 + .../Utilities/SqlServerTestStore.cs | 165 ++++ .../Utilities/TestEnvironment.cs | 24 + .../config.json | 7 + .../InMemoryStore.cs | 230 +++++ .../InMemoryStoreTest.cs | 32 + ...Core.Identity.Service.InMemory.Test.csproj | 29 + .../TestApplication.cs | 42 + .../ClientApplicationValidatorTest.cs | 115 +++ ...yOptionsServiceCollectionExtensionsTest.cs | 28 + ...ft.AspNetCore.Identity.Service.Test.csproj | 25 + 218 files changed, 16477 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/DeveloperCertificateMiddleware.cs create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/DeveloperCertificateOptions.cs create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/IdentityApplicationBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Microsoft.AspNetCore.Diagnostics.Identity.Service.csproj create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Strings.Designer.cs create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Strings.resx create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateErrorPage.Designer.cs create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateErrorPage.cshtml create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateViewModel.cs create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/ErrorPage.css create mode 100644 src/Microsoft.AspNetCore.Diagnostics.Identity.Service/generate-developer-certificate-diagnostics-page.cmd create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ApplicationScope.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationGrant.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationRequest.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationResponse.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ClaimsGenerationContext.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ITokenClaimsManager.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ITokenClaimsProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ConfigurationContext.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationRequestFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationResponseFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationResponseParameterProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/IConfigurationManager.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/IConfigurationMetadataProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/IIdentityServiceBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/IKeySetMetadataProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ILogoutRequestFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ISigningCredentialsPolicyProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ISigningCredentialsSource.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenManager.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenRequestFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenResponseFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenResponseParameterProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/IdentityServiceClaimTypes.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/IdentityServiceErrorCodes.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IAccessTokenIssuer.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IAuthorizationCodeIssuer.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IIdTokenIssuer.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IRefreshTokenIssuer.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/ITokenHasher.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/TokenResult.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/LogoutRequest.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Microsoft.AspNetCore.Identity.Service.Abstractions.csproj create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/PromptValues.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ProtocolError.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/RedirectUriResolutionResult.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/RequestGrants.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/ScopeResolutionResult.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/SigningCredentialsDescriptor.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenGeneratingContext.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenKinds.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenRequest.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenTypes.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/AccessToken.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/AuthorizationCode.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/IdToken.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/RefreshToken.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/Token.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IAuthorizationRequestValidator.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IClientIdValidator.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IRedirectUriResolver.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IScopeResolver.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/ITimeStampManager.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/ITokenRequestValidator.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/AuthorizationCodeIssuer.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/AuthorizationRequestFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Claims/DefaultTokenClaimsManager.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Claims/DefaultTokenClaimsProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Claims/GrantedTokensTokenClaimsProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Claims/NonceTokenClaimsProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Claims/ScopesTokenClaimsProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Claims/TimestampsTokenClaimsProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Claims/TokenHashTokenClaimsProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/CryptographyHelpers.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/DefaultAuthorizationResponseFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/DefaultAuthorizationResponseParameterProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/DefaultConfigurationManager.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/DefaultSigningCredentialsPolicyProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/DefaultSigningCredentialsSource.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/DefaultTokenResponseFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/DefaultTokenResponseParameterProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/FormPostResponseGenerator.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/FragmentResponseGenerator.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/IdentityServiceBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/JwtAccessTokenIssuer.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/JwtIdTokenIssuer.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/LogoutRequestFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Metadata/DefaultConfigurationMetadataProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Metadata/DefaultKeySetMetadataProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Microsoft.AspNetCore.Identity.Service.Core.csproj create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceAuthorizationOptionsSetup.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceOptions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceOptionsDefaultSetup.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenMapping.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenOptions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenValueCardinality.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenValueDescriptor.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/PrincipalExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/ProtocolErrorProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/QueryResponseGenerator.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/RefreshTokenIssuer.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/RequestParametersHelper.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/AuthorizationCodeConverter.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/RefreshTokenConverter.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/TokenConverter.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/TokenDataSerializer.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/TimeStampManager.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/TokenHasher.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/TokenManager.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/TokenMapper.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Core/TokenRequestFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/ApplicationStore.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplication.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplicationClaim.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceDbContext.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceRedirectUri.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceScope.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.csproj create mode 100644 src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/EnableIntegratedWebClientAttribute.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebApplicationRedirectFilter.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientConfigurationManager.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientModelConvention.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientOpenIdConnectOptionsSetup.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientOptions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientServiceCollectionExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebclientMvcOptionsSetup.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient.csproj create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Mvc/AuthorizationRequestModelBinder.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Mvc/FormPostResult.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Mvc/FragmentResult.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Mvc/IdentityServiceControllerExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Mvc/LogoutRequestModelBinder.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Mvc/Microsoft.AspNetCore.Identity.Service.Mvc.csproj create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Mvc/QueryResult.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Mvc/TokenRequestModelBinder.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Mvc/UrlHelperExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceResultAssert.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceSpecificationTestBase.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/Microsoft.AspNetCore.Identity.Service.Specification.Tests.csproj create mode 100644 src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/NuGet.config create mode 100644 src/Microsoft.AspNetCore.Identity.Service/ApplicationClaimsPrincipalFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/ApplicationManager.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/AuthorizationStatus.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/AuthorizeResult.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/Claims/PairwiseSubTokenClaimProvider.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/ClientApplicationValidator.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/Configuration/IdentityServiceOptionsSetup.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimStore.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimsPrincipalFactory.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IApplicationClientSecretStore.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IApplicationScopeStore.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IApplicationStore.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IApplicationValidator.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IQueryableApplicationStore.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IRedirectUriStore.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IdentityServiceBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IdentityServiceError.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IdentityServiceResult.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/IdentityServiceServiceCollectionExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/LogoutResult.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/LogoutStatus.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/Microsoft.AspNetCore.Identity.Service.csproj create mode 100644 src/Microsoft.AspNetCore.Identity.Service/Session.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/SessionManager.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test.csproj create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/TokenGeneratingContextTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/AccessTokenTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/AuthorizationCodeTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/IdTokenTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/RefreshTokenTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationCodeExchangeIntegrationTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationCodeIssuerTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationRequestFactoryTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizeIntegrationTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/DefaultTokenClaimsManagerTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/DefaultTokenClaimsProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/GrantedTokensTokenClaimsProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/NonceTokenClaimsProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/ScopeTokenClaimsProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/TimeStampTokenClaimsProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/TokenHashTokenClaimsProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/CryptoUtilities.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultAuthorizationResponseFactoryTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultAuthorizationResponseParameterProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultSigningCredentialsPolicyProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultSigningCredentialsSourceTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultTokenResponseFactoryTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultTokenResponseParameterProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/FormPostResponseGeneratorTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/FragmentResponseGeneratorTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/IdentityServiceErrorComparer.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtAccessTokenIssuerTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtIdTokenIssuerTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtRefreshTokenIssuerTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/Microsoft.AspNetCore.Identity.Service.Core.Test.csproj create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/QueryResponseGeneratorTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/TokenHasherTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Core.Test/TokenRequestFactoryTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryContext.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryEFApplicationStoreTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test.csproj create mode 100644 test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/ApplicationStoreTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test.csproj create mode 100644 test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/DbUtil.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/ScratchDatabaseFixture.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/SqlServerTestStore.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/TestEnvironment.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/config.json create mode 100644 test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStore.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStoreTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/Microsoft.AspNetCore.Identity.Service.InMemory.Test.csproj create mode 100644 test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/TestApplication.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Test/ClientApplicationValidatorTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Test/IdentityOptionsServiceCollectionExtensionsTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Test/Microsoft.AspNetCore.Identity.Service.Test.csproj diff --git a/Identity.sln b/Identity.sln index 45e7de6521..02d0353edc 100644 --- a/Identity.sln +++ b/Identity.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26123.0 +VisualStudioVersion = 15.0.26507.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0F647068-6602-4E24-B1DC-8ED91481A50A}" EndProject @@ -26,6 +26,34 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNet.Identity.A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Specification.Tests", "src\Microsoft.AspNetCore.Identity.Specification.Tests\Microsoft.AspNetCore.Identity.Specification.Tests.csproj", "{5608E828-DD54-4E2A-B73C-FC22268BE797}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Diagnostics.Identity.Service", "src\Microsoft.AspNetCore.Diagnostics.Identity.Service\Microsoft.AspNetCore.Diagnostics.Identity.Service.csproj", "{CD787C9A-58B7-4CBC-B8E3-66698EE58C11}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service", "src\Microsoft.AspNetCore.Identity.Service\Microsoft.AspNetCore.Identity.Service.csproj", "{B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.Abstractions", "src\Microsoft.AspNetCore.Identity.Service.Abstractions\Microsoft.AspNetCore.Identity.Service.Abstractions.csproj", "{F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.Core", "src\Microsoft.AspNetCore.Identity.Service.Core\Microsoft.AspNetCore.Identity.Service.Core.csproj", "{590697C1-EA60-4412-8A21-4EF35142381F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore", "src\Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore\Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.csproj", "{CD360545-3395-4C44-AD27-C32EECDD9572}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.IntegratedWebClient", "src\Microsoft.AspNetCore.Identity.Service.IntegratedWebClient\Microsoft.AspNetCore.Identity.Service.IntegratedWebClient.csproj", "{CA19785B-CE2F-480D-BB57-93A43A2DFDAB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.Mvc", "src\Microsoft.AspNetCore.Identity.Service.Mvc\Microsoft.AspNetCore.Identity.Service.Mvc.csproj", "{B3AE446B-859B-4C2C-98FD-A084C854941E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.Abstractions.Test", "test\Microsoft.AspNetCore.Identity.Service.Abstractions.Test\Microsoft.AspNetCore.Identity.Service.Abstractions.Test.csproj", "{27D28F0E-08F6-4EEA-8705-E0B559C87F3B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.Core.Test", "test\Microsoft.AspNetCore.Identity.Service.Core.Test\Microsoft.AspNetCore.Identity.Service.Core.Test.csproj", "{444F07E7-CF65-4717-BEF3-BA29F60DDE6E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.Test", "test\Microsoft.AspNetCore.Identity.Service.Test\Microsoft.AspNetCore.Identity.Service.Test.csproj", "{204163F9-E9BB-4940-9659-77F617C00D97}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.Specification.Tests", "src\Microsoft.AspNetCore.Identity.Service.Specification.Tests\Microsoft.AspNetCore.Identity.Service.Specification.Tests.csproj", "{C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test", "test\Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test\Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test.csproj", "{7423EB30-FFE9-4707-A44B-571E89A7CA15}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test", "test\Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test\Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test.csproj", "{4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Identity.Service.InMemory.Test", "test\Microsoft.AspNetCore.Identity.Service.InMemory.Test\Microsoft.AspNetCore.Identity.Service.InMemory.Test.csproj", "{94EC586A-2AE6-4AF2-894A-B0973C65BD68}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -130,6 +158,174 @@ Global {5608E828-DD54-4E2A-B73C-FC22268BE797}.Release|Mixed Platforms.Build.0 = Release|Any CPU {5608E828-DD54-4E2A-B73C-FC22268BE797}.Release|x86.ActiveCfg = Release|Any CPU {5608E828-DD54-4E2A-B73C-FC22268BE797}.Release|x86.Build.0 = Release|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Debug|x86.Build.0 = Debug|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Release|Any CPU.Build.0 = Release|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Release|x86.ActiveCfg = Release|Any CPU + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11}.Release|x86.Build.0 = Release|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Debug|x86.ActiveCfg = Debug|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Debug|x86.Build.0 = Debug|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Release|Any CPU.Build.0 = Release|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Release|x86.ActiveCfg = Release|Any CPU + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C}.Release|x86.Build.0 = Release|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Debug|x86.ActiveCfg = Debug|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Debug|x86.Build.0 = Debug|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Release|Any CPU.Build.0 = Release|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Release|x86.ActiveCfg = Release|Any CPU + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81}.Release|x86.Build.0 = Release|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Debug|x86.ActiveCfg = Debug|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Debug|x86.Build.0 = Debug|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Release|Any CPU.Build.0 = Release|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Release|x86.ActiveCfg = Release|Any CPU + {590697C1-EA60-4412-8A21-4EF35142381F}.Release|x86.Build.0 = Release|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Debug|x86.Build.0 = Debug|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Release|Any CPU.Build.0 = Release|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Release|x86.ActiveCfg = Release|Any CPU + {CD360545-3395-4C44-AD27-C32EECDD9572}.Release|x86.Build.0 = Release|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Debug|x86.Build.0 = Debug|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Release|Any CPU.Build.0 = Release|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Release|x86.ActiveCfg = Release|Any CPU + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB}.Release|x86.Build.0 = Release|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Debug|x86.ActiveCfg = Debug|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Debug|x86.Build.0 = Debug|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Release|Any CPU.Build.0 = Release|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Release|x86.ActiveCfg = Release|Any CPU + {B3AE446B-859B-4C2C-98FD-A084C854941E}.Release|x86.Build.0 = Release|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Debug|x86.ActiveCfg = Debug|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Debug|x86.Build.0 = Debug|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Release|Any CPU.Build.0 = Release|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Release|x86.ActiveCfg = Release|Any CPU + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B}.Release|x86.Build.0 = Release|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Debug|x86.ActiveCfg = Debug|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Debug|x86.Build.0 = Debug|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Release|Any CPU.Build.0 = Release|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Release|x86.ActiveCfg = Release|Any CPU + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E}.Release|x86.Build.0 = Release|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Debug|x86.ActiveCfg = Debug|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Debug|x86.Build.0 = Debug|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Release|Any CPU.Build.0 = Release|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Release|x86.ActiveCfg = Release|Any CPU + {204163F9-E9BB-4940-9659-77F617C00D97}.Release|x86.Build.0 = Release|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Debug|x86.ActiveCfg = Debug|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Debug|x86.Build.0 = Debug|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Release|Any CPU.Build.0 = Release|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Release|x86.ActiveCfg = Release|Any CPU + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36}.Release|x86.Build.0 = Release|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Debug|x86.ActiveCfg = Debug|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Debug|x86.Build.0 = Debug|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Release|Any CPU.Build.0 = Release|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Release|x86.ActiveCfg = Release|Any CPU + {7423EB30-FFE9-4707-A44B-571E89A7CA15}.Release|x86.Build.0 = Release|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Debug|x86.Build.0 = Debug|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Release|Any CPU.Build.0 = Release|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Release|x86.ActiveCfg = Release|Any CPU + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66}.Release|x86.Build.0 = Release|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Debug|x86.ActiveCfg = Debug|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Debug|x86.Build.0 = Debug|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Release|Any CPU.Build.0 = Release|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Release|x86.ActiveCfg = Release|Any CPU + {94EC586A-2AE6-4AF2-894A-B0973C65BD68}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -144,5 +340,19 @@ Global {4490894C-3572-4E63-86F1-EE5105CE8A06} = {0F647068-6602-4E24-B1DC-8ED91481A50A} {6A74C6EA-B241-4D6B-BCE4-BF89EC1D2475} = {0F647068-6602-4E24-B1DC-8ED91481A50A} {5608E828-DD54-4E2A-B73C-FC22268BE797} = {0F647068-6602-4E24-B1DC-8ED91481A50A} + {CD787C9A-58B7-4CBC-B8E3-66698EE58C11} = {0F647068-6602-4E24-B1DC-8ED91481A50A} + {B44C2A7F-EA9E-4A9F-9698-1C9F9BB40E0C} = {0F647068-6602-4E24-B1DC-8ED91481A50A} + {F34C3ED8-D4A9-47CE-BE0F-1F234A33AC81} = {0F647068-6602-4E24-B1DC-8ED91481A50A} + {590697C1-EA60-4412-8A21-4EF35142381F} = {0F647068-6602-4E24-B1DC-8ED91481A50A} + {CD360545-3395-4C44-AD27-C32EECDD9572} = {0F647068-6602-4E24-B1DC-8ED91481A50A} + {CA19785B-CE2F-480D-BB57-93A43A2DFDAB} = {0F647068-6602-4E24-B1DC-8ED91481A50A} + {B3AE446B-859B-4C2C-98FD-A084C854941E} = {0F647068-6602-4E24-B1DC-8ED91481A50A} + {27D28F0E-08F6-4EEA-8705-E0B559C87F3B} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} + {444F07E7-CF65-4717-BEF3-BA29F60DDE6E} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} + {204163F9-E9BB-4940-9659-77F617C00D97} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} + {C05D641C-A3EE-4A56-9A39-F20F3B9C4D36} = {0F647068-6602-4E24-B1DC-8ED91481A50A} + {7423EB30-FFE9-4707-A44B-571E89A7CA15} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} + {4F5D777E-3CFA-4EDF-BA89-4FE04BBF7A66} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} + {94EC586A-2AE6-4AF2-894A-B0973C65BD68} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} EndGlobalSection EndGlobal diff --git a/build/dependencies.props b/build/dependencies.props index eb8e868210..8d59fdcabb 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,6 +1,8 @@ 2.0.0-* + 2.1.3 + 4.3.0 2.2.1 2.1.0-* 4.7.1 diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/DeveloperCertificateMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/DeveloperCertificateMiddleware.cs new file mode 100644 index 0000000000..68dc9c084f --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/DeveloperCertificateMiddleware.cs @@ -0,0 +1,137 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity.Service; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Diagnostics.Identity.Service +{ + public class DeveloperCertificateMiddleware + { + private readonly RequestDelegate _next; + private readonly IHostingEnvironment _environment; + private readonly IOptions _options; + private readonly ITimeStampManager _timeStampManager; + private readonly IConfiguration _configuration; + private readonly IOptionsCache _identityServiceOptionsCache; + + public DeveloperCertificateMiddleware( + RequestDelegate next, + IOptions options, + IOptionsCache identityServiceOptions, + ITimeStampManager timeStampManager, + IHostingEnvironment environment, + IConfiguration configuration) + { + _next = next; + _options = options; + _identityServiceOptionsCache = identityServiceOptions; + _environment = environment; + _timeStampManager = timeStampManager; + _configuration = configuration; + } + + public async Task InvokeAsync(HttpContext context) + { + var credentialsProvider = context.RequestServices.GetRequiredService(); + var openIdOptionsCache = context.RequestServices.GetRequiredService>(); + + if (_environment.IsDevelopment() && + context.Request.Path.Equals(_options.Value.ListeningEndpoint)) + { + if (context.Request.Method.Equals(HttpMethods.Get)) + { + var credentials = await credentialsProvider.GetAllCredentialsAsync(); + bool hasDevelopmentCertificate = await IsDevelopmentCertificateConfiguredAndValid(); + var foundDeveloperCertificate = FoundDeveloperCertificate(); + if (!foundDeveloperCertificate || !hasDevelopmentCertificate) + { + var page = new DeveloperCertificateErrorPage(); + page.Model = new DeveloperCertificateViewModel() + { + CertificateExists = foundDeveloperCertificate, + CertificateIsInvalid = !hasDevelopmentCertificate, + Options = _options.Value + }; + + await page.ExecuteAsync(context); + return; + } + } + if (context.Request.Method.Equals(HttpMethods.Post)) + { + CreateDevelopmentCertificate(); + return; + } + } + + await _next(context); + void CreateDevelopmentCertificate() + { + using (var rsa = RSA.Create(2048)) + { + var signingRequest = new CertificateRequest( + new X500DistinguishedName("CN=IdentityService.Development"), rsa, HashAlgorithmName.SHA256); + var enhacedKeyUsage = new OidCollection(); + enhacedKeyUsage.Add(new Oid("1.3.6.1.5.5.7.3.1", "Server Authentication")); + signingRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(enhacedKeyUsage, critical: true)); + signingRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true)); + + var certificate = signingRequest.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1)); + certificate.FriendlyName = "Identity Service developer certificate"; + + // We need to take this step so that the key gets persisted. + var export = certificate.Export(X509ContentType.Pkcs12, ""); + var imported = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet); + Array.Clear(export, 0, export.Length); + + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + store.Add(imported); + store.Close(); + _identityServiceOptionsCache.TryRemove(Options.DefaultName); + openIdOptionsCache.TryRemove(OpenIdConnectDefaults.AuthenticationScheme); + + context.Response.StatusCode = StatusCodes.Status204NoContent; + }; + } + } + + bool FoundDeveloperCertificate() + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadOnly); + var developmentCertificate = store.Certificates.Find( + X509FindType.FindBySubjectName, + "IdentityService.Development", + validOnly: false); + + store.Close(); + return developmentCertificate.OfType().Any(); + } + } + + async Task IsDevelopmentCertificateConfiguredAndValid() + { + var certificates = await credentialsProvider.GetAllCredentialsAsync(); + return certificates.Any( + c => _timeStampManager.IsValidPeriod(c.NotBefore, c.Expires) && + c.Credentials.Key is X509SecurityKey key && + key.Certificate.Subject.Equals("CN=IdentityService.Development")); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/DeveloperCertificateOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/DeveloperCertificateOptions.cs new file mode 100644 index 0000000000..83509efd4a --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/DeveloperCertificateOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Diagnostics.Identity.Service +{ + public class DeveloperCertificateOptions + { + public PathString ListeningEndpoint { get; set; } = "/tfp/IdentityService/signinsignup/oauth2/v2.0/authorize"; + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/IdentityApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/IdentityApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..2cd9bebd08 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/IdentityApplicationBuilderExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Diagnostics.Identity.Service +{ + public static class IdentityApplicationBuilderExtensions + { + public static IApplicationBuilder UseDevelopmentCertificateErrorPage( + this IApplicationBuilder builder, + IConfiguration configuration) + { + builder.UseMiddleware(configuration); + return builder; + } + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Microsoft.AspNetCore.Diagnostics.Identity.Service.csproj b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Microsoft.AspNetCore.Diagnostics.Identity.Service.csproj new file mode 100644 index 0000000000..ddba986ae6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Microsoft.AspNetCore.Diagnostics.Identity.Service.csproj @@ -0,0 +1,44 @@ + + + + + + ASP.NET Core Identity Service Diagnostics Middleware. + netcoreapp2.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + + + + + + + + + + + + + True + True + Strings.resx + + + + + + ResXFileCodeGenerator + Strings.Designer.cs + + + + diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Strings.Designer.cs b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Strings.Designer.cs new file mode 100644 index 0000000000..3249c864c4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Strings.Designer.cs @@ -0,0 +1,146 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNetCore.Diagnostics.Identity.Service { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.Diagnostics.Identity.Service.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Developer certificate. + /// + internal static string CertificateErrorPage_Title { + get { + return ResourceManager.GetString("CertificateErrorPage_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create certificate. + /// + internal static string CreateCertificate { + get { + return ResourceManager.GetString("CreateCertificate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Done. + /// + internal static string CreateCertificateDone { + get { + return ResourceManager.GetString("CreateCertificateDone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Certificate creation failed.. + /// + internal static string CreateCertificateFailed { + get { + return ResourceManager.GetString("CreateCertificateFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Certificate creation succeeded. Try refreshing the page.. + /// + internal static string CreateCertificateRefresh { + get { + return ResourceManager.GetString("CreateCertificateRefresh", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Creating developer certificate.... + /// + internal static string CreateCertificateRunning { + get { + return ResourceManager.GetString("CreateCertificateRunning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identity requires a certificate to sign tokens. You can create a developer certificate by clicking the Create Certificate button to generate a developer certificate for you automatically. This will create a self-signed certificate with subject IdentityService.Development and will add it to your current user personal store. + /// Alternatively, you can create this certificate manually with the instructions given in the following link: + /// . + /// + internal static string ManualCertificateGenerationInfo { + get { + return ResourceManager.GetString("ManualCertificateGenerationInfo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to https://go.microsoft.com/fwlink/?linkid=848037. + /// + internal static string ManualCertificateGenerationInfoLink { + get { + return ResourceManager.GetString("ManualCertificateGenerationInfoLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The developer certificate is missing or invalid. + /// + internal static string MissingOrInvalidCertificate { + get { + return ResourceManager.GetString("MissingOrInvalidCertificate", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Strings.resx b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Strings.resx new file mode 100644 index 0000000000..0491baa806 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Strings.resx @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Developer certificate + + + Create certificate + + + Done + + + Certificate creation failed. + + + Certificate creation succeeded. Try refreshing the page. + + + Creating developer certificate... + + + Identity requires a certificate to sign tokens. You can create a developer certificate by clicking the Create Certificate button to generate a developer certificate for you automatically. This will create a self-signed certificate with subject IdentityService.Development and will add it to your current user personal store. + Alternatively, you can create this certificate manually with the instructions given in the following link: + + + + https://go.microsoft.com/fwlink/?linkid=848037 + + + The developer certificate is missing or invalid + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateErrorPage.Designer.cs b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateErrorPage.Designer.cs new file mode 100644 index 0000000000..d8249e13ff --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateErrorPage.Designer.cs @@ -0,0 +1,247 @@ +namespace Microsoft.AspNetCore.Diagnostics.Identity.Service +{ + #line hidden +#line 1 "DeveloperCertificateErrorPage.cshtml" +using System; + +#line default +#line hidden + using System.Threading.Tasks; + internal class DeveloperCertificateErrorPage : Microsoft.Extensions.RazorViews.BaseView + { + #pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { +#line 2 "DeveloperCertificateErrorPage.cshtml" + + Response.StatusCode = 500; + Response.ContentType = "text/html; charset=utf-8"; + Response.ContentLength = null; // Clear any prior Content-Length + +#line default +#line hidden + WriteLiteral(@" + + + + + Internal Server Error + + + +

"); +#line 110 "DeveloperCertificateErrorPage.cshtml" + Write(Strings.CertificateErrorPage_Title); + +#line default +#line hidden + WriteLiteral("

\r\n

\r\n

\r\n
\r\n\r\n"); +#line 115 "DeveloperCertificateErrorPage.cshtml" + if (!Model.CertificateExists || Model.CertificateIsInvalid) + { + +#line default +#line hidden + WriteLiteral("

"); +#line 117 "DeveloperCertificateErrorPage.cshtml" + Write(Strings.MissingOrInvalidCertificate); + +#line default +#line hidden + WriteLiteral("

\r\n

"); +#line 118 "DeveloperCertificateErrorPage.cshtml" + Write(Strings.ManualCertificateGenerationInfo); + +#line default +#line hidden + WriteLiteral("

\r\n
\r\n
\r\n
\r\n

\r\n + + +

+ \r\n
\r\n"); +#line 163 "DeveloperCertificateErrorPage.cshtml" + } + +#line default +#line hidden + WriteLiteral("\r\n"); + } + #pragma warning restore 1998 +#line 8 "DeveloperCertificateErrorPage.cshtml" + + public DeveloperCertificateViewModel Model { get; set; } + + public string UrlEncode(string content) + { + return UrlEncoder.Encode(content); + } + + public string JavaScriptEncode(string content) + { + return JavaScriptEncoder.Encode(content); + } + +#line default +#line hidden + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateErrorPage.cshtml b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateErrorPage.cshtml new file mode 100644 index 0000000000..91523fb21a --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateErrorPage.cshtml @@ -0,0 +1,87 @@ +@using System +@{ + Response.StatusCode = 500; + Response.ContentType = "text/html; charset=utf-8"; + Response.ContentLength = null; // Clear any prior Content-Length +} +@functions +{ + public DeveloperCertificateViewModel Model { get; set; } + + public string UrlEncode(string content) + { + return UrlEncoder.Encode(content); + } + + public string JavaScriptEncode(string content) + { + return JavaScriptEncoder.Encode(content); + } +} + + + + + + Internal Server Error + + + +

@Strings.CertificateErrorPage_Title

+

+

+
+ + @if (!Model.CertificateExists || Model.CertificateIsInvalid) + { +

@Strings.MissingOrInvalidCertificate

+

@Strings.ManualCertificateGenerationInfo@Strings.ManualCertificateGenerationInfoLink

+
+
+
+

+ + + +

+ +
+ } + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateViewModel.cs b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateViewModel.cs new file mode 100644 index 0000000000..ec54bb08b0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/DeveloperCertificateViewModel.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Diagnostics.Identity.Service +{ + public class DeveloperCertificateViewModel + { + public DeveloperCertificateOptions Options { get; set; } + public bool CertificateExists { get; set; } + public bool CertificateIsInvalid { get; set; } + public bool CertificateIsFoundInConfiguration { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/ErrorPage.css b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/ErrorPage.css new file mode 100644 index 0000000000..5f350aeb60 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/Views/ErrorPage.css @@ -0,0 +1,78 @@ +body { + font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif; + font-size: .813em; + line-height: 1.4em; + color: #222; +} + +h1, h2, h3, h4, h5 { + font-weight: 100; +} + +h1 { + color: #44525e; + margin: 15px 0 15px 0; +} + +h2 { + margin: 10px 5px 0 0; +} + +h3 { + color: #363636; + margin: 5px 5px 0 0; +} + +code { + font-family: Consolas, "Courier New", courier, monospace; +} + +a { + color: #1ba1e2; + text-decoration: none; +} + + a:hover { + color: #13709e; + text-decoration: underline; + } + +hr { + border: 1px #ddd solid; +} + +body .titleerror { + padding: 3px; +} + +#createCertificate { + font-size: 14px; + background: #44c5f2; + color: #ffffff; + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-weight: normal; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; +} + + #createCertificate:disabled { + background-color: #a9e4f9; + border-color: #44c5f2; + } + +.error { + color: red; +} + +.expanded { + display: block; +} + +.collapsed { + display: none; +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/generate-developer-certificate-diagnostics-page.cmd b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/generate-developer-certificate-diagnostics-page.cmd new file mode 100644 index 0000000000..c739bcfe08 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.Identity.Service/generate-developer-certificate-diagnostics-page.cmd @@ -0,0 +1 @@ +dotnet razorpagegenerator Microsoft.AspNetCore.Diagnostics.Identity.Service \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ApplicationScope.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ApplicationScope.cs new file mode 100644 index 0000000000..4c21584931 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ApplicationScope.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Identity.Service +{ + [DebuggerDisplay("{DebuggerDisplay(),nq}")] + public class ApplicationScope : IEquatable + { + public static readonly ApplicationScope OpenId = new ApplicationScope("openid"); + public static readonly ApplicationScope Profile = new ApplicationScope("profile"); + public static readonly ApplicationScope Email = new ApplicationScope("email"); + public static readonly ApplicationScope OfflineAccess = new ApplicationScope("offline_access"); + + public static readonly IReadOnlyDictionary CanonicalScopes = new Dictionary(StringComparer.Ordinal) + { + [OpenId.Scope] = OpenId, + [Profile.Scope] = Profile, + [Email.Scope] = Email, + [OfflineAccess.Scope] = OfflineAccess + }; + + private ApplicationScope(string scope) + { + Scope = scope; + } + + public ApplicationScope(string clientId, string scope) + { + ClientId = clientId; + Scope = scope; + } + + public string ClientId { get; } + public string Scope { get; } + + public bool Equals(ApplicationScope other) => string.Equals(ClientId, other?.ClientId, StringComparison.Ordinal) && + string.Equals(Scope, other?.Scope, StringComparison.Ordinal); + + public override bool Equals(object obj) => Equals(obj as ApplicationScope); + + public override int GetHashCode() => ClientId == null ? Scope.GetHashCode() : ClientId.GetHashCode() ^ Scope.GetHashCode(); + + private string DebuggerDisplay() => ClientId != null ? $"{ClientId},{Scope}" : Scope; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationGrant.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationGrant.cs new file mode 100644 index 0000000000..e473a86ae4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationGrant.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationGrant + { + private AuthorizationGrant(OpenIdConnectMessage error) + { + IsValid = false; + Error = error; + } + + private AuthorizationGrant( + string userId, + string clientId, + IEnumerable grantedTokens, + IEnumerable grantedScopes, + Token token) + { + IsValid = true; + UserId = userId; + ClientId = clientId; + GrantedTokens = grantedTokens; + GrantedScopes = grantedScopes; + Token = token; + } + + public bool IsValid { get; } + + public OpenIdConnectMessage Error { get; } + public Token Token { get; } + public string UserId { get; } + public string ClientId { get; } + public IEnumerable GrantedTokens { get; } + public IEnumerable GrantedScopes { get; } + public string Resource { get; } + + public static AuthorizationGrant Invalid(OpenIdConnectMessage error) + { + return new AuthorizationGrant(error); + } + + public static AuthorizationGrant Valid( + string userId, + string clientId, + IEnumerable grantedTokens, + IEnumerable grantedScopes, + Token token) + { + return new AuthorizationGrant(userId, clientId, grantedTokens, grantedScopes, token); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationRequest.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationRequest.cs new file mode 100644 index 0000000000..462f888a21 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationRequest.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationRequest + { + private AuthorizationRequest(AuthorizationRequestError error) + { + IsValid = false; + Error = error; + } + + private AuthorizationRequest(OpenIdConnectMessage request, RequestGrants requestGrants) + { + IsValid = true; + Message = request; + RequestGrants = requestGrants; + } + + public bool IsValid { get; } + public AuthorizationRequestError Error { get; } + public OpenIdConnectMessage Message { get; } + public RequestGrants RequestGrants { get; } + + public static AuthorizationRequest Invalid(AuthorizationRequestError authorizationRequestError) + { + return new AuthorizationRequest(authorizationRequestError); + } + + public static AuthorizationRequest Valid( + OpenIdConnectMessage request, + RequestGrants requestGrants) + { + return new AuthorizationRequest(request, requestGrants); + } + + public TokenGeneratingContext CreateTokenGeneratingContext(ClaimsPrincipal user, ClaimsPrincipal application) + { + return new TokenGeneratingContext( + user, + application, + Message, + RequestGrants); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationResponse.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationResponse.cs new file mode 100644 index 0000000000..76e9acfcf8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/AuthorizationResponse.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationResponse + { + public OpenIdConnectMessage Message { get; set; } + public string RedirectUri { get; set; } + public string ResponseMode { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ClaimsGenerationContext.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ClaimsGenerationContext.cs new file mode 100644 index 0000000000..541b87febc --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ClaimsGenerationContext.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class ClaimsGenerationContext + { + public string TokenType { get; set; } + public TokenGeneratingContext GenerationContext { get; set; } + public IEnumerable GeneratedTokens { get; set; } + public IList Claims { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ITokenClaimsManager.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ITokenClaimsManager.cs new file mode 100644 index 0000000000..d2fd99e576 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ITokenClaimsManager.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ITokenClaimsManager + { + Task CreateClaimsAsync(TokenGeneratingContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ITokenClaimsProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ITokenClaimsProvider.cs new file mode 100644 index 0000000000..c0fe447a4a --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Claims/ITokenClaimsProvider.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public interface ITokenClaimsProvider + { + int Order { get; } + Task OnGeneratingClaims(TokenGeneratingContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ConfigurationContext.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ConfigurationContext.cs new file mode 100644 index 0000000000..e497801749 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ConfigurationContext.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Specialized; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ConfigurationContext + { + public string Id { get; set; } + public HttpContext HttpContext { get; set; } + public string AuthorizationEndpoint { get; set; } + public string TokenEndpoint { get; set; } + public string JwksUriEndpoint { get; set; } + public string EndSessionEndpoint { get; set; } + public NameValueCollection AdditionalValues { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationRequestFactory.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationRequestFactory.cs new file mode 100644 index 0000000000..36221c81ab --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationRequestFactory.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IAuthorizationRequestFactory + { + Task CreateAuthorizationRequestAsync(IDictionary requestParameters); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationResponseFactory.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationResponseFactory.cs new file mode 100644 index 0000000000..9c01b4d84e --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationResponseFactory.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IAuthorizationResponseFactory + { + Task CreateAuthorizationResponseAsync(TokenGeneratingContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationResponseParameterProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationResponseParameterProvider.cs new file mode 100644 index 0000000000..ea321e579d --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IAuthorizationResponseParameterProvider.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IAuthorizationResponseParameterProvider + { + int Order { get; } + + Task AddParameters(TokenGeneratingContext context, AuthorizationResponse response); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IConfigurationManager.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IConfigurationManager.cs new file mode 100644 index 0000000000..b0bb9c209e --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IConfigurationManager.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IConfigurationManager + { + Task GetConfigurationAsync(ConfigurationContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IConfigurationMetadataProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IConfigurationMetadataProvider.cs new file mode 100644 index 0000000000..fa059de736 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IConfigurationMetadataProvider.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IConfigurationMetadataProvider + { + int Order { get; } + + Task ConfigureMetadataAsync(OpenIdConnectConfiguration configuration, ConfigurationContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IIdentityServiceBuilder.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IIdentityServiceBuilder.cs new file mode 100644 index 0000000000..409a69ff81 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IIdentityServiceBuilder.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IIdentityServiceBuilder + { + IServiceCollection Services { get; } + Type ApplicationType { get; } + Type UserType { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IKeySetMetadataProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IKeySetMetadataProvider.cs new file mode 100644 index 0000000000..cbaf2d2123 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IKeySetMetadataProvider.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.IdentityModel.Tokens; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IKeySetMetadataProvider + { + Task GetKeysAsync(); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ILogoutRequestFactory.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ILogoutRequestFactory.cs new file mode 100644 index 0000000000..604b977a03 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ILogoutRequestFactory.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ILogoutRequestFactory + { + Task CreateLogoutRequestAsync(IDictionary requestParameters); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ISigningCredentialsPolicyProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ISigningCredentialsPolicyProvider.cs new file mode 100644 index 0000000000..081c41551f --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ISigningCredentialsPolicyProvider.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ISigningCredentialsPolicyProvider + { + Task> GetAllCredentialsAsync(); + Task GetSigningCredentialsAsync(); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ISigningCredentialsSource.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ISigningCredentialsSource.cs new file mode 100644 index 0000000000..f3a95fab0c --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ISigningCredentialsSource.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ISigningCredentialsSource + { + Task> GetCredentials(); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenManager.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenManager.cs new file mode 100644 index 0000000000..0e19293fad --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenManager.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ITokenManager + { + Task IssueTokensAsync(TokenGeneratingContext context); + Task ExchangeTokenAsync(OpenIdConnectMessage message); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenRequestFactory.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenRequestFactory.cs new file mode 100644 index 0000000000..2e9640364e --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenRequestFactory.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ITokenRequestFactory + { + Task CreateTokenRequestAsync(IDictionary requestParameters); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenResponseFactory.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenResponseFactory.cs new file mode 100644 index 0000000000..7373db4446 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenResponseFactory.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ITokenResponseFactory + { + Task CreateTokenResponseAsync(TokenGeneratingContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenResponseParameterProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenResponseParameterProvider.cs new file mode 100644 index 0000000000..97d42c5760 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ITokenResponseParameterProvider.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ITokenResponseParameterProvider + { + int Order { get; } + + Task AddParameters(TokenGeneratingContext context, OpenIdConnectMessage response); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IdentityServiceClaimTypes.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IdentityServiceClaimTypes.cs new file mode 100644 index 0000000000..e0b9fba9d0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IdentityServiceClaimTypes.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceClaimTypes + { + public const string TokenUniqueId = "tuid"; + public const string ObjectId = "oid"; + public const string JwtId = "jti"; + public const string Issuer = "iss"; + public const string Subject = "sub"; + public const string Audience = "aud"; + public const string AuthorizedParty = "azp"; + public const string ClientId = "client_id"; + public const string RedirectUri = "r_uri"; + public const string LogoutRedirectUri = "lo_uri"; + public const string IssuedAt = "iat"; + public const string Expires = "exp"; + public const string NotBefore = "nbf"; + public const string Scope = "scp"; + public const string Nonce = "nonce"; + public const string CodeHash = "c_hash"; + public const string AccessTokenHash = "at_hash"; + public const string AuthenticationTime = "auth_time"; + public const string UserId = "user_id"; + public const string Version = "ver"; + public const string Name = "name"; + public const string GrantedToken = "g_token"; + public const string TenantId = "tid"; + public const string Resource = "rid"; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IdentityServiceErrorCodes.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IdentityServiceErrorCodes.cs new file mode 100644 index 0000000000..ea09bcfb0a --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/IdentityServiceErrorCodes.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceErrorCodes + { + public const string InvalidRequest = "invalid_request"; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IAccessTokenIssuer.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IAccessTokenIssuer.cs new file mode 100644 index 0000000000..c2de19130a --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IAccessTokenIssuer.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IAccessTokenIssuer + { + Task IssueAccessTokenAsync(TokenGeneratingContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IAuthorizationCodeIssuer.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IAuthorizationCodeIssuer.cs new file mode 100644 index 0000000000..bef59dfd5b --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IAuthorizationCodeIssuer.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IAuthorizationCodeIssuer + { + Task CreateAuthorizationCodeAsync(TokenGeneratingContext context); + Task ExchangeAuthorizationCodeAsync(OpenIdConnectMessage message); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IIdTokenIssuer.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IIdTokenIssuer.cs new file mode 100644 index 0000000000..095d3f962a --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IIdTokenIssuer.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IIdTokenIssuer + { + Task IssueIdTokenAsync(TokenGeneratingContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IRefreshTokenIssuer.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IRefreshTokenIssuer.cs new file mode 100644 index 0000000000..f2060af116 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/IRefreshTokenIssuer.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IRefreshTokenIssuer + { + Task IssueRefreshTokenAsync(TokenGeneratingContext context); + Task ExchangeRefreshTokenAsync(OpenIdConnectMessage message); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/ITokenHasher.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/ITokenHasher.cs new file mode 100644 index 0000000000..e16e1b01fa --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/ITokenHasher.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ITokenHasher + { + string HashToken(string token, string algorithm); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/TokenResult.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/TokenResult.cs new file mode 100644 index 0000000000..6f44aa96be --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Issuers/TokenResult.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenResult + { + public TokenResult(Token token, string serializedValue) + { + Token = token; + SerializedValue = serializedValue; + } + + public TokenResult(Token token, string serializedValue, string tokenType) + : this(token, serializedValue) + { + Token = token; + TokenType = tokenType; + SerializedValue = serializedValue; + } + + public Token Token { get; } + public string TokenType { get; } + public string SerializedValue { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/LogoutRequest.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/LogoutRequest.cs new file mode 100644 index 0000000000..64632f742f --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/LogoutRequest.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class LogoutRequest + { + private LogoutRequest(OpenIdConnectMessage message) + { + Message = message; + IsValid = false; + } + + private LogoutRequest(OpenIdConnectMessage message, string logoutRedirectUri) + { + Message = message; + LogoutRedirectUri = logoutRedirectUri; + IsValid = true; + } + + public OpenIdConnectMessage Message { get; } + public string LogoutRedirectUri { get; set; } + public bool IsValid { get; } + + public static LogoutRequest Valid(OpenIdConnectMessage message, string logoutRedirectUri) => new LogoutRequest(message, logoutRedirectUri); + public static LogoutRequest Invalid(OpenIdConnectMessage error) => new LogoutRequest(error); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Microsoft.AspNetCore.Identity.Service.Abstractions.csproj b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Microsoft.AspNetCore.Identity.Service.Abstractions.csproj new file mode 100644 index 0000000000..59c95f8713 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Microsoft.AspNetCore.Identity.Service.Abstractions.csproj @@ -0,0 +1,19 @@ + + + + + + ASP.NET Core common types defining the main abstractions for Identity Service. + netcoreapp2.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/PromptValues.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/PromptValues.cs new file mode 100644 index 0000000000..0a51510b39 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/PromptValues.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public static class PromptValues + { + public const string None = "none"; + public const string Login = "login"; + public const string Consent = "consent"; + public const string SelectAccount = "select_account"; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ProtocolError.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ProtocolError.cs new file mode 100644 index 0000000000..378425833e --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ProtocolError.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationRequestError + { + public AuthorizationRequestError(OpenIdConnectMessage error, string redirectUri, string responseMode) + { + Message = error; + RedirectUri = redirectUri; + ResponseMode = responseMode; + } + + public OpenIdConnectMessage Message { get; set; } + + public string RedirectUri { get; set; } + + public string ResponseMode { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/RedirectUriResolutionResult.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/RedirectUriResolutionResult.cs new file mode 100644 index 0000000000..ff17b207e4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/RedirectUriResolutionResult.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class RedirectUriResolutionResult + { + private RedirectUriResolutionResult(string uri) + { + IsValid = true; + Uri = uri; + } + + private RedirectUriResolutionResult(OpenIdConnectMessage error) + { + IsValid = false; + Error = error; + } + + public bool IsValid { get; } + public string Uri { get; } + public OpenIdConnectMessage Error { get; } + + public static RedirectUriResolutionResult Valid(string uri) + { + return new RedirectUriResolutionResult(uri); + } + + public static RedirectUriResolutionResult Invalid(OpenIdConnectMessage error) + { + return new RedirectUriResolutionResult(error); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/RequestGrants.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/RequestGrants.cs new file mode 100644 index 0000000000..840424045a --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/RequestGrants.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class RequestGrants + { + public string RedirectUri { get; set; } + public string ResponseMode { get; set; } + public IList Tokens { get; set; } = new List(); + public IList Scopes { get; set; } = new List(); + public IList Claims { get; set; } = new List(); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ScopeResolutionResult.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ScopeResolutionResult.cs new file mode 100644 index 0000000000..a4e09ae1af --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/ScopeResolutionResult.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ScopeResolutionResult + { + public ScopeResolutionResult(IEnumerable scopes) + { + IsValid = true; + Scopes = scopes; + } + + public ScopeResolutionResult(OpenIdConnectMessage error) + { + IsValid = false; + Error = error; + } + + public IEnumerable Scopes { get; } + public OpenIdConnectMessage Error { get; } + public bool IsValid { get; } + + public static ScopeResolutionResult Valid(IEnumerable scopes) + { + return new ScopeResolutionResult(scopes); + } + + public static ScopeResolutionResult Invalid(OpenIdConnectMessage error) + { + return new ScopeResolutionResult(error); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/SigningCredentialsDescriptor.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/SigningCredentialsDescriptor.cs new file mode 100644 index 0000000000..e9cf1d3de5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/SigningCredentialsDescriptor.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service +{ + [DebuggerDisplay("{GetDebugDisplay(),nq}")] + public class SigningCredentialsDescriptor + { + public SigningCredentialsDescriptor( + SigningCredentials credentials, + string algorithm, + DateTimeOffset notBefore, + DateTimeOffset expires, + IDictionary metadata) + { + Credentials = credentials; + Algorithm = algorithm; + NotBefore = notBefore; + Expires = expires; + Metadata = metadata; + } + + public string Id => Credentials.Kid; + public string Algorithm { get; set; } + public DateTimeOffset NotBefore { get; set; } + public DateTimeOffset Expires { get; set; } + public SigningCredentials Credentials { get; set; } + public IDictionary Metadata { get; set; } + + private string GetDebugDisplay() + { + var builder = new StringBuilder(); + builder.Append($"Id = {Id}, "); + builder.Append($"Alg = {Algorithm}, "); + builder.Append($"Nbf = {NotBefore}, "); + builder.Append($"Exp = {Expires}, "); + foreach (var kvp in Metadata) + { + builder.Append($"{kvp.Key} = {kvp.Value}, "); + } + + return builder.ToString(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenGeneratingContext.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenGeneratingContext.cs new file mode 100644 index 0000000000..5472dd1cd3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenGeneratingContext.cs @@ -0,0 +1,134 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenGeneratingContext + { + private readonly IList _issuedTokens = new List(); + + public TokenGeneratingContext( + ClaimsPrincipal user, + ClaimsPrincipal application, + OpenIdConnectMessage requestParameters, + RequestGrants requestGrants) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (requestParameters == null) + { + throw new ArgumentNullException(nameof(requestParameters)); + } + + if (requestGrants == null) + { + throw new ArgumentNullException(nameof(requestGrants)); + } + + User = user; + Application = application; + RequestParameters = requestParameters; + RequestGrants = requestGrants; + } + + public ClaimsPrincipal User { get; } + public ClaimsPrincipal Application { get; } + public OpenIdConnectMessage RequestParameters { get; } + public RequestGrants RequestGrants { get; } + public IList AmbientClaims { get; } = new List(); + public string CurrentToken { get; private set; } + public IList CurrentClaims { get; private set; } + public IEnumerable IssuedTokens { get => _issuedTokens; } + + public TokenResult AuthorizationCode => + IssuedTokens.SingleOrDefault(it => it.Token.IsOfKind(TokenTypes.AuthorizationCode)); + public TokenResult AccessToken => + IssuedTokens.SingleOrDefault(it => it.Token.IsOfKind(TokenTypes.AccessToken)); + public TokenResult IdToken => + IssuedTokens.SingleOrDefault(it => it.Token.IsOfKind(TokenTypes.IdToken)); + public TokenResult RefreshToken => + IssuedTokens.SingleOrDefault(it => it.Token.IsOfKind(TokenTypes.RefreshToken)); + + public void InitializeForToken(string tokenType) + { + if (tokenType == null) + { + throw new ArgumentNullException(nameof(tokenType)); + } + + if (CurrentToken != null) + { + throw new InvalidOperationException($"Currently issuing a token for {CurrentToken}"); + } + + if (IssuedTokens.Any(it => it.Token.IsOfKind(tokenType))) + { + throw new InvalidOperationException($"A token of type '{tokenType}' has already been emitted."); + } + + CurrentToken = tokenType; + CurrentClaims = new List(); + } + + public bool IsContextForTokenTypes(params string [] tokenTypes) + { + if (CurrentToken == null) + { + return false; + } + foreach (var token in tokenTypes) + { + if (CurrentToken.Equals(token, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + public void AddClaimToCurrentToken(Claim claim) + { + if (CurrentToken == null) + { + throw new InvalidOperationException(); + } + + CurrentClaims.Add(claim); + } + + public void AddClaimToCurrentToken(string type, string value) => AddClaimToCurrentToken(new Claim(type, value)); + + public void AddToken(TokenResult result) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + if (!result.Token.IsOfKind(CurrentToken)) + { + throw new InvalidOperationException( + $"Can't add a result of token type '{result.Token.Kind}' to a context of '{CurrentToken ?? "(null)"}'"); + } + + _issuedTokens.Add(result); + CurrentToken = null; + CurrentClaims = null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenKinds.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenKinds.cs new file mode 100644 index 0000000000..3ec8f1c32f --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenKinds.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenKinds + { + public static readonly string Bearer = "Bearer"; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenRequest.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenRequest.cs new file mode 100644 index 0000000000..e75dde1d76 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenRequest.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenRequest + { + private TokenRequest(OpenIdConnectMessage error) + { + IsValid = false; + Error = error; + } + + private TokenRequest( + OpenIdConnectMessage request, + string userId, + string clientId, + RequestGrants grants) + { + IsValid = true; + Request = request; + UserId = userId; + ClientId = clientId; + RequestGrants = grants; + } + + public bool IsValid { get; } + public OpenIdConnectMessage Request { get; } + public string UserId { get; } + public string ClientId { get; } + public OpenIdConnectMessage Error { get; } + public RequestGrants RequestGrants { get; } + + public static TokenRequest Invalid(OpenIdConnectMessage error) + { + return new TokenRequest(error); + } + + public static TokenRequest Valid( + OpenIdConnectMessage request, + string userId, + string clientId, + RequestGrants grants) + { + return new TokenRequest(request, userId, clientId, grants); + } + + public TokenGeneratingContext CreateTokenGeneratingContext(ClaimsPrincipal user, ClaimsPrincipal application) + { + return new TokenGeneratingContext(user, application, Request, RequestGrants); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenTypes.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenTypes.cs new file mode 100644 index 0000000000..c5d944702b --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/TokenTypes.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenTypes + { + public const string AuthorizationCode = "code"; + public const string AccessToken = "access_token"; + public const string RefreshToken = "refresh_token"; + public const string IdToken = "id_token"; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/AccessToken.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/AccessToken.cs new file mode 100644 index 0000000000..b52336c10d --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/AccessToken.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AccessToken : Token + { + public AccessToken(IEnumerable claims) + : base(ValidateClaims(claims)) + { + } + + private static IEnumerable ValidateClaims(IEnumerable claims) + { + EnsureUniqueClaim(IdentityServiceClaimTypes.Issuer, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.Subject, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.Audience, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.Scope, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.AuthorizedParty, claims); + return claims; + } + + public override string Kind => TokenTypes.AccessToken; + public string Issuer => GetClaimValue(IdentityServiceClaimTypes.Issuer); + public string Subject => GetClaimValue(IdentityServiceClaimTypes.Subject); + public string Audience => GetClaimValue(IdentityServiceClaimTypes.Audience); + public string AuthorizedParty => GetClaimValue(IdentityServiceClaimTypes.AuthorizedParty); + public IEnumerable Scopes => GetClaimValuesOrEmpty(IdentityServiceClaimTypes.Scope); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/AuthorizationCode.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/AuthorizationCode.cs new file mode 100644 index 0000000000..d052d59691 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/AuthorizationCode.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationCode : Token + { + public AuthorizationCode(IEnumerable claims) + : base(ValidateClaims(claims)) + { + } + + private static IEnumerable ValidateClaims(IEnumerable claims) + { + EnsureUniqueClaim(IdentityServiceClaimTypes.UserId, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.ClientId, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.RedirectUri, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.Scope, claims); + EnsureRequiredClaim(IdentityServiceClaimTypes.GrantedToken, claims); + + return claims; + } + + public override string Kind => TokenTypes.AuthorizationCode; + public string UserId => GetClaimValue(IdentityServiceClaimTypes.UserId); + public string ClientId => GetClaimValue(IdentityServiceClaimTypes.ClientId); + public string Resource => GetClaimValue(IdentityServiceClaimTypes.Resource); + public string RedirectUri => GetClaimValue(IdentityServiceClaimTypes.RedirectUri); + public IEnumerable Scopes => GetClaimValuesOrEmpty(IdentityServiceClaimTypes.Scope); + public IEnumerable GrantedTokens => GetClaimValuesOrEmpty(IdentityServiceClaimTypes.GrantedToken); + public string Nonce => GetClaimValue(IdentityServiceClaimTypes.Nonce); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/IdToken.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/IdToken.cs new file mode 100644 index 0000000000..ed881b406f --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/IdToken.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdToken : Token + { + public IdToken(IEnumerable claims) + : base(ValidateClaims(claims)) + { + } + + private static IEnumerable ValidateClaims(IEnumerable claims) + { + EnsureUniqueClaim(IdentityServiceClaimTypes.Issuer, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.Subject, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.Audience, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.Nonce, claims, required: false); + EnsureUniqueClaim(IdentityServiceClaimTypes.CodeHash, claims, required: false); + EnsureUniqueClaim(IdentityServiceClaimTypes.AccessTokenHash, claims, required: false); + return claims; + } + + public override string Kind => TokenTypes.IdToken; + public string Issuer => GetClaimValue(IdentityServiceClaimTypes.Issuer); + public string Subject => GetClaimValue(IdentityServiceClaimTypes.Subject); + public string Audience => GetClaimValue(IdentityServiceClaimTypes.Audience); + public string Nonce => GetClaimValue(IdentityServiceClaimTypes.Nonce); + public string CodeHash => GetClaimValue(IdentityServiceClaimTypes.CodeHash); + public string AccessTokenHash => GetClaimValue(IdentityServiceClaimTypes.AccessTokenHash); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/RefreshToken.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/RefreshToken.cs new file mode 100644 index 0000000000..3df30ea450 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/RefreshToken.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class RefreshToken : Token + { + public RefreshToken(IEnumerable claims) + : base(ValidateClaims(claims)) + { + } + + private static IEnumerable ValidateClaims(IEnumerable claims) + { + EnsureUniqueClaim(IdentityServiceClaimTypes.UserId, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.ClientId, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.Scope, claims); + EnsureRequiredClaim(IdentityServiceClaimTypes.GrantedToken, claims); + + return claims; + } + + public override string Kind => TokenTypes.RefreshToken; + public string UserId => GetClaimValue(IdentityServiceClaimTypes.UserId); + public string ClientId => GetClaimValue(IdentityServiceClaimTypes.ClientId); + public string Resource => GetClaimValue(IdentityServiceClaimTypes.Resource); + public string Issuer => GetClaimValue(IdentityServiceClaimTypes.Issuer); + public IEnumerable GrantedTokens => GetClaimValuesOrEmpty(IdentityServiceClaimTypes.GrantedToken); + public IEnumerable Scopes => GetClaimValuesOrEmpty(IdentityServiceClaimTypes.Scope); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/Token.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/Token.cs new file mode 100644 index 0000000000..53221f1f6a --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Tokens/Token.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public abstract class Token : IEnumerable + { + private readonly IList _claims = new List(); + + protected Token(IEnumerable claims) + { + EnsureUniqueClaim(IdentityServiceClaimTypes.TokenUniqueId, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.IssuedAt, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.Expires, claims); + EnsureUniqueClaim(IdentityServiceClaimTypes.NotBefore, claims); + _claims = new List(claims); + } + + internal static void EnsureUniqueClaim(string claimType, IEnumerable claims, bool required = true) + { + var count = 0; + foreach (var claim in claims) + { + if (string.Equals(claimType, claim.Type, StringComparison.Ordinal)) + { + count++; + } + + if (count > 1) + { + break; + } + } + + if (count == 0 && required) + { + throw new InvalidOperationException($"'{claimType}' is required."); + } + + if (count > 1) + { + throw new InvalidOperationException($"'{claimType}' must be unique."); + } + } + + internal static void EnsureRequiredClaim(string claimType, IEnumerable claims) + { + foreach (var claim in claims) + { + if (string.Equals(claimType, claim.Type, StringComparison.Ordinal)) + { + return; + } + } + + throw new InvalidOperationException($"'{claimType}' not found."); + } + + public abstract string Kind { get; } + public virtual string Id => GetClaimValue(IdentityServiceClaimTypes.TokenUniqueId); + public virtual DateTimeOffset IssuedAt => GetClaimValueOrNull(IdentityServiceClaimTypes.IssuedAt, v => EpochTime.DateTime(long.Parse(v))); + public virtual DateTimeOffset Expires => GetClaimValueOrNull(IdentityServiceClaimTypes.Expires, v => EpochTime.DateTime(long.Parse(v))); + public virtual DateTimeOffset NotBefore => GetClaimValueOrNull(IdentityServiceClaimTypes.NotBefore, v => EpochTime.DateTime(long.Parse(v))); + + public bool IsOfKind(string tokenType) => Kind.Equals(tokenType, StringComparison.Ordinal); + + public virtual string GetClaimValue(string claimType) + { + foreach (var claim in _claims) + { + if (string.Equals(claimType, claim.Type, StringComparison.Ordinal)) + { + return claim.Value; + } + } + + return null; + } + + protected virtual T GetClaimValueOrNull(string claimType, Func valueFactory) + { + foreach (var claim in _claims) + { + if (string.Equals(claimType, claim.Type, StringComparison.Ordinal)) + { + return valueFactory(claim.Value); + } + } + + return default(T); + } + + protected virtual IEnumerable GetClaimValuesOrEmpty(string claimType) + { + foreach (var claim in _claims) + { + if (string.Equals(claimType, claim.Type, StringComparison.Ordinal)) + { + yield return claim.Value; + } + } + } + + public IEnumerator GetEnumerator() => _claims.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _claims.GetEnumerator(); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IAuthorizationRequestValidator.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IAuthorizationRequestValidator.cs new file mode 100644 index 0000000000..74b4ca47df --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IAuthorizationRequestValidator.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IAuthorizationRequestValidator + { + Task ValidateRequestAsync(AuthorizationRequest request); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IClientIdValidator.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IClientIdValidator.cs new file mode 100644 index 0000000000..6447a8d80e --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IClientIdValidator.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IClientIdValidator + { + Task ValidateClientIdAsync(string clientId); + Task ValidateClientCredentialsAsync(string clientId, string clientSecret); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IRedirectUriResolver.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IRedirectUriResolver.cs new file mode 100644 index 0000000000..b41ff4cd9c --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IRedirectUriResolver.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IRedirectUriResolver + { + Task ResolveRedirectUriAsync(string clientId, string redirectUrl); + Task ResolveLogoutUriAsync(string clientId, string logoutUrl); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IScopeResolver.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IScopeResolver.cs new file mode 100644 index 0000000000..2bb18b686f --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/IScopeResolver.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IScopeResolver + { + Task ResolveScopesAsync(string clientId, IEnumerable scopes); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/ITimeStampManager.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/ITimeStampManager.cs new file mode 100644 index 0000000000..46c0b97a97 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/ITimeStampManager.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ITimeStampManager + { + DateTimeOffset GetCurrentTimeStampUtc(); + DateTimeOffset GetTimeStampUtc(TimeSpan validityPeriod); + DateTime GetCurrentTimeStampUtcAsDateTime(); + DateTime GetTimeStampUtcAsDateTime(TimeSpan validityPeriod); + string GetTimeStampInEpochTime(TimeSpan validityPeriod); + string GetCurrentTimeStampInEpochTime(); + DateTimeOffset GetTimeStampFromEpochTime(string epochTime); + long GetDurationInSeconds(DateTimeOffset end, DateTimeOffset beginning); + bool IsValidPeriod(DateTimeOffset start, DateTimeOffset end); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/ITokenRequestValidator.cs b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/ITokenRequestValidator.cs new file mode 100644 index 0000000000..3b5e4927db --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Abstractions/Validation/ITokenRequestValidator.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface ITokenRequestValidator + { + Task ValidateRequestAsync(TokenRequest request); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/AuthorizationCodeIssuer.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/AuthorizationCodeIssuer.cs new file mode 100644 index 0000000000..0a05c32bbb --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/AuthorizationCodeIssuer.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationCodeIssuer : IAuthorizationCodeIssuer + { + private readonly ISecureDataFormat _dataFormat; + private readonly ITokenClaimsManager _claimsManager; + private readonly ProtocolErrorProvider _errorProvider; + + public AuthorizationCodeIssuer( + ITokenClaimsManager claimsManager, + ISecureDataFormat dataFormat, + ProtocolErrorProvider errorProvider) + { + _claimsManager = claimsManager; + _dataFormat = dataFormat; + _errorProvider = errorProvider; + } + + public async Task CreateAuthorizationCodeAsync(TokenGeneratingContext context) + { + await _claimsManager.CreateClaimsAsync(context); + var claims = context.CurrentClaims; + + var code = new AuthorizationCode(claims); + + var tokenResult = new TokenResult(code, _dataFormat.Protect(code)); + + context.AddToken(tokenResult); + } + + public Task ExchangeAuthorizationCodeAsync(OpenIdConnectMessage message) + { + var code = _dataFormat.Unprotect(message.Code); + + if (code == null) + { + return Task.FromResult(AuthorizationGrant.Invalid(_errorProvider.InvalidAuthorizationCode())); + } + + var userId = code.UserId; + var clientId = code.ClientId; + var scopes = code.Scopes; + var resource = code.Resource; + var nonce = code.Nonce; + + var tokenTypes = code.GrantedTokens; + var grantedScopes = scopes.SelectMany(s => s.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) + .Select(s => ApplicationScope.CanonicalScopes.TryGetValue(s, out var canonicalScope) ? canonicalScope : new ApplicationScope(resource, s)) + .ToList(); + + return Task.FromResult(AuthorizationGrant.Valid(userId, clientId, tokenTypes, grantedScopes, code)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/AuthorizationRequestFactory.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/AuthorizationRequestFactory.cs new file mode 100644 index 0000000000..39d28f5eef --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/AuthorizationRequestFactory.cs @@ -0,0 +1,398 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationRequestFactory : IAuthorizationRequestFactory + { + private static readonly string[] ValidResponseTypes = new string[] { + OpenIdConnectResponseType.None, + OpenIdConnectResponseType.Token, + OpenIdConnectResponseType.IdToken, + OpenIdConnectResponseType.Code + }; + + private static readonly string[] ValidResponseModes = new string[] { + OpenIdConnectResponseMode.Query, + OpenIdConnectResponseMode.Fragment, + OpenIdConnectResponseMode.FormPost + }; + + private readonly IClientIdValidator _clientIdValidator; + private readonly IRedirectUriResolver _redirectUrlValidator; + private readonly IScopeResolver _scopeValidator; + private readonly IEnumerable _validators; + private readonly ProtocolErrorProvider _errorProvider; + + public AuthorizationRequestFactory( + IClientIdValidator clientIdValidator, + IRedirectUriResolver redirectUriValidator, + IScopeResolver scopeValidator, + IEnumerable validators, + ProtocolErrorProvider errorProvider) + { + _clientIdValidator = clientIdValidator; + _redirectUrlValidator = redirectUriValidator; + _scopeValidator = scopeValidator; + _validators = validators; + _errorProvider = errorProvider; + } + + public async Task CreateAuthorizationRequestAsync(IDictionary requestParameters) + { + // Parameters sent without a value MUST be treated as if they were + // omitted from the request.The authorization server MUST ignore + // unrecognized request parameters.Request and response parameters + // MUST NOT be included more than once. + + // Validate that we only got send one state property as it needs to be included in all responses (including error ones) + var (state, stateError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.State, _errorProvider); + + + // Start by validating the client_id and redirect_uri as any of them being invalid indicates that we need to + // return a 400 response instead of a 302 response with the error. This is signaled by the result not containing + // a url to redirect to. + var (clientId, redirectUri, clientError) = await ValidateClientIdAndRedirectUri(requestParameters, state); + if (clientError != null) + { + // Send first the state error if there was one. + return AuthorizationRequest.Invalid(new AuthorizationRequestError( + stateError ?? clientError, + redirectUri: null, + responseMode: null)); + } + + // We need to determine what response mode to use to send the errors in case there are any. + // In case the response type and response modes are valid, we should use those values when + // notifying clients of the errors. + // In case there is an issue with the response type or the response mode we need to determine + // how to notify the relying party of the errors. + // We can divide this in two situations: + // The response mode is invalid: + // * We ignore the response mode and base our response based on the response type specified. + // If a token was requested we send the error response on the fragment of the redirect uri. + // If no token was requested we send the error response on the query of the redirect uri. + // The response type is invalid: + // * We try to determine if this is a hybrid or implicit flow: + // If the invalid response type contained a request for an id_token or an access_token, or + // contained more than one space separated value, we send the response on the fragment, + // unless the response mode is specified and form_post. + // If the invalid response type only contained one value and we can not determine is an + // implicit request flow, we return the error on the query string unless the response mode + // is specified and form_post or fragment. + + var (responseType, parsedResponseType, tokenRequested, responseTypeError) = ValidateResponseType(requestParameters); + var (responseMode, responseModeError) = ValidateResponseMode(requestParameters); + + var invalidCombinationError = ValidateResponseModeTypeCombination(responseType, tokenRequested, responseMode); + if (responseModeError != null || responseMode == null) + { + responseMode = GetResponseMode(parsedResponseType, tokenRequested); + } + + if (responseTypeError != null) + { + responseTypeError.State = state; + return AuthorizationRequest.Invalid( + new AuthorizationRequestError(stateError ?? responseTypeError, redirectUri, responseMode)); + } + + if (responseModeError != null) + { + responseModeError.State = state; + return AuthorizationRequest.Invalid( + new AuthorizationRequestError(stateError ?? responseModeError, redirectUri, responseMode)); + } + + if (invalidCombinationError != null) + { + invalidCombinationError.State = state; + return AuthorizationRequest.Invalid( + new AuthorizationRequestError(stateError ?? invalidCombinationError, redirectUri, responseMode)); + } + + var (nonce, nonceError) = tokenRequested ? + RequestParametersHelper.ValidateParameterIsUnique(requestParameters, OpenIdConnectParameterNames.Nonce, _errorProvider) : + RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.Nonce, _errorProvider); + + if (nonceError != null) + { + nonceError.State = state; + return AuthorizationRequest.Invalid(new AuthorizationRequestError(nonceError, redirectUri, responseMode)); + } + + var (scope, scopeError) = RequestParametersHelper.ValidateParameterIsUnique(requestParameters, OpenIdConnectParameterNames.Scope, _errorProvider); + if (scopeError != null) + { + scopeError.State = state; + return AuthorizationRequest.Invalid(new AuthorizationRequestError(scopeError, redirectUri, responseMode)); + } + + var parsedScope = scope.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var allWhiteSpace = true; + for (int i = 0; i < parsedScope.Length; i++) + { + allWhiteSpace = string.IsNullOrWhiteSpace(parsedScope[i]); + if (!allWhiteSpace) + { + break; + } + } + + if (allWhiteSpace) + { + scopeError = _errorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.Scope); + scopeError.State = state; + + return AuthorizationRequest.Invalid(new AuthorizationRequestError( + scopeError, + redirectUri, + responseMode)); + } + + if (parsedResponseType.Contains(OpenIdConnectResponseType.IdToken) && !parsedScope.Contains(OpenIdConnectScope.OpenId)) + { + scopeError = _errorProvider.MissingOpenIdScope(); + scopeError.State = state; + + return AuthorizationRequest.Invalid(new AuthorizationRequestError( + scopeError, + redirectUri, + responseMode)); + } + + var resolvedScopes = await _scopeValidator.ResolveScopesAsync(clientId, parsedScope); + if (!resolvedScopes.IsValid) + { + resolvedScopes.Error.State = state; + return AuthorizationRequest.Invalid(new AuthorizationRequestError(resolvedScopes.Error, redirectUri, responseMode)); + } + + var (prompt, promptError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.Prompt, _errorProvider); + if (promptError != null) + { + promptError.State = state; + return AuthorizationRequest.Invalid(new AuthorizationRequestError(promptError, redirectUri, responseMode)); + } + + if (prompt != null) + { + var parsedPrompt = prompt.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + promptError = ValidatePrompt(parsedPrompt); + if (promptError != null) + { + promptError.State = state; + return AuthorizationRequest.Invalid(new AuthorizationRequestError(promptError, redirectUri, responseMode)); + } + } + + var result = new OpenIdConnectMessage(requestParameters); + result.RequestType = OpenIdConnectRequestType.Authentication; + + var requestGrants = new RequestGrants + { + Tokens = GetRequestedTokens(parsedResponseType, resolvedScopes.Scopes), + Scopes = resolvedScopes.Scopes.ToList(), + ResponseMode = responseMode, + RedirectUri = redirectUri + }; + + return await ValidateRequestAsync(AuthorizationRequest.Valid(result, requestGrants)); + } + + private IList GetRequestedTokens(IEnumerable parsedResponseType, IEnumerable scopes) + { + var tokens = new List(); + foreach (var response in parsedResponseType) + { + switch (response) + { + case OpenIdConnectResponseType.Code: + tokens.Add(TokenTypes.AuthorizationCode); + break; + case OpenIdConnectResponseType.Token when HasCustomScope(): + tokens.Add(TokenTypes.AccessToken); + break; + case OpenIdConnectResponseType.IdToken when HasOpenIdScope(): + tokens.Add(TokenTypes.IdToken); + break; + default: + break; + } + } + + return tokens; + + bool HasCustomScope() => scopes.Any(s => s.ClientId != null); + bool HasOpenIdScope() => scopes.Contains(ApplicationScope.OpenId); + } + + + private async Task ValidateRequestAsync(AuthorizationRequest authorizationRequest) + { + foreach (var validator in _validators) + { + var newRequest = await validator.ValidateRequestAsync(authorizationRequest); + if (!newRequest.IsValid) + { + return newRequest; + } + } + + return authorizationRequest; + } + + private OpenIdConnectMessage ValidatePrompt(string[] parsedPrompt) + { + for (int i = 0; i < parsedPrompt.Length; i++) + { + var prompt = parsedPrompt[i]; + if (string.Equals(prompt, PromptValues.None, StringComparison.Ordinal)) + { + if (parsedPrompt.Length > 1) + { + return _errorProvider.PromptNoneMustBeTheOnlyValue(string.Join(" ", parsedPrompt)); + } + + continue; + } + + if (string.Equals(prompt, PromptValues.Login, StringComparison.Ordinal) || + string.Equals(prompt, PromptValues.Consent, StringComparison.Ordinal) || + string.Equals(prompt, PromptValues.SelectAccount, StringComparison.Ordinal)) + { + continue; + } + + return _errorProvider.InvalidPromptValue(prompt); + } + + return null; + } + + private static string GetResponseMode(string[] parsedResponseType, bool tokenRequested) + { + return tokenRequested || parsedResponseType != null && parsedResponseType.Length > 1 + ? OpenIdConnectResponseMode.Fragment : OpenIdConnectResponseMode.Query; + } + + private OpenIdConnectMessage ValidateResponseModeTypeCombination(string responseType, bool tokenRequested, string responseMode) + { + return tokenRequested && responseMode != null && responseMode.Equals(OpenIdConnectResponseMode.Query) ? + _errorProvider.InvalidResponseTypeModeCombination(responseType, responseMode) : + null; + } + + private (string responseMode, OpenIdConnectMessage responseModeError) ValidateResponseMode(IDictionary parameters) + { + var (responseMode, responseModeParameterError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(parameters, OpenIdConnectParameterNames.ResponseMode, _errorProvider); + var responseModeValidationError = responseMode != null && !ValidResponseModes.Contains(responseMode) ? + _errorProvider.InvalidParameterValue(responseMode, OpenIdConnectParameterNames.ResponseMode) : + null; + var isResponseModeInvalid = responseModeParameterError != null || responseModeValidationError != null; + + return (isResponseModeInvalid ? null : responseMode, responseModeParameterError ?? responseModeValidationError); + } + + private (string responseType, string[] parsedResponseType, bool tokenRequested, OpenIdConnectMessage error) ValidateResponseType(IDictionary parameters) + { + var (responseType, responseTypeParameterError) = RequestParametersHelper.ValidateParameterIsUnique(parameters, OpenIdConnectParameterNames.ResponseType, _errorProvider); + var parsedResponseType = responseType?.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var (tokenRequested, responseTypeValidationError) = parsedResponseType != null ? IsValidResponseTypeCombination(parsedResponseType) : (false, null); + + return (responseType, parsedResponseType, tokenRequested, responseTypeParameterError ?? responseTypeValidationError); + } + + private (bool tokenRequested, OpenIdConnectMessage error) IsValidResponseTypeCombination(string[] parsedResponseType) + { + var containsNone = false; + var tokenRequested = false; + for (var i = 0; i < parsedResponseType.Length; i++) + { + containsNone = containsNone || string.Equals(OpenIdConnectResponseType.None, parsedResponseType[i], StringComparison.Ordinal); + + tokenRequested = tokenRequested || + string.Equals(OpenIdConnectResponseType.Token, parsedResponseType[i], StringComparison.Ordinal) || + string.Equals(OpenIdConnectResponseType.IdToken, parsedResponseType[i], StringComparison.Ordinal); + } + + if (containsNone && parsedResponseType.Length > 1) + { + return (tokenRequested, _errorProvider.ResponseTypeNoneNotAllowed()); + } + + for (var i = 0; i < parsedResponseType.Length; i++) + { + if (!ValidResponseTypes.Contains(parsedResponseType[i])) + { + var error = _errorProvider.InvalidParameterValue( + parsedResponseType[i], + OpenIdConnectParameterNames.ResponseType); + return (tokenRequested, error); + } + } + + return (tokenRequested, null); + + } + + private async Task<(string clientId, string redirectUri, OpenIdConnectMessage error)> ValidateClientIdAndRedirectUri( + IDictionary requestParameters, string state) + { + var (clientId, clientIdError) = RequestParametersHelper.ValidateParameterIsUnique(requestParameters, OpenIdConnectParameterNames.ClientId, _errorProvider); + if (clientIdError != null) + { + clientIdError.State = state; + return (null, null, clientIdError); + } + + if (!await _clientIdValidator.ValidateClientIdAsync(clientId)) + { + clientIdError = _errorProvider.InvalidClientId(clientId); + clientIdError.State = state; + + return (null, null, clientIdError); + } + + var (redirectUri, redirectUriError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.RedirectUri, _errorProvider); + if (redirectUriError != null) + { + redirectUriError.State = state; + return (null, null, redirectUriError); + } + + if (redirectUri != null) + { + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + redirectUriError = _errorProvider.InvalidUriFormat(redirectUri); + redirectUriError.State = state; + return (null, null, redirectUriError); + } + + var parsedUri = new Uri(redirectUri, UriKind.Absolute); + if (!string.IsNullOrEmpty(parsedUri.Fragment)) + { + redirectUriError = _errorProvider.InvalidUriFormat(redirectUri); + redirectUriError.State = state; + return (null, null, redirectUriError); + } + } + + var resolvedUriResult = await _redirectUrlValidator.ResolveRedirectUriAsync(clientId, redirectUri); + if (!resolvedUriResult.IsValid) + { + resolvedUriResult.Error.State = state; + return (null, null, resolvedUriResult.Error); + } + + return (clientId, resolvedUriResult.Uri, null); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/DefaultTokenClaimsManager.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/DefaultTokenClaimsManager.cs new file mode 100644 index 0000000000..56fdf1fb4d --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/DefaultTokenClaimsManager.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Service.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultTokenClaimsManager : ITokenClaimsManager + { + private readonly ITokenClaimsProvider[] _providers; + + public DefaultTokenClaimsManager(IEnumerable providers) + { + _providers = providers.OrderBy(p => p.Order).ToArray(); + } + + public async Task CreateClaimsAsync(TokenGeneratingContext context) + { + foreach (var provider in _providers) + { + await provider.OnGeneratingClaims(context); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/DefaultTokenClaimsProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/DefaultTokenClaimsProvider.cs new file mode 100644 index 0000000000..ec1a1d75de --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/DefaultTokenClaimsProvider.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class DefaultTokenClaimsProvider : ITokenClaimsProvider + { + private readonly IOptions _options; + + public DefaultTokenClaimsProvider(IOptions options) + { + _options = options; + } + + public int Order => 100; + + public Task OnGeneratingClaims(TokenGeneratingContext context) + { + context.AddClaimToCurrentToken(IdentityServiceClaimTypes.TokenUniqueId, Guid.NewGuid().ToString()); + + var userMapping = GetUserPrincipalTokenMapping(context.CurrentToken); + var applicationMapping = GetApplicationPrincipalTokenMapping(context.CurrentToken); + var ambientMapping = GetAmbientClaimsTokenMapping(context.CurrentToken); + + MapFromPrincipal(context, context.User, userMapping); + MapFromPrincipal(context, context.Application, applicationMapping); + MapFromContext(context, context.AmbientClaims, ambientMapping); + + if (context.IsContextForTokenTypes(TokenTypes.AccessToken, TokenTypes.IdToken)) + { + context.AddClaimToCurrentToken(IdentityServiceClaimTypes.Issuer, _options.Value.Issuer); + } + + if (context.IsContextForTokenTypes(TokenTypes.AuthorizationCode) && context.RequestParameters.RedirectUri != null) + { + context.AddClaimToCurrentToken(IdentityServiceClaimTypes.RedirectUri, context.RequestParameters.RedirectUri); + } + + return Task.CompletedTask; + } + + private TokenMapping GetAmbientClaimsTokenMapping(string tokenType) + { + switch (tokenType) + { + case TokenTypes.AuthorizationCode: + return _options.Value.AuthorizationCodeOptions.ContextClaims; + case TokenTypes.AccessToken: + return _options.Value.AccessTokenOptions.ContextClaims; + case TokenTypes.IdToken: + return _options.Value.IdTokenOptions.ContextClaims; + case TokenTypes.RefreshToken: + return _options.Value.RefreshTokenOptions.ContextClaims; + default: + throw new InvalidOperationException(); + } + } + + private TokenMapping GetApplicationPrincipalTokenMapping(string tokenType) + { + switch (tokenType) + { + case TokenTypes.AuthorizationCode: + return _options.Value.AuthorizationCodeOptions.ApplicationClaims; + case TokenTypes.AccessToken: + return _options.Value.AccessTokenOptions.ApplicationClaims; + case TokenTypes.IdToken: + return _options.Value.IdTokenOptions.ApplicationClaims; + case TokenTypes.RefreshToken: + return _options.Value.RefreshTokenOptions.ApplicationClaims; + default: + throw new InvalidOperationException(); + } + } + + private TokenMapping GetUserPrincipalTokenMapping(string tokenType) + { + switch (tokenType) + { + case TokenTypes.AuthorizationCode: + return _options.Value.AuthorizationCodeOptions.UserClaims; + case TokenTypes.AccessToken: + return _options.Value.AccessTokenOptions.UserClaims; + case TokenTypes.IdToken: + return _options.Value.IdTokenOptions.UserClaims; + case TokenTypes.RefreshToken: + return _options.Value.RefreshTokenOptions.UserClaims; + default: + throw new InvalidOperationException(); + } + } + + private static void MapFromPrincipal( + TokenGeneratingContext context, + ClaimsPrincipal user, + TokenMapping claimsDefinition) + { + foreach (var mapping in claimsDefinition) + { + var foundClaims = user.FindAll(mapping.Alias); + ValidateCardinality(mapping, foundClaims, claimsDefinition.Source); + foreach (var userClaim in foundClaims) + { + context.AddClaimToCurrentToken(mapping.Name, userClaim.Value); + } + } + } + + private static void MapFromContext( + TokenGeneratingContext context, + IList ambientClaims, + TokenMapping claimsDefinition) + { + foreach (var mapping in claimsDefinition) + { + var ctxValues = ambientClaims.Where(c => c.Type == mapping.Alias); + ValidateCardinality(mapping, ctxValues, claimsDefinition.Source); + foreach (var ctxValue in ctxValues) + { + context.AddClaimToCurrentToken(mapping.Name, ctxValue.Value); + } + } + } + + private static void ValidateCardinality(TokenValueDescriptor mapping, IEnumerable foundClaims, string source) + { + if (mapping.Cardinality != TokenValueCardinality.Zero && !foundClaims.Any()) + { + throw new InvalidOperationException($"Missing '{mapping.Alias}' claim from the {source}."); + } + + if (mapping.Cardinality != TokenValueCardinality.Many && foundClaims.Skip(1).Any()) + { + throw new InvalidOperationException($"Multiple claims found for '{mapping.Alias}' claim from the {source}."); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/GrantedTokensTokenClaimsProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/GrantedTokensTokenClaimsProvider.cs new file mode 100644 index 0000000000..587a89e84c --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/GrantedTokensTokenClaimsProvider.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class GrantedTokensTokenClaimsProvider : ITokenClaimsProvider + { + public int Order => 100; + + public Task OnGeneratingClaims(TokenGeneratingContext context) + { + if (context.IsContextForTokenTypes(TokenTypes.AuthorizationCode)) + { + foreach (var grantedToken in GetGrantedTokensForAuthorizationCode(context)) + { + context.AddClaimToCurrentToken(IdentityServiceClaimTypes.GrantedToken, grantedToken); + } + } + + if (context.IsContextForTokenTypes(TokenTypes.RefreshToken)) + { + foreach (var grantedToken in context.RequestGrants.Tokens) + { + context.AddClaimToCurrentToken(IdentityServiceClaimTypes.GrantedToken, grantedToken); + } + } + + return Task.CompletedTask; + } + + private IEnumerable GetGrantedTokensForAuthorizationCode(TokenGeneratingContext context) + { + if (context.RequestGrants.Scopes.Any(s => s.ClientId != null)) + { + yield return TokenTypes.AccessToken; + } + + if (context.RequestGrants.Scopes.Contains(ApplicationScope.OpenId)) + { + yield return TokenTypes.IdToken; + } + + if (context.RequestGrants.Scopes.Contains(ApplicationScope.OfflineAccess)) + { + yield return TokenTypes.RefreshToken; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/NonceTokenClaimsProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/NonceTokenClaimsProvider.cs new file mode 100644 index 0000000000..1ae16f0b71 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/NonceTokenClaimsProvider.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class NonceTokenClaimsProvider : ITokenClaimsProvider + { + public int Order => 100; + + public Task OnGeneratingClaims(TokenGeneratingContext context) + { + var nonce = GetNonce(context); + if (context.IsContextForTokenTypes( + TokenTypes.IdToken, + TokenTypes.AccessToken, + TokenTypes.AuthorizationCode) && nonce != null) + { + context.AddClaimToCurrentToken(IdentityServiceClaimTypes.Nonce, nonce); + } + + return Task.CompletedTask; + } + + private string GetNonce(TokenGeneratingContext context) => + context.RequestParameters.RequestType == OpenIdConnectRequestType.Authentication ? + context.RequestParameters.Nonce : + context.RequestGrants.Claims.SingleOrDefault(c => c.Type.Equals(IdentityServiceClaimTypes.Nonce))?.Value; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/ScopesTokenClaimsProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/ScopesTokenClaimsProvider.cs new file mode 100644 index 0000000000..13c47b14c0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/ScopesTokenClaimsProvider.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class ScopesTokenClaimsProvider : ITokenClaimsProvider + { + public int Order => 100; + + public Task OnGeneratingClaims(TokenGeneratingContext context) + { + var resource = context.RequestGrants.Scopes.FirstOrDefault(rg => rg.ClientId != null)?.ClientId; + + if (context.IsContextForTokenTypes(TokenTypes.AccessToken) && resource != null) + { + // For access tokens we use the scopes from the set of granted scopes, this takes into account the + // fact that a token request can ask for a subset of the scopes granted during authorization, either + // on a code exchange or on a refresh token grant flow. + AddClaimsForAccessToken(context, resource); + return Task.CompletedTask; + } + + if (context.IsContextForTokenTypes(TokenTypes.AuthorizationCode)) + { + context.AddClaimToCurrentToken( + IdentityServiceClaimTypes.Scope, + GetScopeValue(context.RequestGrants.Scopes, excludeCanonical: false)); + + if (resource != null) + { + context.AddClaimToCurrentToken(IdentityServiceClaimTypes.Resource, resource); + } + + return Task.CompletedTask; + } + + + if (context.IsContextForTokenTypes(TokenTypes.RefreshToken)) + { + // For refresh tokens the scope claim never changes as the set of scopes granted for a refresh token + // should not change no matter what scopes are sent on a token request. + var scopeClaim = context + .RequestGrants + .Claims + .Single(c => c.Type.Equals(IdentityServiceClaimTypes.Scope, StringComparison.Ordinal)); + + var resourceClaim = context + .RequestGrants + .Claims + .SingleOrDefault(c => c.Type.Equals(IdentityServiceClaimTypes.Resource, StringComparison.Ordinal)); + + context.AddClaimToCurrentToken(scopeClaim); + + if (resourceClaim != null) + { + context.AddClaimToCurrentToken(resourceClaim); + } + } + + return Task.CompletedTask; + } + + private void AddClaimsForAccessToken(TokenGeneratingContext context, string resource) + { + var scopes = context.RequestGrants.Scopes; + var accessTokenScopes = GetAccessTokenScopes(scopes); + + context.AddClaimToCurrentToken(IdentityServiceClaimTypes.Scope, GetScopeValue(scopes, excludeCanonical: true)); + context.AddClaimToCurrentToken(IdentityServiceClaimTypes.Audience, resource); + context.AddClaimToCurrentToken(IdentityServiceClaimTypes.AuthorizedParty, context.RequestParameters.ClientId); + } + + private IEnumerable GetAccessTokenScopes(IEnumerable applicationScopes) => + applicationScopes.Where(s => s.ClientId != null); + + private string GetScopeValue(IEnumerable scopes, bool excludeCanonical) => + !excludeCanonical ? + string.Join(" ", scopes.Select(s => s.Scope)) : + string.Join(" ", scopes.Where(s => s.ClientId != null).Select(s => s.Scope)); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/TimestampsTokenClaimsProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/TimestampsTokenClaimsProvider.cs new file mode 100644 index 0000000000..860389e983 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/TimestampsTokenClaimsProvider.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class TimestampsTokenClaimsProvider : ITokenClaimsProvider + { + private readonly ITimeStampManager _timeStampManager; + private readonly IOptions _options; + + public TimestampsTokenClaimsProvider( + ITimeStampManager timestampManager, + IOptions options) + { + _timeStampManager = timestampManager; + _options = options; + } + + public int Order => 100; + + public Task OnGeneratingClaims(TokenGeneratingContext context) + { + var options = GetOptions(context.CurrentToken); + context.CurrentClaims.Add(new Claim( + IdentityServiceClaimTypes.NotBefore, + _timeStampManager.GetTimeStampInEpochTime(options.NotValidBefore))); + + context.CurrentClaims.Add(new Claim( + IdentityServiceClaimTypes.IssuedAt, + _timeStampManager.GetCurrentTimeStampInEpochTime())); + + context.CurrentClaims.Add(new Claim( + IdentityServiceClaimTypes.Expires, + _timeStampManager.GetTimeStampInEpochTime(options.NotValidAfter))); + + return Task.CompletedTask; + } + + private TokenOptions GetOptions(string tokenType) + { + switch (tokenType) + { + case TokenTypes.AccessToken: + return _options.Value.AccessTokenOptions; + case TokenTypes.AuthorizationCode: + return _options.Value.AuthorizationCodeOptions; + case TokenTypes.IdToken: + return _options.Value.IdTokenOptions; + case TokenTypes.RefreshToken: + return _options.Value.RefreshTokenOptions; + default: + throw new InvalidOperationException(); + } + } + + public Task OnValidatingClaims(TokenGeneratingContext context) + { + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/TokenHashTokenClaimsProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/TokenHashTokenClaimsProvider.cs new file mode 100644 index 0000000000..c7f813c180 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Claims/TokenHashTokenClaimsProvider.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class TokenHashTokenClaimsProvider : ITokenClaimsProvider + { + private readonly ITokenHasher _tokenHasher; + + public TokenHashTokenClaimsProvider(ITokenHasher tokenHasher) + { + _tokenHasher = tokenHasher; + } + + public int Order => 100; + + public Task OnGeneratingClaims(TokenGeneratingContext context) + { + if (context.IsContextForTokenTypes(TokenTypes.IdToken)) + { + var accessToken = context + .IssuedTokens.SingleOrDefault(t => t.Token.Kind == TokenTypes.AccessToken); + var authorizationCode = context + .IssuedTokens.SingleOrDefault(t => t.Token.Kind == TokenTypes.AuthorizationCode); + + if (accessToken != null) + { + context.CurrentClaims.Add(new Claim( + IdentityServiceClaimTypes.AccessTokenHash, + GetTokenHash(accessToken.SerializedValue))); + } + + if (authorizationCode != null) + { + context.CurrentClaims.Add(new Claim( + IdentityServiceClaimTypes.CodeHash, + GetTokenHash(authorizationCode.SerializedValue))); + } + } + + return Task.CompletedTask; + } + + private string GetTokenHash(string token) + { + return _tokenHasher.HashToken(token, "RS256"); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/CryptographyHelpers.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/CryptographyHelpers.cs new file mode 100644 index 0000000000..c29d0bca99 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/CryptographyHelpers.cs @@ -0,0 +1,99 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service +{ + internal static class CryptographyHelpers + { + public static string FindAlgorithm(X509Certificate2 certificate) + { + var rsapk = certificate.GetRSAPublicKey(); + if (rsapk != null) + { + return "RS256"; + } + else + { + throw new InvalidOperationException("Algorithm not supported."); + } + } + + public static void ValidateRsaKeyLength(X509Certificate2 certificate) + { + var rsa = certificate.GetRSAPublicKey(); + if (rsa == null) + { + throw new InvalidOperationException("Algorithm not supported."); + } + ValidateRsaKeyLength(rsa); + } + + public static void ValidateRsaKeyLength(RSA rsa) + { + if (rsa.KeySize < 2048) + { + throw new InvalidOperationException("The RSA key must be at least 2048 bits long."); + } + + return; + } + + public static RSA CreateRsaAlgorithm() => RSA.Create(2048); + + public static SHA256 CreateSHA256() => SHA256.Create(); + + public static RSAParameters GetRSAParameters(SigningCredentials credentials) + { + RSA algorithm = null; + if (credentials.Key is X509SecurityKey x509SecurityKey) + { + algorithm = x509SecurityKey.PublicKey as RSA; + } + + if (credentials.Key is RsaSecurityKey rsaSecurityKey) + { + algorithm = rsaSecurityKey.Rsa; + + if (algorithm == null) + { + var rsa = RSA.Create(); + rsa.ImportParameters(rsaSecurityKey.Parameters); + algorithm = rsa; + } + } + + var parameters = algorithm.ExportParameters(includePrivateParameters: false); + return parameters; + } + + public static string GetAlgorithm(SigningCredentials credentials) + { + RSA algorithm = null; + if (credentials.Key is X509SecurityKey x509SecurityKey && x509SecurityKey.PublicKey is RSA) + { + return JsonWebAlgorithmsKeyTypes.RSA; + } + + var rsaSecurityKey = credentials.Key as RsaSecurityKey; + // Check that the key has either an Asymetric Algorithm assigned or that at least + // one of the RSA parameters are initialized to consider the key "valid". + if (rsaSecurityKey != null && + (rsaSecurityKey.Rsa != null || rsaSecurityKey.Parameters.Modulus != null)) + { + return JsonWebAlgorithmsKeyTypes.RSA; + } + + if (algorithm != null) + { + return JsonWebAlgorithmsKeyTypes.RSA; + } + + throw new NotSupportedException(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultAuthorizationResponseFactory.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultAuthorizationResponseFactory.cs new file mode 100644 index 0000000000..d13e6f260c --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultAuthorizationResponseFactory.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultAuthorizationResponseFactory : IAuthorizationResponseFactory + { + private readonly IAuthorizationResponseParameterProvider[] _providers; + + public DefaultAuthorizationResponseFactory(IEnumerable providers) + { + _providers = providers.OrderBy(p => p.Order).ToArray(); + } + + public async Task CreateAuthorizationResponseAsync(TokenGeneratingContext context) + { + var result = new AuthorizationResponse(); + result.Message = new OpenIdConnectMessage(); + result.ResponseMode = context.RequestGrants.ResponseMode; + result.RedirectUri = context.RequestGrants.RedirectUri; + + foreach (var provider in _providers) + { + await provider.AddParameters(context, result); + } + + return result; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultAuthorizationResponseParameterProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultAuthorizationResponseParameterProvider.cs new file mode 100644 index 0000000000..2c45a1b639 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultAuthorizationResponseParameterProvider.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultAuthorizationResponseParameterProvider : IAuthorizationResponseParameterProvider + { + private readonly ITimeStampManager _manager; + + public int Order => 100; + + public DefaultAuthorizationResponseParameterProvider(ITimeStampManager manager) + { + _manager = manager; + } + + public Task AddParameters(TokenGeneratingContext context, AuthorizationResponse response) + { + if (context.AuthorizationCode != null) + { + response.Message.Code = context.AuthorizationCode.SerializedValue; + } + + if (context.AccessToken != null) + { + response.Message.AccessToken = context.AccessToken.SerializedValue; + response.Message.TokenType = "Bearer"; + response.Message.ExpiresIn = GetExpirationTime(context.AccessToken.Token); + response.Message.Scope = string.Join(" ", context.RequestGrants.Scopes.Select(s => s.Scope)); + } + + if (context.IdToken != null) + { + response.Message.IdToken = context.IdToken.SerializedValue; + } + + response.Message.State = context.RequestParameters.State; + return Task.CompletedTask; + } + + private string GetExpirationTime(Token token) + { + if (token.Expires < token.IssuedAt) + { + throw new InvalidOperationException("Can't expire before issuance."); + } + + return _manager.GetDurationInSeconds(token.Expires, token.IssuedAt).ToString(CultureInfo.InvariantCulture); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultConfigurationManager.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultConfigurationManager.cs new file mode 100644 index 0000000000..32c7759524 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultConfigurationManager.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultConfigurationManager : IConfigurationManager + { + private readonly IConfigurationMetadataProvider[] _providers; + private readonly ConcurrentDictionary>> _configurations = + new ConcurrentDictionary>>(); + + public DefaultConfigurationManager( + IEnumerable providers) + { + _providers = providers.OrderBy(p => p.Order).ToArray(); + } + + public async Task GetConfigurationAsync(ConfigurationContext context) + { + return await _configurations.GetOrAdd( + context.Id, + new Lazy>(CreateConfiguration)).Value; + + async Task CreateConfiguration() + { + var configuration = new OpenIdConnectConfiguration(); + foreach (var provider in _providers) + { + await provider.ConfigureMetadataAsync(configuration, context); + } + + return configuration; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultSigningCredentialsPolicyProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultSigningCredentialsPolicyProvider.cs new file mode 100644 index 0000000000..7fdde5b987 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultSigningCredentialsPolicyProvider.cs @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultSigningCredentialsPolicyProvider : ISigningCredentialsPolicyProvider + { + private readonly IEnumerable _sources; + private readonly IHostingEnvironment _environment; + private SigningCredentialsDescriptor[] _credentials; + private readonly ITimeStampManager _timeStampManager; + + public DefaultSigningCredentialsPolicyProvider( + IEnumerable sources, + ITimeStampManager timeStampManager, + IHostingEnvironment environment) + { + _sources = sources; + _timeStampManager = timeStampManager; + _environment = environment; + } + + public async Task> GetAllCredentialsAsync() + { + if (_credentials == null || CredentialsExpired()) + { + // This has the potential to spin up multiple calls to RetrieveCredentials + // we might consider an alternative pattern in which we hold a task in this + // instance and upon expired credentials we lock, make the call to retrieve + // credentials, swap the task on the instance, release the lock and then await. + _credentials = await RetrieveCredentials(); + } + + return _credentials; + + async Task RetrieveCredentials() + { + var credentialsFromSources = await Task.WhenAll(_sources.Select(s => s.GetCredentials())); + + var finalList = new List(); + foreach (var credential in credentialsFromSources.SelectMany(c => c)) + { + if (!_environment.IsDevelopment() && credential.Id.StartsWith("IdentityService.Development")) + { + continue; + } + + finalList.Add(credential); + } + + return finalList.OrderBy(o => o.NotBefore).ThenBy(d => d.Expires).ToArray(); + } + } + + private bool CredentialsExpired() + { + foreach (var credential in _credentials) + { + if (_timeStampManager.IsValidPeriod(credential.NotBefore, credential.Expires)) + { + return false; + } + } + + return true; + } + + public async Task GetSigningCredentialsAsync() + { + var credentials = await GetAllCredentialsAsync(); + foreach (var credential in credentials) + { + if (_timeStampManager.IsValidPeriod(credential.NotBefore, credential.Expires)) + { + return credential; + } + } + + throw new InvalidOperationException("Could not find valid credentials to use for signing."); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultSigningCredentialsSource.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultSigningCredentialsSource.cs new file mode 100644 index 0000000000..2ee0c6e4ec --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultSigningCredentialsSource.cs @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service.Core +{ + public class DefaultSigningCredentialsSource : ISigningCredentialsSource + { + private readonly IOptionsSnapshot _options; + private readonly ITimeStampManager _timeStampManager; + + public DefaultSigningCredentialsSource( + IOptionsSnapshot options, + ITimeStampManager timeStampManager) + { + _options = options; + _timeStampManager = timeStampManager; + } + + public Task> GetCredentials() + { + var descriptors = GetDescriptors(_options.Get(Options.DefaultName)); + return Task.FromResult(descriptors); + } + + private IEnumerable GetDescriptors(IdentityServiceOptions options) + { + return options.SigningKeys.Select(sk => + { + var validity = GetValidity(sk); + return new SigningCredentialsDescriptor( + sk, + CryptographyHelpers.GetAlgorithm(sk), + validity.NotBefore, + validity.Expires, + GetMetadata(sk)); + }); + } + + private Validity GetValidity(SigningCredentials credentials) + { + var x509SecurityKey = credentials.Key as X509SecurityKey; + if (x509SecurityKey != null) + { + var certificate = x509SecurityKey.Certificate; + return new Validity + { + NotBefore = certificate.NotBefore.ToUniversalTime(), + Expires = certificate.NotAfter.ToUniversalTime() + }; + } + + var rsaSecurityKey = credentials.Key as RsaSecurityKey; + if (rsaSecurityKey != null) + { + return new Validity + { + NotBefore = _timeStampManager.GetCurrentTimeStampUtcAsDateTime(), + Expires = _timeStampManager.GetTimeStampUtcAsDateTime(TimeSpan.FromDays(1)) + }; + } + + throw new NotSupportedException(); + } + + private struct Validity + { + public DateTimeOffset NotBefore; + public DateTimeOffset Expires; + } + + private IDictionary GetMetadata(SigningCredentials credentials) + { + var rsaParameters = CryptographyHelpers.GetRSAParameters(credentials); + return new Dictionary + { + [JsonWebKeyParameterNames.E] = Base64UrlEncoder.Encode(rsaParameters.Exponent), + [JsonWebKeyParameterNames.N] = Base64UrlEncoder.Encode(rsaParameters.Modulus), + }; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultTokenResponseFactory.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultTokenResponseFactory.cs new file mode 100644 index 0000000000..1769a72b71 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultTokenResponseFactory.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultTokenResponseFactory : ITokenResponseFactory + { + private readonly ITokenResponseParameterProvider[] _providers; + + public DefaultTokenResponseFactory(IEnumerable providers) + { + _providers = providers.OrderBy(o => o.Order).ToArray(); + } + + public async Task CreateTokenResponseAsync(TokenGeneratingContext context) + { + var response = new OpenIdConnectMessage(); + foreach (var provider in _providers) + { + await provider.AddParameters(context, response); + } + + return response; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultTokenResponseParameterProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultTokenResponseParameterProvider.cs new file mode 100644 index 0000000000..1bc63195bc --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/DefaultTokenResponseParameterProvider.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultTokenResponseParameterProvider : ITokenResponseParameterProvider + { + private readonly ITimeStampManager _manager; + + public int Order => 100; + + public DefaultTokenResponseParameterProvider(ITimeStampManager manager) + { + _manager = manager; + } + + public Task AddParameters(TokenGeneratingContext context, OpenIdConnectMessage response) + { + if (context.IdToken != null) + { + response.IdToken = context.IdToken.SerializedValue; + var expiresIn = _manager.GetDurationInSeconds( + context.IdToken.Token.Expires, + context.IdToken.Token.IssuedAt); + + response.Parameters.Add( + "id_token_expires_in", + expiresIn.ToString(CultureInfo.InvariantCulture)); + } + + if (context.AccessToken != null) + { + response.AccessToken = context.AccessToken.SerializedValue; + response.ExpiresIn = GetExpirationTime(context.AccessToken.Token); + response.Parameters["expires_on"] = context.AccessToken.Token.GetClaimValue(IdentityServiceClaimTypes.Expires); + response.Parameters["not_before"] = context.AccessToken.Token.GetClaimValue(IdentityServiceClaimTypes.NotBefore); + response.Resource = context.RequestGrants.Scopes.First(s => s.ClientId != null).ClientId; + } + + if (context.RefreshToken != null) + { + response.RefreshToken = context.RefreshToken.SerializedValue; + var expiresIn = _manager.GetDurationInSeconds( + context.RefreshToken.Token.Expires, + context.RefreshToken.Token.IssuedAt); + + response.Parameters.Add( + "refresh_token_expires_in", + expiresIn.ToString(CultureInfo.InvariantCulture)); + } + + response.TokenType = "Bearer"; + return Task.CompletedTask; + } + + private static string GetExpirationTime(Token token) + { + if (token.Expires < token.IssuedAt) + { + throw new InvalidOperationException("Can't expire before issuance."); + } + + var expirationTimeInSeconds = Math.Truncate((token.Expires - token.IssuedAt).TotalSeconds); + checked + { + return ((long)expirationTimeInSeconds).ToString(CultureInfo.InvariantCulture); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/FormPostResponseGenerator.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/FormPostResponseGenerator.cs new file mode 100644 index 0000000000..08dc6f0431 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/FormPostResponseGenerator.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class FormPostResponseGenerator + { + private const string FormPostHeaderFormat = @" + + + Please wait while you're being redirected to the identity provider + + +
"; + + private const string FormPostParameterFormat = @" "; + + private const string FormPostFooterFormat = +@" +
+ + +"; + + private readonly HtmlEncoder _encoder; + + public FormPostResponseGenerator(HtmlEncoder encoder) + { + _encoder = encoder; + } + + public async Task GenerateResponseAsync( + HttpContext context, + string redirectUri, + IEnumerable> parameters) + { + using (var stream = new MemoryStream()) + { + using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 1024, leaveOpen: true)) + { + writer.WriteLine(FormPostHeaderFormat, redirectUri); + foreach (var parameter in parameters) + { + writer.WriteLine(FormPostParameterFormat, _encoder.Encode(parameter.Key), _encoder.Encode(parameter.Value)); + } + writer.Write(FormPostFooterFormat); + }; + stream.Seek(0, SeekOrigin.Begin); + + context.Response.ContentType = "text/html; charset=utf-8"; + context.Response.ContentLength = stream.Length; + await stream.CopyToAsync(context.Response.Body); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/FragmentResponseGenerator.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/FragmentResponseGenerator.cs new file mode 100644 index 0000000000..d603289b7c --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/FragmentResponseGenerator.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class FragmentResponseGenerator + { + private readonly UrlEncoder _urlEncoder; + + public FragmentResponseGenerator(UrlEncoder urlEncoder) + { + _urlEncoder = urlEncoder; + } + + public void GenerateResponse( + HttpContext context, + string redirectUri, + IEnumerable> parameters) + { + var builder = new StringBuilder(); + builder.Append(redirectUri); + builder.Append('#'); + + var enumerator = parameters.GetEnumerator(); + while (enumerator.MoveNext()) + { + if (!ShouldSkipKey(enumerator.Current.Key)) + { + builder.Append(_urlEncoder.Encode(enumerator.Current.Key)); + builder.Append('='); + builder.Append(_urlEncoder.Encode(enumerator.Current.Value)); + break; + } + } + + while (enumerator.MoveNext()) + { + if (!ShouldSkipKey(enumerator.Current.Key)) + { + builder.Append('&'); + builder.Append(_urlEncoder.Encode(enumerator.Current.Key)); + builder.Append('='); + builder.Append(_urlEncoder.Encode(enumerator.Current.Value)); + } + } + + context.Response.Redirect(builder.ToString()); + } + + private bool ShouldSkipKey(string key) + { + return string.Equals(key, OpenIdConnectParameterNames.RedirectUri, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/IdentityServiceBuilderExtensions.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/IdentityServiceBuilderExtensions.cs new file mode 100644 index 0000000000..74aa3661c5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/IdentityServiceBuilderExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public static class IdentityServiceBuilderExtensions + { + public static IIdentityServiceBuilder AddSigningCertificate( + this IIdentityServiceBuilder builder, + X509Certificate2 certificate) + { + CryptographyHelpers.ValidateRsaKeyLength(certificate); + var key = new X509SecurityKey(certificate); + builder.Services.Configure( + options => + { + var algorithm = CryptographyHelpers.FindAlgorithm(certificate); + options.SigningKeys.Add(new SigningCredentials(key, algorithm)); + }); + + return builder; + } + + public static IIdentityServiceBuilder AddSigningCertificates( + this IIdentityServiceBuilder builder, + IEnumerable certificates) + { + foreach (var certificate in certificates) + { + builder.AddSigningCertificate(certificate); + } + + return builder; + } + + public static IIdentityServiceBuilder AddSigningCertificates( + this IIdentityServiceBuilder builder, + Func> certificatesLoader) + { + builder.Services.Configure(o => + { + var certificates = certificatesLoader(); + foreach (var certificate in certificates) + { + var algorithm = CryptographyHelpers.FindAlgorithm(certificate); + o.SigningKeys.Add(new SigningCredentials(new X509SecurityKey(certificate), algorithm)); + } + }); + + return builder; + } + + public static IIdentityServiceBuilder AddSigningCertificate(this IIdentityServiceBuilder builder, Func func) + { + var cert = func(); + if (cert == null) + { + return builder; + } + else + { + return builder.AddSigningCertificate(cert); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/JwtAccessTokenIssuer.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/JwtAccessTokenIssuer.cs new file mode 100644 index 0000000000..ba8bfa844c --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/JwtAccessTokenIssuer.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class JwtAccessTokenIssuer : IAccessTokenIssuer + { + private static readonly string[] ClaimsToFilter = new string[] + { + IdentityServiceClaimTypes.TokenUniqueId, + IdentityServiceClaimTypes.ObjectId, + IdentityServiceClaimTypes.Issuer, + IdentityServiceClaimTypes.Audience, + IdentityServiceClaimTypes.IssuedAt, + IdentityServiceClaimTypes.Expires, + IdentityServiceClaimTypes.NotBefore, + }; + + private readonly JwtSecurityTokenHandler _handler; + private readonly IdentityServiceOptions _options; + private readonly ITokenClaimsManager _claimsManager; + private readonly ISigningCredentialsPolicyProvider _credentialsProvider; + + public JwtAccessTokenIssuer( + ITokenClaimsManager claimsManager, + ISigningCredentialsPolicyProvider credentialsProvider, + JwtSecurityTokenHandler handler, + IOptions options) + { + _claimsManager = claimsManager; + _credentialsProvider = credentialsProvider; + _handler = handler; + _options = options.Value; + } + + public async Task IssueAccessTokenAsync(TokenGeneratingContext context) + { + var accessToken = await CreateAccessTokenAsync(context); + var subjectIdentity = CreateSubject(accessToken); + + var descriptor = new SecurityTokenDescriptor(); + + descriptor.Issuer = accessToken.Issuer; + descriptor.Audience = accessToken.Audience; + descriptor.Subject = subjectIdentity; + descriptor.Expires = accessToken.Expires.UtcDateTime; + descriptor.NotBefore = accessToken.NotBefore.UtcDateTime; + + var credentialsDescriptor = await _credentialsProvider.GetSigningCredentialsAsync(); + descriptor.SigningCredentials = credentialsDescriptor.Credentials; + + var token = _handler.CreateJwtSecurityToken(descriptor); + + token.Payload.Remove(IdentityServiceClaimTypes.JwtId); + token.Payload.Remove(IdentityServiceClaimTypes.IssuedAt); + //token.Payload.Add(IdentityServiceClaimTypes.JwtId, accessToken.Id); + + context.AddToken(new TokenResult(accessToken, _handler.WriteToken(token), TokenKinds.Bearer)); + } + + private ClaimsIdentity CreateSubject(AccessToken accessToken) => new ClaimsIdentity(GetFilteredClaims(accessToken)); + + private IEnumerable GetFilteredClaims(AccessToken token) + { + foreach (var claim in token) + { + if (!ClaimsToFilter.Contains(claim.Type)) + { + yield return claim; + } + } + } + + private async Task CreateAccessTokenAsync(TokenGeneratingContext context) + { + await _claimsManager.CreateClaimsAsync(context); + + var claims = context.CurrentClaims; + return new AccessToken(claims); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/JwtIdTokenIssuer.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/JwtIdTokenIssuer.cs new file mode 100644 index 0000000000..f0d1d615f4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/JwtIdTokenIssuer.cs @@ -0,0 +1,109 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class JwtIdTokenIssuer : IIdTokenIssuer + { + private static readonly string[] ClaimsToFilter = new string[] + { + IdentityServiceClaimTypes.TokenUniqueId, + IdentityServiceClaimTypes.Issuer, + IdentityServiceClaimTypes.Audience, + IdentityServiceClaimTypes.IssuedAt, + IdentityServiceClaimTypes.Expires, + IdentityServiceClaimTypes.NotBefore, + IdentityServiceClaimTypes.Nonce, + IdentityServiceClaimTypes.CodeHash, + IdentityServiceClaimTypes.AccessTokenHash, + }; + + private readonly ITokenClaimsManager _claimsManager; + private readonly JwtSecurityTokenHandler _handler; + private readonly IdentityServiceOptions _options; + private readonly ISigningCredentialsPolicyProvider _credentialsProvider; + + public JwtIdTokenIssuer( + ITokenClaimsManager claimsManager, + ISigningCredentialsPolicyProvider credentialsProvider, + JwtSecurityTokenHandler handler, + IOptions options) + { + _claimsManager = claimsManager; + _credentialsProvider = credentialsProvider; + _handler = handler; + _options = options.Value; + } + + public async Task IssueIdTokenAsync(TokenGeneratingContext context) + { + var idToken = await CreateIdTokenAsync(context); + var subjectIdentity = CreateSubject(idToken); + + var descriptor = new SecurityTokenDescriptor(); + + descriptor.Issuer = idToken.Issuer; + descriptor.Audience = idToken.Audience; + descriptor.Subject = subjectIdentity; + descriptor.IssuedAt = idToken.IssuedAt.UtcDateTime; + descriptor.Expires = idToken.Expires.UtcDateTime; + descriptor.NotBefore = idToken.NotBefore.UtcDateTime; + + var credentialsDescriptor = await _credentialsProvider.GetSigningCredentialsAsync(); + descriptor.SigningCredentials = credentialsDescriptor.Credentials; + + var token = _handler.CreateJwtSecurityToken(descriptor); + + token.Payload.Remove(IdentityServiceClaimTypes.JwtId); + //token.Payload.Add(IdentityServiceClaimTypes.JwtId, idToken.Id); + + if (idToken.Nonce != null) + { + token.Payload.AddClaim(new Claim(IdentityServiceClaimTypes.Nonce, idToken.Nonce)); + } + + if (idToken.CodeHash != null) + { + token.Payload.AddClaim(new Claim(IdentityServiceClaimTypes.CodeHash, idToken.CodeHash)); + } + + if (idToken.AccessTokenHash != null) + { + token.Payload.AddClaim(new Claim(IdentityServiceClaimTypes.AccessTokenHash, idToken.AccessTokenHash)); + } + + context.AddToken(new TokenResult(idToken, _handler.WriteToken(token))); + } + + private ClaimsIdentity CreateSubject(IdToken idToken) => + new ClaimsIdentity(GetFilteredClaims(idToken)); + + private IEnumerable GetFilteredClaims(IdToken token) + { + foreach (var claim in token) + { + if (!ClaimsToFilter.Contains(claim.Type)) + { + yield return claim; + } + } + } + + private async Task CreateIdTokenAsync(TokenGeneratingContext context) + { + await _claimsManager.CreateClaimsAsync(context); + + var claims = context.CurrentClaims; + + return new IdToken(claims); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/LogoutRequestFactory.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/LogoutRequestFactory.cs new file mode 100644 index 0000000000..a759c65639 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/LogoutRequestFactory.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class LogoutRequestFactory : ILogoutRequestFactory + { + private readonly IRedirectUriResolver _redirectUriValidator; + private readonly ProtocolErrorProvider _errorProvider; + + public LogoutRequestFactory( + IRedirectUriResolver redirectUriValidator, + ProtocolErrorProvider errorProvider) + { + _redirectUriValidator = redirectUriValidator; + _errorProvider = errorProvider; + } + + public async Task CreateLogoutRequestAsync(IDictionary requestParameters) + { + var (state, stateError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.State, _errorProvider); + if (stateError != null) + { + return LogoutRequest.Invalid(stateError); + } + + var (logoutRedirectUri, redirectUriError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.PostLogoutRedirectUri, _errorProvider); + if (redirectUriError != null) + { + return LogoutRequest.Invalid(redirectUriError); + } + + var (idTokenHint,idTokenHintError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.IdTokenHint, _errorProvider); + if (idTokenHintError != null) + { + return LogoutRequest.Invalid(idTokenHintError); + } + + var redirectUriValidationResult = await _redirectUriValidator.ResolveLogoutUriAsync(null, logoutRedirectUri); + if (!redirectUriValidationResult.IsValid) + { + return LogoutRequest.Invalid(redirectUriValidationResult.Error); + } + + return LogoutRequest.Valid(new OpenIdConnectMessage(requestParameters),redirectUriValidationResult.Uri); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Metadata/DefaultConfigurationMetadataProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Metadata/DefaultConfigurationMetadataProvider.cs new file mode 100644 index 0000000000..7dd0c5b1c0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Metadata/DefaultConfigurationMetadataProvider.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service.Metadata +{ + public class DefaultConfigurationMetadataProvider : IConfigurationMetadataProvider + { + private readonly IOptions _options; + + public DefaultConfigurationMetadataProvider(IOptions options) + { + _options = options; + } + + public int Order { get; } = 1000; + + public Task ConfigureMetadataAsync(OpenIdConnectConfiguration configuration, ConfigurationContext context) + { + configuration.Issuer = _options.Value.Issuer; + configuration.AuthorizationEndpoint = context.AuthorizationEndpoint; + configuration.TokenEndpoint = context.TokenEndpoint; + configuration.JwksUri = context.JwksUriEndpoint; + configuration.EndSessionEndpoint = context.EndSessionEndpoint; + configuration.ResponseModesSupported.Add(OpenIdConnectResponseMode.Query); + configuration.ResponseModesSupported.Add(OpenIdConnectResponseMode.Fragment); + configuration.ResponseModesSupported.Add(OpenIdConnectResponseMode.FormPost); + configuration.ResponseTypesSupported.Add(OpenIdConnectResponseType.Code); + configuration.ResponseTypesSupported.Add(OpenIdConnectResponseType.IdToken); + configuration.ResponseTypesSupported.Add(OpenIdConnectResponseType.CodeIdToken); + configuration.ScopesSupported.Add("openid"); + configuration.SubjectTypesSupported.Add("pairwise"); + configuration.IdTokenSigningAlgValuesSupported.Add("RS256"); + configuration.TokenEndpointAuthMethodsSupported.Add("client_secret_post"); + configuration.ClaimsSupported.Add("oid"); + configuration.ClaimsSupported.Add("sub"); + configuration.ClaimsSupported.Add("name"); + + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Metadata/DefaultKeySetMetadataProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Metadata/DefaultKeySetMetadataProvider.cs new file mode 100644 index 0000000000..b719413c8e --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Metadata/DefaultKeySetMetadataProvider.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service.Metadata +{ + public class DefaultKeySetMetadataProvider : IKeySetMetadataProvider + { + private readonly ISigningCredentialsPolicyProvider _provider; + + public DefaultKeySetMetadataProvider(ISigningCredentialsPolicyProvider provider) + { + _provider = provider; + } + + public async Task GetKeysAsync() + { + var keySet = new JsonWebKeySet(); + var credentials = await _provider.GetAllCredentialsAsync(); + foreach (var key in credentials) + { + keySet.Keys.Add(CreateJsonWebKey(key)); + } + return keySet; + } + + private JsonWebKey CreateJsonWebKey(SigningCredentialsDescriptor descriptor) + { + var jsonWebKey = new JsonWebKey + { + Kid = descriptor.Id, + Use = JsonWebKeyUseNames.Sig, + Kty = descriptor.Algorithm + }; + + if (!descriptor.Algorithm.Equals(JsonWebAlgorithmsKeyTypes.RSA)) + { + throw new NotSupportedException(); + } + if (!descriptor.Metadata.TryGetValue(JsonWebKeyParameterNames.E, out var exponent)) + { + throw new InvalidOperationException($"Missing '{JsonWebKeyParameterNames.E}' from metadata"); + } + if (!descriptor.Metadata.TryGetValue(JsonWebKeyParameterNames.N, out var modulus)) + { + throw new InvalidOperationException($"Missing '{JsonWebKeyParameterNames.N}' from metadata"); + } + + jsonWebKey.E = exponent; + jsonWebKey.N = modulus; + + return jsonWebKey; + } + + private static RSAParameters GetRSAParameters(SigningCredentials credentials) + { + RSA algorithm = null; + var x509SecurityKey = credentials.Key as X509SecurityKey; + if (x509SecurityKey != null) + { + algorithm = x509SecurityKey.PublicKey as RSA; + } + + var rsaSecurityKey = credentials.Key as RsaSecurityKey; + if (rsaSecurityKey != null) + { + algorithm = rsaSecurityKey.Rsa; + + if (algorithm == null) + { + var rsa = RSA.Create(); + rsa.ImportParameters(rsaSecurityKey.Parameters); + algorithm = rsa; + } + } + + var parameters = algorithm.ExportParameters(includePrivateParameters: false); + return parameters; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Microsoft.AspNetCore.Identity.Service.Core.csproj b/src/Microsoft.AspNetCore.Identity.Service.Core/Microsoft.AspNetCore.Identity.Service.Core.csproj new file mode 100644 index 0000000000..c76fee5848 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Microsoft.AspNetCore.Identity.Service.Core.csproj @@ -0,0 +1,25 @@ + + + + + + ASP.NET Core common types implementing the main abstractions for Identity Service. + netcoreapp2.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceAuthorizationOptionsSetup.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceAuthorizationOptionsSetup.cs new file mode 100644 index 0000000000..d32ec2216e --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceAuthorizationOptionsSetup.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceAuthorizationOptionsSetup : IConfigureOptions + { + private readonly IOptions _identityServiceOptions; + + public IdentityServiceAuthorizationOptionsSetup(IOptions identityServiceOptions) + { + _identityServiceOptions = identityServiceOptions; + } + + public void Configure(AuthorizationOptions options) + { + options.AddPolicy(IdentityServiceOptions.LoginPolicyName, _identityServiceOptions.Value.LoginPolicy); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceOptions.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceOptions.cs new file mode 100644 index 0000000000..76a8818445 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceOptions.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceOptions + { + public const string LoginPolicyName = "Microsoft.AspNetCore.Identity.Service.Login"; + public const string SessionPolicyName = "Microsoft.AspNetCore.Identity.Service.Session"; + public const string CookieAuthenticationScheme = "Microsoft.AspNetCore.Identity.Service.Session.Cookies"; + public const string AuthenticationCookieName = "Microsoft.AspNetCore.Identity.Service"; + + public string Issuer { get; set; } + + public AuthorizationPolicy LoginPolicy { get; set; } + public AuthorizationPolicy SessionPolicy { get; set; } + + public IList SigningKeys { get; set; } = new List(); + + public TokenOptions AuthorizationCodeOptions { get; set; } = new TokenOptions(); + + public TokenOptions AccessTokenOptions { get; set; } = new TokenOptions(); + + public TokenOptions RefreshTokenOptions { get; set; } = new TokenOptions(); + + public TokenOptions IdTokenOptions { get; set; } = new TokenOptions(); + + public JsonSerializerSettings SerializationSettings { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceOptionsDefaultSetup.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceOptionsDefaultSetup.cs new file mode 100644 index 0000000000..3af76bcebc --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/IdentityServiceOptionsDefaultSetup.cs @@ -0,0 +1,117 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity.Service.Serialization; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceOptionsDefaultSetup : IConfigureOptions + { + public void Configure(IdentityServiceOptions options) + { + options.LoginPolicy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); + + options.SessionPolicy = new AuthorizationPolicyBuilder() + .AddAuthenticationSchemes(IdentityServiceOptions.CookieAuthenticationScheme) + .RequireAuthenticatedUser() + .Build(); + + options.SerializationSettings = CreateDefault(); + options.SerializationSettings.Converters.Insert(0, new AuthorizationCodeConverter()); + options.SerializationSettings.Converters.Insert(0, new RefreshTokenConverter()); + options.AuthorizationCodeOptions = CreateAuthorizationCodeOptions(TimeSpan.FromMinutes(5), TimeSpan.Zero); + + options.AccessTokenOptions = CreateAccessTokenOptions(TimeSpan.FromHours(2), TimeSpan.Zero); + options.RefreshTokenOptions = CreateRefreshTokenOptions(TimeSpan.FromDays(30), TimeSpan.Zero); + options.IdTokenOptions = CreateIdTokenOptions(TimeSpan.FromHours(2), TimeSpan.Zero); + } + + private static TokenOptions CreateAuthorizationCodeOptions(TimeSpan notValidAfter, TimeSpan notValidBefore) + { + var userClaims = new TokenMapping("user"); + userClaims.AddSingle(IdentityServiceClaimTypes.UserId, ClaimTypes.NameIdentifier); + + var applicationClaims = new TokenMapping("application"); + applicationClaims.AddSingle(IdentityServiceClaimTypes.ClientId); + + return new TokenOptions() + { + UserClaims = userClaims, + ApplicationClaims = applicationClaims, + NotValidAfter = notValidAfter, + NotValidBefore = notValidBefore + }; + } + + private static TokenOptions CreateAccessTokenOptions(TimeSpan notValidAfter, TimeSpan notValidBefore) + { + var userClaims = new TokenMapping("user"); + userClaims.AddSingle(IdentityServiceClaimTypes.Subject, ClaimTypes.NameIdentifier); + + var applicationClaims = new TokenMapping("application"); + + return new TokenOptions() + { + UserClaims = userClaims, + ApplicationClaims = applicationClaims, + NotValidAfter = notValidAfter, + NotValidBefore = notValidBefore + }; + } + + private static TokenOptions CreateRefreshTokenOptions(TimeSpan notValidAfter, TimeSpan notValidBefore) + { + var userClaims = new TokenMapping("user"); + userClaims.AddSingle(IdentityServiceClaimTypes.UserId, ClaimTypes.NameIdentifier); + + var applicationClaims = new TokenMapping("application"); + applicationClaims.AddSingle(IdentityServiceClaimTypes.ClientId, IdentityServiceClaimTypes.ClientId); + + return new TokenOptions() + { + UserClaims = userClaims, + ApplicationClaims = applicationClaims, + NotValidAfter = notValidAfter, + NotValidBefore = notValidBefore + }; + } + + private static TokenOptions CreateIdTokenOptions(TimeSpan notValidAfter, TimeSpan notValidBefore) + { + var userClaims = new TokenMapping("user"); + + var applicationClaims = new TokenMapping("application"); + applicationClaims.AddSingle(IdentityServiceClaimTypes.Audience, IdentityServiceClaimTypes.ClientId); + + return new TokenOptions() + { + UserClaims = userClaims, + ApplicationClaims = applicationClaims, + NotValidAfter = notValidAfter, + NotValidBefore = notValidBefore + }; + } + + private static JsonSerializerSettings CreateDefault() => + new JsonSerializerSettings + { + MissingMemberHandling = MissingMemberHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore, + + // Limit the object graph we'll consume to a fixed depth. This prevents stackoverflow exceptions + // from deserialization errors that might occur from deeply nested objects. + MaxDepth = 32, + + // Do not change this setting + // Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types + TypeNameHandling = TypeNameHandling.None, + }; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenMapping.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenMapping.cs new file mode 100644 index 0000000000..777741119f --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenMapping.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.ObjectModel; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenMapping : Collection + { + public TokenMapping(string source) + { + Source = source; + } + + public string Source { get; } + + public void AddSingle(string claimType, string contextKey) + { + Add(new TokenValueDescriptor(claimType, contextKey, TokenValueCardinality.One)); + } + + public void AddSingle(string name) + { + Add(new TokenValueDescriptor(name, TokenValueCardinality.One)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenOptions.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenOptions.cs new file mode 100644 index 0000000000..80f8081175 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenOptions + { + public TokenMapping UserClaims { get; set; } = new TokenMapping("user"); + public TokenMapping ApplicationClaims { get; set; } = new TokenMapping("application"); + public TokenMapping ContextClaims { get; set; } = new TokenMapping("context"); + public TimeSpan NotValidBefore { get; set; } + public TimeSpan NotValidAfter { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenValueCardinality.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenValueCardinality.cs new file mode 100644 index 0000000000..a3181b5423 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenValueCardinality.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public enum TokenValueCardinality + { + Zero, + One, + Many + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenValueDescriptor.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenValueDescriptor.cs new file mode 100644 index 0000000000..d272e43732 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Options/TokenValueDescriptor.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenValueDescriptor + { + public TokenValueDescriptor(string name, TokenValueCardinality cardinality) + : this(name, name, cardinality) + { + } + + public TokenValueDescriptor(string name, string alias, TokenValueCardinality cardinality) + { + Name = name; + Alias = alias; + Cardinality = cardinality; + } + + public string Name { get; } + public string Alias { get; } + public TokenValueCardinality Cardinality { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/PrincipalExtensions.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/PrincipalExtensions.cs new file mode 100644 index 0000000000..ac3dd011c1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/PrincipalExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace System.Security.Claims +{ + internal static class PrincipalExtensions + { + public static string FindFirstValue(this ClaimsPrincipal principal, string claimType) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + var claim = principal.FindFirst(claimType); + return claim != null ? claim.Value : null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/ProtocolErrorProvider.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/ProtocolErrorProvider.cs new file mode 100644 index 0000000000..a84a225e5a --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/ProtocolErrorProvider.cs @@ -0,0 +1,154 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ProtocolErrorProvider + { + public virtual OpenIdConnectMessage InvalidRedirectUri(string redirectUri) + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The redirect uri '{redirectUri}' is not registered."); + } + + public virtual OpenIdConnectMessage InvalidLogoutRedirectUri(string logoutUri) + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The logout redirect uri '{logoutUri}' is not registered."); + } + + public virtual OpenIdConnectMessage InvalidGrantType(string grantType) + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The grant type '{grantType}' is not a valid grant type."); + } + + public virtual OpenIdConnectMessage InvalidClientId(string clientId) + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The client id '{clientId}' is not associated with any application."); + } + + public virtual OpenIdConnectMessage InvalidAuthorizationCode() + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The authorization code presented is not valid or has expired."); + } + + public virtual OpenIdConnectMessage TooManyParameters(string parameterName) + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The parameter '{parameterName}' must be unique."); + } + + public virtual OpenIdConnectMessage MissingPolicy() + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The 'p' parameter is missing."); + } + + public virtual OpenIdConnectMessage MissingRequiredParameter(string parameterName) + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The request is missing the required parameter {parameterName}."); + } + + public virtual OpenIdConnectMessage InvalidParameterValue(string value, string parameterName) + { + return CreateError( + IdentityServiceErrorCodes.InvalidRequest, + $"{value} is not a valid value for the parameter {parameterName}."); + } + + public virtual OpenIdConnectMessage RequiresLogin() + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The request requires the user to explicitly login."); + } + + public virtual OpenIdConnectMessage MissingOpenIdScope() + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The request is missing the required value 'openid' on the 'scope' parameter."); + } + + public virtual OpenIdConnectMessage ResponseTypeNoneNotAllowed() + { + return CreateError( + IdentityServiceErrorCodes.InvalidRequest, + $"The parameter {OpenIdConnectParameterNames.ResponseType} must not contain any other value when the value 'none' is used."); + } + + public virtual OpenIdConnectMessage InvalidLifetime() + { + return CreateError( + IdentityServiceErrorCodes.InvalidRequest, + $"The token is not yet active or it has expired."); + } + + public virtual OpenIdConnectMessage MultipleResourcesNotSupported(string resourceName, string name) + { + return CreateError( + IdentityServiceErrorCodes.InvalidRequest, + $"Can't get an access token for multiple resources '{resourceName}','{name}'."); + } + + public virtual OpenIdConnectMessage InvalidGrant() + { + return CreateError( + IdentityServiceErrorCodes.InvalidRequest, + "The given grant is not valid."); + } + + public virtual OpenIdConnectMessage InvalidResponseTypeModeCombination(string responseType, string responseMode) + { + return CreateError( + IdentityServiceErrorCodes.InvalidRequest, + $"The value '{responseMode}' for the '{OpenIdConnectParameterNames.ResponseMode}' " + + $"parameter is not compatible with the value '{responseMode}' for the '{OpenIdConnectParameterNames.ResponseType}' parameter."); + } + + public virtual OpenIdConnectMessage InvalidClientCredentials() + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, "Invalid client credentials."); + } + + public virtual OpenIdConnectMessage MismatchedRedirectUrl(string redirectUri) + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The uri '{redirectUri}' must match the uri used during authorization."); + } + + public virtual OpenIdConnectMessage InvalidScope(string scope) + { + return CreateError(IdentityServiceErrorCodes.InvalidRequest, $"The scope '{scope}' is not valid."); + } + + public virtual OpenIdConnectMessage UnauthorizedScope() + { + return CreateError( + IdentityServiceErrorCodes.InvalidRequest, + "One or more scopes on the request are not allowed by the given grant."); + } + + public virtual OpenIdConnectMessage InvalidUriFormat(string redirectUri) + { + return CreateError( + IdentityServiceErrorCodes.InvalidRequest, + $"'{redirectUri}' must be an absolute uri without a fragment"); + } + + public virtual OpenIdConnectMessage InvalidPromptValue(string promptValue) + { + return CreateError( + IdentityServiceErrorCodes.InvalidRequest, + $"The prompt value '{promptValue}' is not valid."); + } + + public virtual OpenIdConnectMessage PromptNoneMustBeTheOnlyValue(string promptValue) + { + return CreateError( + IdentityServiceErrorCodes.InvalidRequest, + $"The prompt value 'none' can't be used in conjunction with other prompt values '{promptValue}'"); + } + + private OpenIdConnectMessage CreateError(string code, string description, string uri = null) => + new OpenIdConnectMessage + { + Error = code, + ErrorDescription = description, + ErrorUri = uri + }; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/QueryResponseGenerator.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/QueryResponseGenerator.cs new file mode 100644 index 0000000000..dc28a50293 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/QueryResponseGenerator.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class QueryResponseGenerator + { + public void GenerateResponse( + HttpContext context, + string redirect, + IEnumerable> parameters) + { + var uri = new Uri(redirect); + + var queryCollection = QueryHelpers.ParseQuery(uri.Query); + var queryBuilder = new QueryBuilder(); + foreach (var kvp in parameters) + { + if (!ShouldSkipKey(kvp.Key)) + { + queryBuilder.Add(kvp.Key, kvp.Value); + } + } + + var queryString = queryBuilder.ToQueryString().ToUriComponent(); + var redirectUri = $"{uri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.Unescaped)}{queryString}"; + context.Response.Redirect(redirectUri); + } + + private bool ShouldSkipKey(string key) + { + return string.Equals(key, OpenIdConnectParameterNames.RedirectUri, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/RefreshTokenIssuer.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/RefreshTokenIssuer.cs new file mode 100644 index 0000000000..ffb8e9ba23 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/RefreshTokenIssuer.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class RefreshTokenIssuer : IRefreshTokenIssuer + { + private static readonly string[] ClaimsToFilter = new string[] + { + IdentityServiceClaimTypes.ObjectId, + IdentityServiceClaimTypes.Issuer, + IdentityServiceClaimTypes.Audience, + IdentityServiceClaimTypes.IssuedAt, + IdentityServiceClaimTypes.Expires, + IdentityServiceClaimTypes.NotBefore, + }; + + private static readonly string[] ClaimsToExclude = new string[] + { + IdentityServiceClaimTypes.JwtId, + IdentityServiceClaimTypes.Issuer, + IdentityServiceClaimTypes.Subject, + IdentityServiceClaimTypes.Audience, + IdentityServiceClaimTypes.Scope, + IdentityServiceClaimTypes.IssuedAt, + IdentityServiceClaimTypes.Expires, + IdentityServiceClaimTypes.NotBefore, + }; + + private readonly ISecureDataFormat _dataFormat; + private readonly ITokenClaimsManager _claimsManager; + + public RefreshTokenIssuer( + ITokenClaimsManager claimsManager, + ISecureDataFormat dataFormat) + { + _claimsManager = claimsManager; + _dataFormat = dataFormat; + } + + public async Task IssueRefreshTokenAsync(TokenGeneratingContext context) + { + var refreshToken = await CreateRefreshTokenAsync(context); + var token = _dataFormat.Protect(refreshToken); + context.AddToken(new TokenResult(refreshToken, token)); + } + + private async Task CreateRefreshTokenAsync(TokenGeneratingContext context) + { + await _claimsManager.CreateClaimsAsync(context); + + var claims = context.CurrentClaims; + + return new RefreshToken(claims); + } + + public Task ExchangeRefreshTokenAsync(OpenIdConnectMessage message) + { + var refreshToken = _dataFormat.Unprotect(message.RefreshToken); + + var resource = refreshToken.Resource; + var scopes = refreshToken.Scopes + .Select(s => ApplicationScope.CanonicalScopes.TryGetValue(s, out var scope) ? scope : new ApplicationScope(resource, s)); + + return Task.FromResult(AuthorizationGrant.Valid( + refreshToken.UserId, + refreshToken.ClientId, + refreshToken.GrantedTokens, + scopes, + refreshToken)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/RequestParametersHelper.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/RequestParametersHelper.cs new file mode 100644 index 0000000000..d98222dfbc --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/RequestParametersHelper.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + internal static class RequestParametersHelper + { + internal static (string value, OpenIdConnectMessage error) ValidateOptionalParameterIsUnique( + IDictionary requestParameters, + string parameterName, ProtocolErrorProvider provider) + { + if (requestParameters.TryGetValue(parameterName, out var currentParameter)) + { + if (currentParameter.Count() != 1) + { + return (null, provider.TooManyParameters(parameterName)); + } + + return (currentParameter.Single(),null); + } + + return (null, null); + } + + internal static (string value, OpenIdConnectMessage error) ValidateParameterIsUnique( + IDictionary requestParameters, + string parameterName, + ProtocolErrorProvider provider) + { + if (requestParameters.TryGetValue(parameterName, out var currentParameter)) + { + if (currentParameter.Length > 1) + { + return (null, provider.TooManyParameters(parameterName)); + } + + var parameterValue = currentParameter.SingleOrDefault(); + if (string.IsNullOrEmpty(parameterValue)) + { + return (null, provider.MissingRequiredParameter(parameterName)); + } + + return (parameterValue,null); + } + else + { + return (null, provider.MissingRequiredParameter(parameterName)); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/AuthorizationCodeConverter.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/AuthorizationCodeConverter.cs new file mode 100644 index 0000000000..d7ed75cf0b --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/AuthorizationCodeConverter.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service.Serialization +{ + internal class AuthorizationCodeConverter : TokenConverter + { + public override AuthorizationCode CreateToken(IEnumerable claims) + { + return new AuthorizationCode(claims); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/RefreshTokenConverter.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/RefreshTokenConverter.cs new file mode 100644 index 0000000000..5afba28787 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/RefreshTokenConverter.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service.Serialization +{ + internal class RefreshTokenConverter : TokenConverter + { + public override RefreshToken CreateToken(IEnumerable claims) + { + return new RefreshToken(claims); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/TokenConverter.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/TokenConverter.cs new file mode 100644 index 0000000000..10108b55b7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/TokenConverter.cs @@ -0,0 +1,144 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Identity.Service.Serialization +{ + internal abstract class TokenConverter : JsonConverter + where TToken : Token + { + public override bool CanConvert(Type objectType) + { + return typeof(TToken).Equals(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (existingValue != null) + { + throw new InvalidOperationException("Can't populate an existing object."); + } + + if (!typeof(TToken).Equals(objectType)) + { + throw new InvalidOperationException($"{objectType.Name} can't be deserialized by this converter."); + } + + if (reader.TokenType != JsonToken.StartObject) + { + throw new InvalidOperationException("Expected an object"); + } + var codeClaims = new List(); + while (reader.Read() && reader.TokenType != JsonToken.EndObject) + { + if (reader.TokenType != JsonToken.PropertyName) + { + throw new InvalidOperationException("Expected a property"); + } + var propertyName = (string)reader.Value; + + if (!reader.Read()) + { + throw new InvalidOperationException("Expected the property content"); + } + + switch (reader.TokenType) + { + case JsonToken.None: + case JsonToken.StartArray: + var value = reader.ReadAsString(); + while (reader.TokenType != JsonToken.EndArray) + { + codeClaims.Add(new Claim(propertyName, value)); + value = reader.ReadAsString(); + } + break; + case JsonToken.Integer: + case JsonToken.Float: + case JsonToken.String: + case JsonToken.Boolean: + case JsonToken.Date: + codeClaims.Add(new Claim(propertyName, (string)reader.Value)); + break; + case JsonToken.EndArray: + break; + case JsonToken.Null: + case JsonToken.Undefined: + break; + case JsonToken.PropertyName: + case JsonToken.Raw: + case JsonToken.Bytes: + case JsonToken.StartObject: + case JsonToken.StartConstructor: + case JsonToken.EndConstructor: + case JsonToken.EndObject: + case JsonToken.Comment: + default: + throw new InvalidOperationException("Invalid token type"); + } + } + if (reader.TokenType == JsonToken.EndObject) + { + return CreateToken(codeClaims); + } + else + { + throw new InvalidOperationException("Failed to read the object"); + } + } + + public abstract TToken CreateToken(IEnumerable claims); + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var code = value as TToken; + var objectType = code?.GetType(); + if (!typeof(TToken).Equals(objectType)) + { + throw new InvalidOperationException($"{objectType.Name} can't be deserialized by this converter."); + } + + var claimsArray = code + .Where(c => c.Value != null) + .OrderBy(c => c.Type, StringComparer.Ordinal).ToArray(); + + writer.WriteStartObject(); + var i = 0; + while (i < claimsArray.Length) + { + var j = i + 1; + while (j < claimsArray.Length) + { + if (!string.Equals(claimsArray[i].Type, claimsArray[j].Type, StringComparison.Ordinal)) + { + break; + } + j++; + } + + writer.WritePropertyName(claimsArray[i].Type); + if (j - i == 1) + { + serializer.Serialize(writer, claimsArray[i].Value); + } + else + { + writer.WriteStartArray(); + for (int k = i; k < j; k++) + { + serializer.Serialize(writer, claimsArray[k].Value); + } + writer.WriteEndArray(); + } + + i = j; + } + writer.WriteEndObject(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/TokenDataSerializer.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/TokenDataSerializer.cs new file mode 100644 index 0000000000..6e0bdd4654 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/Serialization/TokenDataSerializer.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Buffers; +using System.IO; +using System.Text; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Identity.Service.Serialization +{ + public class TokenDataSerializer : IDataSerializer + where TToken : Token + { + private readonly IdentityServiceOptions _options; + private readonly JsonSerializer _serializer; + private readonly IArrayPool _pool; + + public TokenDataSerializer( + IOptions options, + ArrayPool arrayPool) + { + _options = options.Value; + _serializer = JsonSerializer.Create(_options.SerializationSettings); + _pool = new JsonArrayPool(arrayPool); + } + + private class JsonArrayPool : IArrayPool + { + private ArrayPool _pool; + + public JsonArrayPool(ArrayPool pool) + { + _pool = pool; + } + public char[] Rent(int minimumLength) + { + return _pool.Rent(minimumLength); + } + + public void Return(char[] array) + { + _pool.Return(array, clearArray: true); + } + } + + public TToken Deserialize(byte[] data) + { + using (var stream = new MemoryStream(data, writable: false)) + { + using (var streamWriter = new StreamReader(stream, Encoding.UTF8)) + { + using (var writer = new JsonTextReader(streamWriter) { ArrayPool = _pool }) + { + return _serializer.Deserialize(writer); + } + } + } + } + + public byte[] Serialize(TToken model) + { + using (var stream = new MemoryStream()) + { + using (var streamWriter = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + { + using (var writer = new JsonTextWriter(streamWriter) { ArrayPool = _pool }) + { + _serializer.Serialize(writer, model); + } + } + + stream.Seek(0, SeekOrigin.Begin); + return stream.ToArray(); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/TimeStampManager.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/TimeStampManager.cs new file mode 100644 index 0000000000..88dcf915e9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/TimeStampManager.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TimeStampManager : ITimeStampManager + { + public virtual DateTimeOffset GetCurrentTimeStampUtc() => DateTimeOffset.UtcNow; + + public DateTime GetCurrentTimeStampUtcAsDateTime() => GetCurrentTimeStampUtc().UtcDateTime; + + public long GetDurationInSeconds(DateTimeOffset end, DateTimeOffset beginning) + { + var expirationTimeInSeconds = Math.Truncate((end - beginning).TotalSeconds); + checked + { + return (long)expirationTimeInSeconds; + } + } + + public string GetDurationInSecondsAsString(DateTimeOffset end, DateTimeOffset beginning) => + GetDurationInSeconds(end, beginning).ToString(CultureInfo.InvariantCulture); + + public DateTimeOffset GetTimeStampFromEpochTime(string epochTime) => + DateTime.SpecifyKind(EpochTime.DateTime(long.Parse(epochTime, CultureInfo.InvariantCulture)), DateTimeKind.Utc); + + public string GetTimeStampInEpochTime(TimeSpan validityPeriod) => + EpochTime.GetIntDate(GetTimeStampUtcAsDateTime(validityPeriod)).ToString(CultureInfo.InvariantCulture); + + public DateTimeOffset GetTimeStampUtc(TimeSpan validityPeriod) => GetCurrentTimeStampUtc() + validityPeriod; + + public DateTime GetTimeStampUtcAsDateTime(TimeSpan validityPeriod) => GetTimeStampUtc(validityPeriod).UtcDateTime; + + public string GetCurrentTimeStampInEpochTime() => + EpochTime.GetIntDate(GetCurrentTimeStampUtcAsDateTime()).ToString(CultureInfo.InvariantCulture); + + public bool IsValidPeriod(DateTimeOffset start, DateTimeOffset end) => + start <= end && GetCurrentTimeStampUtc() >= start && GetCurrentTimeStampUtc() <= end; + + public bool TimeStampHasExpired(DateTimeOffset timeStamp) => timeStamp > GetCurrentTimeStampUtc(); + + public bool TimeStampHasTakenEffect(DateTimeOffset startTimeStamp) => startTimeStamp < GetCurrentTimeStampUtc(); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/TokenHasher.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/TokenHasher.cs new file mode 100644 index 0000000000..ccf62a737c --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/TokenHasher.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenHasher : ITokenHasher + { + public string HashToken(string token, string hashingAlgorithm) + { + var algorithm = GetAlgorithm(hashingAlgorithm); + + var bytes = Encoding.ASCII.GetBytes(token); + var hashed = algorithm.ComputeHash(bytes); + var result = Base64UrlEncoder.Encode(hashed, 0, hashed.Length / 2); + + return result; + } + + private HashAlgorithm GetAlgorithm(string hashingAlgorithm) + { + switch (hashingAlgorithm) + { + case "RS256": + return CryptographyHelpers.CreateSHA256(); + default: + throw new InvalidOperationException($"Unsupported hashing algorithm '{hashingAlgorithm}'"); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/TokenManager.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/TokenManager.cs new file mode 100644 index 0000000000..6dbd84a058 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/TokenManager.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenManager : ITokenManager + { + private readonly IAccessTokenIssuer _accessTokenIssuer; + private readonly IAuthorizationCodeIssuer _codeIssuer; + private readonly IIdTokenIssuer _idTokenIssuer; + private readonly IRefreshTokenIssuer _refreshTokenIssuer; + private readonly ProtocolErrorProvider _errorProvider; + + public TokenManager( + IAuthorizationCodeIssuer codeIssuer, + IAccessTokenIssuer accessTokenIssuer, + IIdTokenIssuer idTokenIssuer, + IRefreshTokenIssuer refreshTokenIssuer, + ProtocolErrorProvider errorProvider) + { + _codeIssuer = codeIssuer; + _accessTokenIssuer = accessTokenIssuer; + _idTokenIssuer = idTokenIssuer; + _refreshTokenIssuer = refreshTokenIssuer; + _errorProvider = errorProvider; + } + + public async Task IssueTokensAsync(TokenGeneratingContext context) + { + if (context.RequestGrants.Tokens.Contains(TokenTypes.AuthorizationCode)) + { + context.InitializeForToken(TokenTypes.AuthorizationCode); + await _codeIssuer.CreateAuthorizationCodeAsync(context); + } + + if (context.RequestGrants.Tokens.Contains(TokenTypes.AccessToken)) + { + context.InitializeForToken(TokenTypes.AccessToken); + await _accessTokenIssuer.IssueAccessTokenAsync(context); + } + + if (context.RequestGrants.Tokens.Contains(TokenTypes.IdToken)) + { + context.InitializeForToken(TokenTypes.IdToken); + await _idTokenIssuer.IssueIdTokenAsync(context); + } + + if (context.RequestGrants.Tokens.Contains(TokenTypes.RefreshToken)) + { + context.InitializeForToken(TokenTypes.RefreshToken); + await _refreshTokenIssuer.IssueRefreshTokenAsync(context); + } + } + + public async Task ExchangeTokenAsync(OpenIdConnectMessage message) + { + switch (message.GrantType) + { + case OpenIdConnectGrantTypes.AuthorizationCode: + return await _codeIssuer.ExchangeAuthorizationCodeAsync(message); + case OpenIdConnectGrantTypes.RefreshToken: + return await _refreshTokenIssuer.ExchangeRefreshTokenAsync(message); + default: + return AuthorizationGrant.Invalid(_errorProvider.InvalidGrantType(message.GrantType)); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/TokenMapper.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/TokenMapper.cs new file mode 100644 index 0000000000..7be10e174e --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/TokenMapper.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + internal class TokenMapper + { + public TokenMapper() + { + Claims = new List(); + } + + public List Claims { get; } = new List(); + + public void MapFromPrincipal(ClaimsPrincipal user, TokenMapping claimsDefinition) + { + foreach (var mapping in claimsDefinition) + { + var foundClaims = user.FindAll(mapping.Alias); + ValidateCardinality(mapping, foundClaims, claimsDefinition.Source); + foreach (var userClaim in foundClaims) + { + Claims.Add(new Claim(mapping.Name, userClaim.Value)); + } + } + } + + public void MapFromContext(IList context, TokenMapping claimsDefinition) + { + foreach (var mapping in claimsDefinition) + { + var ctxValues = context.Where(c => c.Type == mapping.Alias); + ValidateCardinality(mapping, ctxValues, claimsDefinition.Source); + foreach (var ctxValue in ctxValues) + { + Claims.Add(new Claim(mapping.Name, ctxValue.Value)); + } + } + } + + private static void ValidateCardinality(TokenValueDescriptor mapping, IEnumerable foundClaims, string source) + { + if (mapping.Cardinality != TokenValueCardinality.Zero && !foundClaims.Any()) + { + throw new InvalidOperationException($"Missing '{mapping.Alias}' claim from the {source}."); + } + + if (mapping.Cardinality != TokenValueCardinality.Many && foundClaims.Skip(1).Any()) + { + throw new InvalidOperationException($"Multiple claims found for '{mapping.Alias}' claim from the {source}."); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Core/TokenRequestFactory.cs b/src/Microsoft.AspNetCore.Identity.Service.Core/TokenRequestFactory.cs new file mode 100644 index 0000000000..8223512842 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Core/TokenRequestFactory.cs @@ -0,0 +1,230 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenRequestFactory : ITokenRequestFactory + { + private readonly IClientIdValidator _clientIdValidator; + private readonly ITokenManager _tokenManager; + private readonly IRedirectUriResolver _redirectUriValidator; + private readonly IScopeResolver _scopeResolver; + private readonly ITimeStampManager _timeStampManager; + private readonly IEnumerable _validators; + private readonly ProtocolErrorProvider _errorProvider; + + public TokenRequestFactory( + IClientIdValidator clientIdValidator, + IRedirectUriResolver redirectUriValidator, + IScopeResolver scopeResolver, + IEnumerable validators, + ITokenManager tokenManager, + ITimeStampManager timeStampManager, + ProtocolErrorProvider errorProvider) + { + _clientIdValidator = clientIdValidator; + _redirectUriValidator = redirectUriValidator; + _scopeResolver = scopeResolver; + _validators = validators; + _tokenManager = tokenManager; + _errorProvider = errorProvider; + _timeStampManager = timeStampManager; + } + + public async Task CreateTokenRequestAsync(IDictionary requestParameters) + { + var (clientId, clientIdParameterError) = RequestParametersHelper.ValidateParameterIsUnique(requestParameters, OpenIdConnectParameterNames.ClientId, _errorProvider); + if (clientIdParameterError != null) + { + return TokenRequest.Invalid(clientIdParameterError); + } + + if (!await _clientIdValidator.ValidateClientIdAsync(clientId)) + { + return TokenRequest.Invalid(_errorProvider.InvalidClientId(clientId)); + } + + var (clientSecret, clientSecretParameterError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.ClientSecret, _errorProvider); + if (clientSecretParameterError != null) + { + return TokenRequest.Invalid(clientSecretParameterError); + } + + if (!await _clientIdValidator.ValidateClientCredentialsAsync(clientId, clientSecret)) + { + return TokenRequest.Invalid(_errorProvider.InvalidClientCredentials()); + } + + var (grantType, grantTypeError) = RequestParametersHelper.ValidateParameterIsUnique( + requestParameters, + OpenIdConnectParameterNames.GrantType, + _errorProvider); + + if (grantTypeError != null) + { + return TokenRequest.Invalid(grantTypeError); + } + + var grantTypeParameter = GetGrantTypeParameter(requestParameters, grantType); + if (grantTypeParameter == null) + { + return TokenRequest.Invalid(_errorProvider.InvalidGrantType(grantType)); + } + + var (grantValue, grantValueError) = RequestParametersHelper.ValidateParameterIsUnique( + requestParameters, + grantTypeParameter, + _errorProvider); + + if (grantValueError != null) + { + return TokenRequest.Invalid(clientIdParameterError); + } + + var message = new OpenIdConnectMessage(requestParameters) + { + RequestType = OpenIdConnectRequestType.Token + }; + + // TODO: File a bug to track we might want to redesign this if we want to consider other flows like + // client credentials or resource owner credentials. + var consentGrant = await _tokenManager.ExchangeTokenAsync(message); + if (!consentGrant.IsValid) + { + return TokenRequest.Invalid(consentGrant.Error); + } + + if (!_timeStampManager.IsValidPeriod(consentGrant.Token.NotBefore, consentGrant.Token.Expires)) + { + return TokenRequest.Invalid(_errorProvider.InvalidLifetime()); + } + + if (!string.Equals(clientId, consentGrant.ClientId, StringComparison.Ordinal)) + { + return TokenRequest.Invalid(_errorProvider.InvalidGrant()); + } + + var (scope, requestScopesError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.Scope, _errorProvider); + if (requestScopesError != null) + { + return TokenRequest.Invalid(requestScopesError); + } + + var grantedScopes = consentGrant.GrantedScopes; + + var parsedScope = scope?.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (parsedScope != null) + { + var scopeResolutionResult = await _scopeResolver.ResolveScopesAsync(clientId, parsedScope); + if (!scopeResolutionResult.IsValid) + { + return TokenRequest.Invalid(scopeResolutionResult.Error); + } + + if (grantType.Equals(OpenIdConnectGrantTypes.AuthorizationCode, StringComparison.Ordinal) || + grantType.Equals(OpenIdConnectGrantTypes.RefreshToken, StringComparison.Ordinal)) + { + if (scopeResolutionResult.Scopes.Any(rs => !consentGrant.GrantedScopes.Contains(rs))) + { + return TokenRequest.Invalid(_errorProvider.UnauthorizedScope()); + } + } + + grantedScopes = scopeResolutionResult.Scopes; + } + + if (grantType.Equals(OpenIdConnectGrantTypes.AuthorizationCode, StringComparison.Ordinal)) + { + var authorizationCodeError = await ValidateAuthorizationCode( + requestParameters, + clientId, + consentGrant); + + if (authorizationCodeError != null) + { + return TokenRequest.Invalid(authorizationCodeError); + } + } + + var requestGrants = new RequestGrants + { + Tokens = consentGrant.GrantedTokens.ToList(), + Claims = consentGrant.Token.ToList(), + Scopes = grantedScopes.ToList() + }; + + return await ValidateRequestAsync(TokenRequest.Valid( + message, + consentGrant.UserId, + consentGrant.ClientId, + requestGrants)); + } + + private async Task ValidateRequestAsync(TokenRequest authorizationRequest) + { + foreach (var validator in _validators) + { + var newRequest = await validator.ValidateRequestAsync(authorizationRequest); + if (!newRequest.IsValid) + { + return newRequest; + } + } + + return authorizationRequest; + } + + private async Task ValidateAuthorizationCode( + IDictionary requestParameters, + string clientId, + AuthorizationGrant consentGrant) + { + var (redirectUri, redirectUriError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.RedirectUri, _errorProvider); + if (redirectUriError != null) + { + return redirectUriError; + } + + var tokenRedirectUri = consentGrant + .Token.SingleOrDefault(c => + string.Equals(c.Type, IdentityServiceClaimTypes.RedirectUri, StringComparison.Ordinal))?.Value; + + if (redirectUri == null && tokenRedirectUri != null) + { + return _errorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri); + } + + if (!string.Equals(redirectUri, tokenRedirectUri, StringComparison.Ordinal)) + { + return _errorProvider.MismatchedRedirectUrl(redirectUri); + } + + var resolution = await _redirectUriValidator.ResolveRedirectUriAsync(clientId, redirectUri); + if (!resolution.IsValid) + { + return _errorProvider.InvalidRedirectUri(redirectUri); + } + + return null; + } + + private string GetGrantTypeParameter(IDictionary parameters, string grantType) + { + switch (grantType) + { + case OpenIdConnectGrantTypes.AuthorizationCode: + return OpenIdConnectParameterNames.Code; + case OpenIdConnectGrantTypes.RefreshToken: + return OpenIdConnectParameterNames.RefreshToken; + default: + return null; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/ApplicationStore.cs b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/ApplicationStore.cs new file mode 100644 index 0000000000..1a04850b81 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/ApplicationStore.cs @@ -0,0 +1,684 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore +{ + public class ApplicationStore + : IRedirectUriStore, + IApplicationClaimStore, + IApplicationClientSecretStore, + IApplicationScopeStore, + IQueryableApplicationStore + where TApplication : IdentityServiceApplication + where TScope : IdentityServiceScope, new() + where TApplicationClaim : IdentityServiceApplicationClaim, new() + where TRedirectUri : IdentityServiceRedirectUri, new() + where TContext : DbContext + where TKey : IEquatable + where TUserKey : IEquatable + { + private bool _disposed; + + public ApplicationStore(TContext context) + { + Context = context; + } + + public TContext Context { get; } + + public DbSet ApplicationsSet => Context.Set(); + + public DbSet Scopes => Context.Set(); + + public DbSet ApplicationClaims => Context.Set(); + + public DbSet RedirectUris => Context.Set(); + + public virtual IQueryable Applications => ApplicationsSet; + + public bool AutoSaveChanges { get; set; } = true; + + protected Task SaveChanges(CancellationToken cancellationToken) + { + return AutoSaveChanges ? Context.SaveChangesAsync(cancellationToken) : Task.CompletedTask; + } + + public async Task CreateAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + Context.Add(application); + await SaveChanges(cancellationToken); + return IdentityServiceResult.Success; + } + + public async Task UpdateAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + Context.Attach(application); + application.ConcurrencyStamp = Guid.NewGuid().ToString(); + Context.Update(application); + try + { + await SaveChanges(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return IdentityServiceResult.Failed(new IdentityServiceError() { Description = "Concurrency failure" }); + } + return IdentityServiceResult.Success; + } + + public async Task DeleteAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + Context.Remove(application); + try + { + await SaveChanges(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return IdentityServiceResult.Failed(new IdentityServiceError() { Description = "Concurrency failure" }); + } + return IdentityServiceResult.Success; + } + + public void Dispose() + { + _disposed = true; + } + + public Task FindByClientIdAsync(string clientId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + return Applications.SingleOrDefaultAsync(a => a.ClientId == clientId, cancellationToken); + } + + public Task FindByNameAsync(string name, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + return Applications.SingleOrDefaultAsync(a => a.Name == name, cancellationToken); + } + + public async Task> FindByUserIdAsync(string userId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var id = ConvertUserIdFromString(userId); + return await Applications.Where(a => a.UserId.Equals(id)).ToListAsync(); + } + + public Task FindByIdAsync(string applicationId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var id = ConvertUserIdFromString(applicationId); + return ApplicationsSet.FindAsync(new object[] { id }, cancellationToken); + } + + public Task GetApplicationIdAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var id = ConvertApplicationIdToString(application.Id); + return Task.FromResult(id); + } + + public Task GetApplicationUserIdAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var id = ConvertUserIdToString(application.UserId); + return Task.FromResult(id); + } + + public Task GetApplicationClientIdAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return Task.FromResult(application.ClientId); + } + + public Task GetApplicationNameAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return Task.FromResult(application.Name); + } + + public async Task> FindRegisteredUrisAsync( + TApplication app, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + var redirectUris = await RedirectUris + .Where(ru => ru.ApplicationId.Equals(app.Id) && !ru.IsLogout) + .Select(ru => ru.Value) + .ToListAsync(cancellationToken); + + return redirectUris; + } + + public async Task RegisterRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (redirectUri == null) + { + throw new ArgumentNullException(nameof(redirectUri)); + } + + var existingRedirectUri = await RedirectUris.SingleOrDefaultAsync( + ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(redirectUri) && !ru.IsLogout); + if (existingRedirectUri != null) + { + return IdentityServiceResult.Failed( + new IdentityServiceError { Description = "A route with the same value already exists." }); + } + + RedirectUris.Add(CreateRedirectUri(app, redirectUri, isLogout: false)); + + return IdentityServiceResult.Success; + } + + private TRedirectUri CreateRedirectUri(TApplication app, string redirectUri, bool isLogout) + { + var registration = new TRedirectUri + { + ApplicationId = app.Id, + IsLogout = isLogout, + Value = redirectUri + }; + + return registration; + } + + public async Task UnregisterRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (redirectUri == null) + { + throw new ArgumentNullException(nameof(redirectUri)); + } + + var registeredUri = await RedirectUris + .SingleOrDefaultAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(redirectUri) && !ru.IsLogout); + if (registeredUri == null) + { + return IdentityServiceResult.Failed( + new IdentityServiceError { Description = "We were unable to find the redirect uri to unregister." }); + } + + RedirectUris.Remove(registeredUri); + + return IdentityServiceResult.Success; + } + + public async Task UpdateRedirectUriAsync( + TApplication app, + string oldRedirectUri, + string newRedirectUri, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (oldRedirectUri == null) + { + throw new ArgumentNullException(nameof(oldRedirectUri)); + } + + if (newRedirectUri == null) + { + throw new ArgumentNullException(nameof(newRedirectUri)); + } + + var existingRedirectUri = await RedirectUris + .SingleOrDefaultAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(oldRedirectUri) && !ru.IsLogout); + + if (existingRedirectUri == null) + { + return IdentityServiceResult.Failed( + new IdentityServiceError { Description = "We were unable to find the registered redirect uri to update." }); + } + + existingRedirectUri.Value = newRedirectUri; + + return IdentityServiceResult.Success; + } + + public async Task> FindRegisteredLogoutUrisAsync(TApplication app, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + var redirectUris = await RedirectUris + .Where(ru => ru.ApplicationId.Equals(app.Id) && ru.IsLogout) + .Select(ru => ru.Value) + .ToListAsync(cancellationToken); + + return redirectUris; + } + + public async Task RegisterLogoutRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (redirectUri == null) + { + throw new ArgumentNullException(nameof(redirectUri)); + } + + var existingRedirectUri = await RedirectUris.SingleOrDefaultAsync( + ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(redirectUri) && ru.IsLogout); + if (existingRedirectUri != null) + { + return IdentityServiceResult.Failed( + new IdentityServiceError { Description = "A route with the same value already exists." }); + } + + RedirectUris.Add(CreateRedirectUri(app, redirectUri, isLogout: true)); + + return IdentityServiceResult.Success; + } + + public async Task UnregisterLogoutRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (redirectUri == null) + { + throw new ArgumentNullException(nameof(redirectUri)); + } + + var registeredUri = await RedirectUris + .SingleOrDefaultAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(redirectUri) && ru.IsLogout); + if (registeredUri == null) + { + return IdentityServiceResult.Failed( + new IdentityServiceError { Description = "We were unable to find the redirect uri to unregister." }); + } + + RedirectUris.Remove(registeredUri); + + return IdentityServiceResult.Success; + } + + public async Task UpdateLogoutRedirectUriAsync(TApplication app, string oldRedirectUri, string newRedirectUri, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (oldRedirectUri == null) + { + throw new ArgumentNullException(nameof(oldRedirectUri)); + } + + if (newRedirectUri == null) + { + throw new ArgumentNullException(nameof(newRedirectUri)); + } + + var existingRedirectUri = await RedirectUris + .SingleOrDefaultAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(oldRedirectUri) && ru.IsLogout); + + if (existingRedirectUri == null) + { + return IdentityServiceResult.Failed( + new IdentityServiceError { Description = "We were unable to find the registered redirect uri to update." }); + } + + existingRedirectUri.Value = newRedirectUri; + + return IdentityServiceResult.Success; + } + + protected void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + public virtual string ConvertApplicationIdToString(TKey id) + { + if (Equals(id, default(TKey))) + { + return null; + } + return id.ToString(); + } + + public virtual string ConvertUserIdToString(TUserKey id) + { + if (Equals(id, default(TUserKey))) + { + return null; + } + return id.ToString(); + } + + public virtual TKey ConvertApplicationIdFromString(string id) + { + if (id == null) + { + return default(TKey); + } + return (TKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id); + } + + public virtual TUserKey ConvertUserIdFromString(string id) + { + if (id == null) + { + return default(TUserKey); + } + return (TUserKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id); + } + + public Task SetClientSecretHashAsync(TApplication application, string clientSecretHash, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ClientSecretHash = clientSecretHash; + return Task.CompletedTask; + } + + public Task GetClientSecretHashAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return Task.FromResult(application.ClientSecretHash); + } + + public Task HasClientSecretAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return Task.FromResult(application.ClientSecretHash != null); + } + + public async Task> FindScopesAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + var scopes = await Scopes + .Where(s => s.ApplicationId.Equals(application.Id)) + .Select(s => s.Value) + .ToListAsync(cancellationToken); + + return scopes; + } + + public async Task AddScopeAsync(TApplication application, string scope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + var existingScope = await Scopes.SingleOrDefaultAsync( + ru => ru.ApplicationId.Equals(application.Id) && ru.Value.Equals(scope)); + if (existingScope != null) + { + return IdentityServiceResult.Failed( + new IdentityServiceError { Description = "A scope with the same value already exists." }); + } + + Scopes.Add(CreateScope(application, scope)); + + return IdentityServiceResult.Success; + } + + private TScope CreateScope(TApplication application, string scope) + { + return new TScope + { + ApplicationId = application.Id, + Value = scope + }; + } + + public async Task UpdateScopeAsync(TApplication application, string oldScope, string newScope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (oldScope == null) + { + throw new ArgumentNullException(nameof(oldScope)); + } + + if (newScope == null) + { + throw new ArgumentNullException(nameof(newScope)); + } + + var existingScope = await Scopes + .SingleOrDefaultAsync(s => s.ApplicationId.Equals(application.Id) && s.Value.Equals(oldScope)); + if (existingScope == null) + { + return IdentityServiceResult.Failed( + new IdentityServiceError { Description = "We were unable to find the scope to update." }); + } + + existingScope.Value = newScope; + + return IdentityServiceResult.Success; + } + + public async Task RemoveScopeAsync(TApplication application, string scope, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + var existingScope = await Scopes + .SingleOrDefaultAsync(ru => ru.ApplicationId.Equals(application.Id) && ru.Value.Equals(scope)); + if (existingScope == null) + { + return IdentityServiceResult.Failed( + new IdentityServiceError { Description = "We were unable to find the scope to remove." }); + } + + Scopes.Remove(existingScope); + + return IdentityServiceResult.Success; + } + + public async Task> GetClaimsAsync(TApplication application, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return await ApplicationClaims.Where(ac => ac.ApplicationId.Equals(application.Id)).Select(c => c.ToClaim()).ToListAsync(cancellationToken); + } + + public Task AddClaimsAsync(TApplication application, IEnumerable claims, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + if (claims == null) + { + throw new ArgumentNullException(nameof(claims)); + } + foreach (var claim in claims) + { + ApplicationClaims.Add(CreateApplicationClaim(application, claim)); + } + return Task.CompletedTask; + } + + private TApplicationClaim CreateApplicationClaim(TApplication application, Claim claim) => + new TApplicationClaim + { + ApplicationId = application.Id, + ClaimType = claim.Type, + ClaimValue = claim.Value + }; + + public async Task ReplaceClaimAsync(TApplication application, Claim oldClaim, Claim newClaim, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + if (oldClaim == null) + { + throw new ArgumentNullException(nameof(oldClaim)); + } + if (newClaim == null) + { + throw new ArgumentNullException(nameof(newClaim)); + } + + var matchedClaims = await ApplicationClaims.Where(ac => ac.ApplicationId.Equals(application.Id) && ac.ClaimValue == oldClaim.Value && ac.ClaimType == oldClaim.Type).ToListAsync(cancellationToken); + foreach (var matchedClaim in matchedClaims) + { + matchedClaim.ClaimValue = newClaim.Value; + matchedClaim.ClaimType = newClaim.Type; + } + } + + public async Task RemoveClaimsAsync(TApplication application, IEnumerable claims, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + if (claims == null) + { + throw new ArgumentNullException(nameof(claims)); + } + foreach (var claim in claims) + { + var matchedClaims = await ApplicationClaims.Where(ac => ac.ApplicationId.Equals(application.Id) && ac.ClaimValue == claim.Value && ac.ClaimType == claim.Type).ToListAsync(cancellationToken); + foreach (var c in matchedClaims) + { + ApplicationClaims.Remove(c); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplication.cs b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplication.cs new file mode 100644 index 0000000000..ebd572881f --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplication.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceApplication : IdentityServiceApplication + { + } + + public class IdentityServiceApplication : + IdentityServiceApplication + where TUserKey : IEquatable + { + } + + public class IdentityServiceApplication : + IdentityServiceApplication< + TApplicationKey, + TUserKey, + IdentityServiceScope, + IdentityServiceApplicationClaim, + IdentityServiceRedirectUri> + where TApplicationKey : IEquatable + where TUserKey : IEquatable + + { + } + + public class IdentityServiceApplication + where TKey : IEquatable + where TUserKey : IEquatable + where TScope : IdentityServiceScope + where TApplicationClaim : IdentityServiceApplicationClaim + where TRedirectUri : IdentityServiceRedirectUri + { + public TKey Id { get; set; } + public string Name { get; set; } + public TUserKey UserId { get; set; } + public string ClientId { get; set; } + public string ClientSecretHash { get; set; } + public string ConcurrencyStamp { get; set; } + public ICollection Scopes { get; set; } + public ICollection Claims { get; set; } + public ICollection RedirectUris { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplicationClaim.cs b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplicationClaim.cs new file mode 100644 index 0000000000..fd971de0f0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplicationClaim.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceApplicationClaim : IdentityServiceApplicationClaim + { + } + + public class IdentityServiceApplicationClaim where TApplicationKey : IEquatable + { + public int Id { get; set; } + public TApplicationKey ApplicationId { get; set; } + public string ClaimType { get; set; } + public string ClaimValue { get; set; } + + public Claim ToClaim() => new Claim(ClaimType, ClaimValue); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceBuilderExtensions.cs b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceBuilderExtensions.cs new file mode 100644 index 0000000000..c0d5c91e48 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceBuilderExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore +{ + public static class IdentityServiceBuilderExtensions + { + public static IIdentityServiceBuilder AddEntityFrameworkStores(this IIdentityServiceBuilder builder) + { + var services = builder.Services; + var applicationType = FindGenericBaseType(builder.ApplicationType, typeof(IdentityServiceApplication<,,,,>)); + var userType = FindGenericBaseType(builder.UserType, typeof(IdentityUser<>)); + + services.AddTransient( + typeof(IApplicationStore<>).MakeGenericType(builder.ApplicationType), + typeof(ApplicationStore<,,,,,,>).MakeGenericType( + builder.ApplicationType, + applicationType.GenericTypeArguments[2], + applicationType.GenericTypeArguments[3], + applicationType.GenericTypeArguments[4], + typeof(TContext), + applicationType.GenericTypeArguments[0], + userType.GenericTypeArguments[0])); + + return builder; + } + + private static TypeInfo FindGenericBaseType(Type currentType, Type genericBaseType) + { + var type = currentType.GetTypeInfo(); + while (type.BaseType != null) + { + type = type.BaseType.GetTypeInfo(); + var genericType = type.IsGenericType ? type.GetGenericTypeDefinition() : null; + if (genericType != null && genericType == genericBaseType) + { + return type; + } + } + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceDbContext.cs b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceDbContext.cs new file mode 100644 index 0000000000..4c5d20d166 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceDbContext.cs @@ -0,0 +1,165 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore +{ + public abstract class IdentityServiceDbContext : + IdentityServiceDbContext + where TUser : IdentityUser + where TApplication : IdentityServiceApplication + { + public IdentityServiceDbContext(DbContextOptions options) + : base(options) + { + } + } + + public abstract class IdentityServiceDbContext + : IdentityServiceDbContext< + TUser, + TRole, + TUserKey, + IdentityUserClaim, + IdentityUserRole, + IdentityUserLogin, + IdentityRoleClaim, + IdentityUserToken, + TApplication, + IdentityServiceScope, + IdentityServiceApplicationClaim, + IdentityServiceRedirectUri, + TApplicationKey> + where TUser : IdentityUser + where TRole : IdentityRole + where TUserKey : IEquatable + where TApplication : IdentityServiceApplication + where TApplicationKey : IEquatable + { + public IdentityServiceDbContext(DbContextOptions options) + : base(options) + { + } + } + + public abstract class IdentityServiceDbContext< + TUser, + TRole, + TUserKey, + TUserClaim, + TUserRole, + TUserLogin, + TRoleClaim, + TUserToken, + TApplication, + TScope, + TApplicationClaim, + TRedirectUri, + TApplicationKey> : + IdentityDbContext + where TUser : IdentityUser + where TRole : IdentityRole + where TUserKey : IEquatable + where TUserClaim : IdentityUserClaim + where TUserRole : IdentityUserRole + where TUserLogin : IdentityUserLogin + where TRoleClaim : IdentityRoleClaim + where TUserToken : IdentityUserToken + where TApplication : IdentityServiceApplication + where TScope : IdentityServiceScope + where TApplicationClaim : IdentityServiceApplicationClaim + where TRedirectUri : IdentityServiceRedirectUri + where TApplicationKey : IEquatable + { + public IdentityServiceDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity(b => + { + b.ToTable("AspNetApplications"); + b.HasKey(a => a.Id); + + b.Property(a => a.Name) + .HasMaxLength(256) + .IsRequired(); + b.HasIndex(a => a.Name) + .HasName("NameIndex") + .IsUnique(); + + b.Property(a => a.ClientId) + .HasMaxLength(256) + .IsRequired(); + b.HasIndex(a => a.ClientId) + .HasName("ClientIdIndex") + .IsUnique(); + + b.HasOne() + .WithMany() + .HasForeignKey(a => a.UserId) + .IsRequired(false); + + b.Property(a => a.ConcurrencyStamp) + .IsConcurrencyToken(); + + b.HasMany(a => a.RedirectUris) + .WithOne() + .HasForeignKey(fk => fk.ApplicationId) + .IsRequired(); + + b.HasMany(a => a.Scopes) + .WithOne() + .HasForeignKey(fk => fk.ApplicationId) + .IsRequired(); + + b.HasMany(a => a.Claims) + .WithOne() + .HasForeignKey(fk => fk.ApplicationId) + .IsRequired(); + }); + + builder.Entity(b => + { + b.ToTable("AspNetRedirectUris"); + b.HasKey(a => a.Id); + b.Property(ru => ru.Value) + .HasMaxLength(256) + .IsRequired(); + }); + + builder.Entity(b => + { + b.ToTable("AspNetScopes"); + b.HasKey(s => s.Id); + b.Property(s => s.Value) + .HasMaxLength(256) + .IsRequired(); + }); + + builder.Entity(b => + { + b.ToTable("AspNetApplicationClaims"); + b.HasKey(s => s.Id); + b.Property(s => s.ClaimType) + .HasMaxLength(256) + .IsRequired(); + b.Property(s => s.ClaimValue) + .HasMaxLength(256) + .IsRequired(); + }); + } + + public DbSet Applications { get; set; } + public DbSet RedirectUris { get; set; } + public DbSet Scopes { get; set; } + public DbSet ApplicationClaims { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceRedirectUri.cs b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceRedirectUri.cs new file mode 100644 index 0000000000..03e4f4d058 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceRedirectUri.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceRedirectUri + { + public string Id { get; set; } + public TApplicationKey ApplicationId { get; set; } + public bool IsLogout { get; set; } + public string Value { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceScope.cs b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceScope.cs new file mode 100644 index 0000000000..547de5ed7a --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceScope.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceScope + { + public string Id { get; set; } + public TApplicationKey ApplicationId { get; set; } + public string Value { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.csproj b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.csproj new file mode 100644 index 0000000000..c15fdb42f2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.csproj @@ -0,0 +1,21 @@ + + + + + + ASP.NET Core Identity Service implementation based on ASP.NET Core Identity. + netcoreapp2.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/EnableIntegratedWebClientAttribute.cs b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/EnableIntegratedWebClientAttribute.cs new file mode 100644 index 0000000000..8da9107fcb --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/EnableIntegratedWebClientAttribute.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Identity.Service.IntegratedWebClient +{ + public class EnableIntegratedWebClientAttribute : Attribute + { + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebApplicationRedirectFilter.cs b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebApplicationRedirectFilter.cs new file mode 100644 index 0000000000..f24c769937 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebApplicationRedirectFilter.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity.Service.IntegratedWebClient +{ + public class IntegratedWebClientRedirectFilter : IActionFilter + { + private IOptions _webClientOptions; + private IEnumerable _parameters; + + public IntegratedWebClientRedirectFilter( + IOptions webClientOptions, + IEnumerable parameters) + { + _webClientOptions = webClientOptions; + _parameters = parameters; + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } + + public void OnActionExecuting(ActionExecutingContext context) + { + foreach (var parameter in _parameters) + { + if (context.ActionArguments.TryGetValue(parameter, out var parameterValue)) + { + switch (parameterValue) + { + case AuthorizationRequest authorization when IsAuthorizationForWebApplication(authorization): + authorization.RequestGrants.RedirectUri = ComputeRedirectUri(isLogout: false); + break; + case LogoutRequest logout when IsLogoutForWebApplication(logout): + logout.LogoutRedirectUri = ComputeRedirectUri(isLogout: true); + break; + default: + break; + } + } + } + + bool IsLogoutForWebApplication(LogoutRequest logout) => + logout.IsValid && logout.LogoutRedirectUri != null && + logout.LogoutRedirectUri.Equals(_webClientOptions.Value.TokenRedirectUrn); + + bool IsAuthorizationForWebApplication(AuthorizationRequest request) => + _webClientOptions.Value.ClientId.Equals(request.Message?.ClientId) && + request.RequestGrants.RedirectUri != null && + request.RequestGrants.RedirectUri.Equals(_webClientOptions.Value.TokenRedirectUrn); + + string ComputeRedirectUri(bool isLogout) + { + var request = context.HttpContext.Request; + var openIdConnectOptions = context.HttpContext.RequestServices.GetRequiredService>(); + var scheme = request.Scheme; + var host = request.Host.ToUriComponent(); + var pathBase = request.PathBase; + var options = openIdConnectOptions.Get(OpenIdConnectDefaults.AuthenticationScheme); + var path = !isLogout ? options.CallbackPath : options.SignedOutCallbackPath; + return $"{scheme}://{host}{pathBase}{path}"; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientConfigurationManager.cs b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientConfigurationManager.cs new file mode 100644 index 0000000000..7d595705e1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientConfigurationManager.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service.IntegratedWebClient +{ + public class WebApplicationConfiguration : IConfigurationManager + { + private readonly IHttpContextAccessor _accessor; + private readonly IntegratedWebClientOptions _integratedWebClientOptions; + + private OpenIdConnectConfiguration _configuration; + + public WebApplicationConfiguration(IntegratedWebClientOptions options, IHttpContextAccessor accessor) + { + _integratedWebClientOptions = options; + _accessor = accessor; + } + + public async Task GetConfigurationAsync(CancellationToken cancel) + { + if (_configuration == null) + { + _configuration = await CreateConfigurationAsync(); + } + + return _configuration; + } + + private async Task CreateConfigurationAsync() + { + var ctx = _accessor.HttpContext; + var manager = ctx.RequestServices.GetRequiredService(); + + var configurationContext = new ConfigurationContext(); + var baseUrl = $"{ctx.Request.Scheme}://{ctx.Request.Host.ToUriComponent()}{ctx.Request.PathBase}"; + if (!Uri.TryCreate(configurationContext.AuthorizationEndpoint, UriKind.RelativeOrAbsolute, out var authorizationUri)) + { + configurationContext.AuthorizationEndpoint = $"{baseUrl}/tfp/IdentityService/signinsignup/oauth2/v2.0/authorize"; + } + else + { + configurationContext.AuthorizationEndpoint = MakeAbsolute(authorizationUri); + } + + if (!Uri.TryCreate(configurationContext.AuthorizationEndpoint, UriKind.RelativeOrAbsolute, out var tokenUri)) + { + configurationContext.TokenEndpoint = $"{baseUrl}/tfp/IdentityService/signinsignup/oauth2/v2.0/token"; + } + else + { + configurationContext.TokenEndpoint = MakeAbsolute(tokenUri); + } + + if (!Uri.TryCreate(configurationContext.EndSessionEndpoint, UriKind.RelativeOrAbsolute, out var logoutUri)) + { + configurationContext.EndSessionEndpoint = $"{baseUrl}/tfp/IdentityService/signinsignup/oauth2/v2.0/logout"; + } + else + { + configurationContext.EndSessionEndpoint = MakeAbsolute(logoutUri); + } + + configurationContext.Id = "WebApplicationClient"; + + var configuration = await manager.GetConfigurationAsync(configurationContext); + return configuration; + string MakeAbsolute(Uri relativeOrAbsoluteUri) + { + if (relativeOrAbsoluteUri.IsAbsoluteUri) + { + return relativeOrAbsoluteUri.ToString(); + } + else + { + return baseUrl + relativeOrAbsoluteUri.ToString(); + } + } + } + + public void RequestRefresh() + { + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientModelConvention.cs b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientModelConvention.cs new file mode 100644 index 0000000000..307c668c29 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientModelConvention.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity.Service.IntegratedWebClient +{ + public class IntegratedWebClientModelConvention : IApplicationModelConvention + { + private readonly IOptions _webClientOptions; + + public IntegratedWebClientModelConvention(IOptions webClientOptions) + { + _webClientOptions = webClientOptions; + } + + public void Apply(ApplicationModel application) + { + foreach (var controller in application.Controllers) + { + foreach (var action in controller.Actions) + { + Apply(action); + } + } + } + + private void Apply(ActionModel action) + { + var parameters = action.Parameters.Where(p => p.Attributes.OfType().Any()); + if (parameters.Any()) + { + action.Filters.Add(new IntegratedWebClientRedirectFilter( + _webClientOptions, + parameters.Select(p => p.ParameterName))); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientOpenIdConnectOptionsSetup.cs b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientOpenIdConnectOptionsSetup.cs new file mode 100644 index 0000000000..1591dc4377 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientOpenIdConnectOptionsSetup.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity.Service.IntegratedWebClient +{ + public class IntegratedWebClientOpenIdConnectOptionsSetup : IConfigureOptions + { + private readonly IHttpContextAccessor _accessor; + private readonly IKeySetMetadataProvider _keysProvider; + private readonly IOptions _webApplicationOptions; + + public IntegratedWebClientOpenIdConnectOptionsSetup( + IOptions webApplicationOptions, + IHttpContextAccessor accessor, + IKeySetMetadataProvider keysProvider) + { + _webApplicationOptions = webApplicationOptions; + _accessor = accessor; + _keysProvider = keysProvider; + } + + public void Configure(OpenIdConnectOptions options) + { + options.TokenValidationParameters.NameClaimType = "name"; + options.SignInScheme = _webApplicationOptions.Value.CookieSignInScheme; + options.ClientId = _webApplicationOptions.Value.ClientId; + + if (!string.IsNullOrEmpty(_webApplicationOptions.Value.TokenRedirectUrn)) + { + options.DisplayName = null; + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProvider = (ctx) => + { + ctx.ProtocolMessage.RedirectUri = _webApplicationOptions.Value.TokenRedirectUrn; + return Task.CompletedTask; + }, + OnRedirectToIdentityProviderForSignOut = (ctx) => + { + + ctx.ProtocolMessage.PostLogoutRedirectUri = _webApplicationOptions.Value.TokenRedirectUrn; + return Task.CompletedTask; + }, + OnAuthorizationCodeReceived = (ctx) => + { + ctx.ProtocolMessage.RedirectUri = _webApplicationOptions.Value.TokenRedirectUrn; + return Task.CompletedTask; + } + }; + + var keys = _keysProvider.GetKeysAsync().GetAwaiter().GetResult().Keys; + + options.ConfigurationManager = new WebApplicationConfiguration(_webApplicationOptions.Value, _accessor); + options.TokenValidationParameters.IssuerSigningKeys = keys; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientOptions.cs b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientOptions.cs new file mode 100644 index 0000000000..01c3ff0e43 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication.Cookies; + +namespace Microsoft.AspNetCore.Identity.Service.IntegratedWebClient +{ + public class IntegratedWebClientOptions + { + public string ClientId { get; set; } + public string TokenRedirectUrn { get; set; } + public string MetadataUri { get; set; } + public string AuthorizationEndpoint { get; set; } + public string TokenEndpoint { get; set; } + public string EndsSessionEndpoint { get; set; } + public string CookieSignInScheme { get; set; } = CookieAuthenticationDefaults.AuthenticationScheme; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientServiceCollectionExtensions.cs new file mode 100644 index 0000000000..0c00cfef20 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebClientServiceCollectionExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity.Service.IntegratedWebClient +{ + public static class IntegratedWebClientServiceCollectionExtensions + { + public static IServiceCollection AddIntegratedWebClient( + this IServiceCollection services, + Action action) + { + services.Configure(action); + services.TryAddEnumerable(ServiceDescriptor.Scoped, IntegratedWebClientOpenIdConnectOptionsSetup>()); + services.TryAddEnumerable(ServiceDescriptor.Singleton, IntegratedWebclientMvcOptionsSetup>()); + + return services; + } + + public static IServiceCollection AddIntegratedWebClient( + this IServiceCollection services, + IConfiguration configuration) + { + services.AddIntegratedWebClient(options => configuration.Bind(options)); + return services; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebclientMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebclientMvcOptionsSetup.cs new file mode 100644 index 0000000000..f711b5ca09 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/IntegratedWebclientMvcOptionsSetup.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity.Service.IntegratedWebClient +{ + public class IntegratedWebclientMvcOptionsSetup : IConfigureOptions + { + private readonly IOptions _webClientOptions; + + public IntegratedWebclientMvcOptionsSetup(IOptions webClientOptions) + { + _webClientOptions = webClientOptions; + } + + public void Configure(MvcOptions options) + { + if (!string.IsNullOrEmpty(_webClientOptions.Value.TokenRedirectUrn)) + { + options.Conventions.Add(new IntegratedWebClientModelConvention(_webClientOptions)); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient.csproj b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient.csproj new file mode 100644 index 0000000000..829926bd63 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient/Microsoft.AspNetCore.Identity.Service.IntegratedWebClient.csproj @@ -0,0 +1,23 @@ + + + + + + ASP.NET Core Identity Service in process client. + netcoreapp2.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Identity.Service.Mvc/AuthorizationRequestModelBinder.cs b/src/Microsoft.AspNetCore.Identity.Service.Mvc/AuthorizationRequestModelBinder.cs new file mode 100644 index 0000000000..bb4d3bb08c --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Mvc/AuthorizationRequestModelBinder.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Identity.Service.Mvc +{ + public class AuthorizationRequestModelBinder : IModelBinder + { + public async Task BindModelAsync(ModelBindingContext bindingContext) + { + if (!bindingContext.IsTopLevelObject) + { + return; + } + + if (bindingContext.ModelType.Equals(typeof(AuthorizationRequest))) + { + var httpContext = bindingContext.HttpContext; + var httpRequest = httpContext.Request; + + IEnumerable> source = null; + if (httpRequest.Method.Equals("GET")) + { + source = httpRequest.Query; + } + else if (httpRequest.Method.Equals("POST")) + { + source = await httpRequest.ReadFormAsync(); + } + + if (source == null) + { + bindingContext.Result = ModelBindingResult.Failed(); + } + else + { + var requestParameters = source.ToDictionary( + kvp => kvp.Key, + kvp => (string[])kvp.Value); + + var factory = httpContext.RequestServices.GetRequiredService(); + bindingContext.Result = ModelBindingResult.Success(await factory.CreateAuthorizationRequestAsync(requestParameters)); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Mvc/FormPostResult.cs b/src/Microsoft.AspNetCore.Identity.Service.Mvc/FormPostResult.cs new file mode 100644 index 0000000000..47546aba11 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Mvc/FormPostResult.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class FormPostResult : IActionResult + { + public FormPostResult(string redirectUri, IEnumerable> responseParameters) + { + RedirectUri = redirectUri; + ResponseParameters = responseParameters; + } + + public IEnumerable> ResponseParameters { get; } + public string RedirectUri { get; } + + public Task ExecuteResultAsync(ActionContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var generator = context.HttpContext.RequestServices.GetRequiredService(); + + return generator.GenerateResponseAsync(context.HttpContext, RedirectUri, ResponseParameters); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Mvc/FragmentResult.cs b/src/Microsoft.AspNetCore.Identity.Service.Mvc/FragmentResult.cs new file mode 100644 index 0000000000..3ee1145c8b --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Mvc/FragmentResult.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Identity.Service.Mvc +{ + public class FragmentResult : IActionResult + { + public FragmentResult(string redirectUri, IEnumerable> responseParameters) + { + RedirectUri = redirectUri; + ResponseParameters = responseParameters; + } + + public IEnumerable> ResponseParameters { get; } + public string RedirectUri { get; } + + public Task ExecuteResultAsync(ActionContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var generator = context.HttpContext.RequestServices.GetRequiredService(); + generator.GenerateResponse(context.HttpContext, RedirectUri, ResponseParameters); + + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Mvc/IdentityServiceControllerExtensions.cs b/src/Microsoft.AspNetCore.Identity.Service.Mvc/IdentityServiceControllerExtensions.cs new file mode 100644 index 0000000000..7ed0974af6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Mvc/IdentityServiceControllerExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service.Mvc +{ + public static class IdentityServiceControllerExtensions + { + public static IActionResult FormPost(this ControllerBase controller, AuthorizationRequestError error) + { + return new FormPostResult(error.RedirectUri, error.Message.Parameters); + } + + public static IActionResult Fragment(this ControllerBase controller, AuthorizationRequestError error) + { + return new FragmentResult(error.RedirectUri, error.Message.Parameters); + } + + public static IActionResult Query(this ControllerBase controller, AuthorizationRequestError error) + { + return new QueryResult(error.RedirectUri, error.Message.Parameters); + } + + public static IActionResult FormPost(this ControllerBase controller, AuthorizationResponse response) + { + return new FormPostResult(response.RedirectUri, response.Message.Parameters); + } + + public static IActionResult Fragment(this ControllerBase controller, AuthorizationResponse response) + { + return new FragmentResult(response.RedirectUri, response.Message.Parameters); + } + + public static IActionResult Query(this ControllerBase controller, AuthorizationResponse response) + { + return new QueryResult(response.RedirectUri, response.Message.Parameters); + } + + public static IActionResult InvalidAuthorization(this ControllerBase controller, AuthorizationRequestError error) + { + switch (error.ResponseMode) + { + case OpenIdConnectResponseMode.FormPost: + return controller.FormPost(error); + case OpenIdConnectResponseMode.Fragment: + return controller.Fragment(error); + case OpenIdConnectResponseMode.Query: + return controller.Query(error); + default: + return new BadRequestResult(); + } + } + + public static IActionResult ValidAuthorization(this ControllerBase controller, AuthorizationResponse response) + { + switch (response.ResponseMode) + { + case OpenIdConnectResponseMode.FormPost: + return controller.FormPost(response); + case OpenIdConnectResponseMode.Fragment: + return controller.Fragment(response); + case OpenIdConnectResponseMode.Query: + return controller.Query(response); + default: + throw new InvalidOperationException("Invalid response mode."); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Mvc/LogoutRequestModelBinder.cs b/src/Microsoft.AspNetCore.Identity.Service.Mvc/LogoutRequestModelBinder.cs new file mode 100644 index 0000000000..35ceb93e71 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Mvc/LogoutRequestModelBinder.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Identity.Service.Mvc +{ + public class LogoutRequestModelBinder : IModelBinder + { + public async Task BindModelAsync(ModelBindingContext bindingContext) + { + if (!bindingContext.IsTopLevelObject) + { + return; + } + + if (bindingContext.ModelType.Equals(typeof(LogoutRequest))) + { + var httpContext = bindingContext.HttpContext; + var httpRequest = httpContext.Request; + + IEnumerable> source = null; + if (httpRequest.Method.Equals("GET")) + { + source = httpRequest.Query; + } + + if (source == null) + { + bindingContext.Result = ModelBindingResult.Failed(); + } + else + { + var requestParameters = source.ToDictionary( + kvp => kvp.Key, + kvp => (string[])kvp.Value); + + var factory = httpContext.RequestServices.GetRequiredService(); + bindingContext.Result = ModelBindingResult.Success(await factory.CreateLogoutRequestAsync(requestParameters)); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Mvc/Microsoft.AspNetCore.Identity.Service.Mvc.csproj b/src/Microsoft.AspNetCore.Identity.Service.Mvc/Microsoft.AspNetCore.Identity.Service.Mvc.csproj new file mode 100644 index 0000000000..1eba29b504 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Mvc/Microsoft.AspNetCore.Identity.Service.Mvc.csproj @@ -0,0 +1,20 @@ + + + + + + ASP.NET Core Identity Service integration for ASP.NET Core MVC. + netcoreapp2.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Identity.Service.Mvc/QueryResult.cs b/src/Microsoft.AspNetCore.Identity.Service.Mvc/QueryResult.cs new file mode 100644 index 0000000000..71b8dbd75f --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Mvc/QueryResult.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Identity.Service.Mvc +{ + public class QueryResult : IActionResult + { + public QueryResult(string redirectUri, IEnumerable> responseParameters) + { + RedirectUri = redirectUri; + ResponseParameters = responseParameters; + } + + public IEnumerable> ResponseParameters { get; } + public string RedirectUri { get; } + + public Task ExecuteResultAsync(ActionContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var generator = context.HttpContext.RequestServices.GetRequiredService(); + generator.GenerateResponse(context.HttpContext, RedirectUri, ResponseParameters); + + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Mvc/TokenRequestModelBinder.cs b/src/Microsoft.AspNetCore.Identity.Service.Mvc/TokenRequestModelBinder.cs new file mode 100644 index 0000000000..daa8297d17 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Mvc/TokenRequestModelBinder.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Identity.Service.Mvc +{ + public class TokenRequestModelBinder : IModelBinder + { + public async Task BindModelAsync(ModelBindingContext bindingContext) + { + if (!bindingContext.IsTopLevelObject) + { + return; + } + + if (bindingContext.ModelType.Equals(typeof(TokenRequest))) + { + var httpContext = bindingContext.HttpContext; + var httpRequest = httpContext.Request; + + IEnumerable> source = null; + if (httpRequest.Method.Equals("POST")) + { + source = await httpRequest.ReadFormAsync(); + } + + if (source == null) + { + bindingContext.Result = ModelBindingResult.Failed(); + } + else + { + var requestParameters = source.ToDictionary( + kvp => kvp.Key, + kvp => (string[])kvp.Value); + + var factory = httpContext.RequestServices.GetRequiredService(); + bindingContext.Result = ModelBindingResult.Success(await factory.CreateTokenRequestAsync(requestParameters)); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Mvc/UrlHelperExtensions.cs b/src/Microsoft.AspNetCore.Identity.Service.Mvc/UrlHelperExtensions.cs new file mode 100644 index 0000000000..ec89118668 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Mvc/UrlHelperExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.AspNetCore.Identity.Service.Mvc +{ + public static class UrlHelperExtensions + { + public static string GenerateEndpointLink( + this IUrlHelper urlHelper, + string action, + string controller, + object values, + string host) + { + var httpContext = urlHelper.ActionContext.HttpContext; + return urlHelper.Action( + action, + controller, + values, + httpContext.Request.Scheme, + host ?? httpContext.Request.Host.ToString()); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceResultAssert.cs b/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceResultAssert.cs new file mode 100644 index 0000000000..510e334927 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceResultAssert.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.AspNetCore.Identity.Service; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// Helper for tests to validate identity results. + /// + public static class IdentityServiceResultAssert + { + /// + /// Asserts that the result has Succeeded. + /// + /// + public static void IsSuccess(IdentityServiceResult result) + { + Assert.NotNull(result); + Assert.True(result.Succeeded); + } + + /// + /// Asserts that the result has not Succeeded. + /// + public static void IsFailure(IdentityServiceResult result) + { + Assert.NotNull(result); + Assert.False(result.Succeeded); + } + + /// + /// Asserts that the result has not Succeeded and that error is the first Error's Description. + /// + public static void IsFailure(IdentityServiceResult result, string error) + { + Assert.NotNull(result); + Assert.False(result.Succeeded); + Assert.Equal(error, result.Errors.First().Description); + } + + /// + /// Asserts that the result has not Succeeded and that first error matches error's code and Description. + /// + public static void IsFailure(IdentityServiceResult result, IdentityServiceError error) + { + Assert.NotNull(result); + Assert.False(result.Succeeded); + Assert.Equal(error.Description, result.Errors.First().Description); + Assert.Equal(error.Code, result.Errors.First().Code); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceSpecificationTestBase.cs b/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceSpecificationTestBase.cs new file mode 100644 index 0000000000..3ecf960668 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceSpecificationTestBase.cs @@ -0,0 +1,219 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Test; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service.Specification.Tests +{ + /// + /// Common functionality tests that verifies all user manager functionality regardless of store implementation. + /// + public abstract class IdentityServiceSpecificationTestBase : IdentityServiceSpecificationTestBase + where TUser : class + where TApplication : class + { } + + /// + /// Base class for tests that exercise basic identity functionality that all stores should support. + /// + /// The type of the user. + /// The type of the application. + /// The primary key type for the user. + /// The primary key type for the application. + public abstract class IdentityServiceSpecificationTestBase + where TUser : class + where TApplication : class + where TUserKey : IEquatable + where TApplicationKey : IEquatable + { + private const string NullValue = "(null)"; + + /// + /// If true, test that require a database will be skipped. + /// + /// + protected virtual bool ShouldSkipDbTests() => false; + + /// + /// Configure the service collection used for tests. + /// + /// + /// + protected virtual void SetupIdentityServiceServices(IServiceCollection services, object context = null) + { + services.AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentityService(options => + { + }); + AddApplicationStore(services, context); + services.AddLogging(); + services.AddSingleton>>(new TestLogger>()); + } + + /// + /// Creates the application manager used for tests. + /// + /// The context that will be passed into the store, typically a db context. + /// The service collection to use, optional. + /// Delegate used to configure the services, optional. + /// The application manager to use for tests. + protected virtual ApplicationManager CreateManager(object context = null, IServiceCollection services = null, Action configureServices = null) + { + if (services == null) + { + services = new ServiceCollection(); + } + if (context == null) + { + context = CreateTestContext(); + } + SetupIdentityServiceServices(services, context); + configureServices?.Invoke(services); + return services.BuildServiceProvider().GetService>(); + } + + /// + /// Creates the context object for a test, typically a DbContext. + /// + /// The context object for a test, typically a DbContext. + protected abstract object CreateTestContext(); + + /// + /// Adds an IApplicationStore to services for the test. + /// + /// The service collection to add to. + /// The context for the store to use, optional. + protected abstract void AddApplicationStore(IServiceCollection services, object context = null); + + /// + /// Creates an application instance for testing. + /// + /// + protected abstract TApplication CreateTestApplication(); + + /// + /// Test. + /// + /// Task. + [Fact] + public async Task CanDeleteApplication() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var application = CreateTestApplication(); + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + var applicationId = await manager.GetApplicationIdAsync(application); + IdentityServiceResultAssert.IsSuccess(await manager.DeleteAsync(application)); + Assert.Null(await manager.FindByIdAsync(applicationId)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanFindById() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var application = CreateTestApplication(); + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.NotNull(await manager.FindByIdAsync(await manager.GetApplicationIdAsync(application))); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanFindByClientId() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var application = CreateTestApplication(); + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.NotNull(await manager.FindByClientIdAsync(await manager.GetApplicationClientIdAsync(application))); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGetRedirectUrisForApplication() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirectUris = GenerateRedirectUris(nameof(CanGetRedirectUrisForApplication), 2); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + foreach (var redirect in redirectUris) + { + await manager.RegisterRedirectUriAsync(application, redirect); + } + + var registeredUris = await manager.FindRegisteredUrisAsync(application); + foreach (var uri in redirectUris) + { + Assert.Contains(uri, registeredUris); + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGetScopesForApplication() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var scopes = GenerateScopes(nameof(CanGetScopesForApplication), 2); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + foreach (var redirect in scopes) + { + await manager.AddScopeAsync(application, redirect); + } + + var applicationScopes = await manager.FindScopesAsync(application); + foreach (var scope in scopes) + { + Assert.Contains(scope, applicationScopes); + } + } + + private IEnumerable GenerateRedirectUris(string prefix, int count) => + Enumerable.Range(0, count).Select(i => $"https://www.example.com/{prefix}/{count}"); + + private IEnumerable GenerateScopes(string prefix, int count) => + Enumerable.Range(0, count).Select(i => $"{prefix}_{count}"); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/Microsoft.AspNetCore.Identity.Service.Specification.Tests.csproj b/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/Microsoft.AspNetCore.Identity.Service.Specification.Tests.csproj new file mode 100644 index 0000000000..f8882b28f8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/Microsoft.AspNetCore.Identity.Service.Specification.Tests.csproj @@ -0,0 +1,31 @@ + + + + + + Shared test suite for Asp.Net Identity Core as a service store implementations. + netcoreapp2.0 + true + aspnetcore;identity + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/NuGet.config b/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/NuGet.config new file mode 100644 index 0000000000..7be9c71eca --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/NuGet.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Microsoft.AspNetCore.Identity.Service/ApplicationClaimsPrincipalFactory.cs b/src/Microsoft.AspNetCore.Identity.Service/ApplicationClaimsPrincipalFactory.cs new file mode 100644 index 0000000000..a25a416cf7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/ApplicationClaimsPrincipalFactory.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ApplicationClaimsPrincipalFactory + : IApplicationClaimsPrincipalFactory + where TApplication : class + { + public ApplicationClaimsPrincipalFactory(ApplicationManager applicationManager) + { + ApplicationManager = applicationManager; + } + + public ApplicationManager ApplicationManager { get; } + + public async Task CreateAsync(TApplication application) + { + var appId = await ApplicationManager.GetApplicationIdAsync(application); + var clientId = await ApplicationManager.GetApplicationClientIdAsync(application); + + var applicationIdentity = new ClaimsIdentity(); + applicationIdentity.AddClaim(new Claim(IdentityServiceClaimTypes.ObjectId, appId)); + applicationIdentity.AddClaim(new Claim(IdentityServiceClaimTypes.ClientId, clientId)); + + var logoutRedirectUris = await ApplicationManager.FindRegisteredLogoutUrisAsync(application); + foreach (var logoutRedirectUri in logoutRedirectUris) + { + applicationIdentity.AddClaim(new Claim(IdentityServiceClaimTypes.LogoutRedirectUri, logoutRedirectUri)); + } + + return new ClaimsPrincipal(applicationIdentity); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/ApplicationManager.cs b/src/Microsoft.AspNetCore.Identity.Service/ApplicationManager.cs new file mode 100644 index 0000000000..5b6e031b2f --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/ApplicationManager.cs @@ -0,0 +1,509 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ApplicationManager : IDisposable where TApplication : class + { + private bool _disposed; + + public ApplicationManager( + IApplicationStore store, + IPasswordHasher passwordHasher, + IEnumerable> applicationValidators, + ILogger> logger) + { + Store = store; + PasswordHasher = passwordHasher; + ApplicationValidators = applicationValidators; + Logger = Logger; + } + + public IApplicationStore Store { get; set; } + public IPasswordHasher PasswordHasher { get; set; } + public IEnumerable> ApplicationValidators { get; set; } + public ILogger> Logger { get; set; } + public CancellationToken CancellationToken { get; set; } + + public virtual bool SupportsQueryableApplications + { + get + { + ThrowIfDisposed(); + return Store is IQueryableUserStore; + } + } + + public virtual IQueryable Applications + { + get + { + var queryableStore = Store as IQueryableApplicationStore; + if (queryableStore == null) + { + throw new NotSupportedException("Store not IQueryableApplicationStore"); + } + return queryableStore.Applications; + } + } + + public Task FindByIdAsync(string applicationId) + { + return Store.FindByIdAsync(applicationId, CancellationToken); + } + + public Task FindByClientIdAsync(string clientId) + { + return Store.FindByClientIdAsync(clientId, CancellationToken.None); + } + + public Task FindByNameAsync(string name) + { + return Store.FindByNameAsync(name, CancellationToken.None); + } + + public virtual async Task CreateAsync(TApplication application) + { + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + var result = await ValidateApplicationAsync(application); + if (!result.Succeeded) + { + return result; + } + + return await Store.CreateAsync(application, CancellationToken); + } + + public virtual async Task DeleteAsync(TApplication application) + { + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return await Store.DeleteAsync(application, CancellationToken); + } + + public virtual async Task UpdateAsync(TApplication application) + { + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + var result = await ValidateApplicationAsync(application); + if (!result.Succeeded) + { + return result; + } + + return await Store.UpdateAsync(application, CancellationToken); + } + + private async Task ValidateApplicationAsync(TApplication application) + { + var errors = new List(); + foreach (var v in ApplicationValidators) + { + var result = await v.ValidateAsync(this, application); + if (!result.Succeeded) + { + errors.AddRange(result.Errors); + } + } + + if (errors.Count > 0) + { + return IdentityServiceResult.Failed(errors.ToArray()); + } + + return IdentityServiceResult.Success; + } + + public Task GetApplicationIdAsync(TApplication application) + { + ThrowIfDisposed(); + return Store.GetApplicationIdAsync(application, CancellationToken); + } + + public Task GetApplicationClientIdAsync(TApplication application) + { + ThrowIfDisposed(); + return Store.GetApplicationClientIdAsync(application, CancellationToken); + } + + public void Dispose() + { + _disposed = true; + } + + public Task GenerateClientSecretAsync() + { + return Task.FromResult(Guid.NewGuid().ToString()); + } + + public async Task AddClientSecretAsync(TApplication application, string clientSecret) + { + ThrowIfDisposed(); + var store = GetClientSecretStore(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + var hash = await store.GetClientSecretHashAsync(application, CancellationToken); + if (hash != null) + { + Logger.LogWarning(1, "User {clientId} already has a password.", await GetApplicationClientIdAsync(application)); + return IdentityServiceResult.Failed(); + } + var result = await UpdateClientSecretHashAsync(store, application, clientSecret); + if (!result.Succeeded) + { + return result; + } + + return await UpdateAsync(application); + } + + public async Task ChangeClientSecretAsync(TApplication application, string newClientSecret) + { + ThrowIfDisposed(); + var store = GetClientSecretStore(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + var result = await UpdateClientSecretHashAsync(store, application, newClientSecret); + if (!result.Succeeded) + { + return result; + } + + return await UpdateAsync(application); + } + + public async Task RemoveClientSecretAsync(TApplication application) + { + ThrowIfDisposed(); + var store = GetClientSecretStore(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + var result = await UpdateClientSecretHashAsync(store, application, clientSecret: null); + if (!result.Succeeded) + { + return result; + } + + return await UpdateAsync(application); + } + + private IRedirectUriStore GetRedirectUriStore() + { + if (Store is IRedirectUriStore cast) + { + return cast; + } + + throw new NotSupportedException(); + } + + public async Task RegisterRedirectUriAsync(TApplication application, string redirectUri) + { + var redirectStore = GetRedirectUriStore(); + var result = await redirectStore.RegisterRedirectUriAsync(application, redirectUri, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await redirectStore.UpdateAsync(application, CancellationToken); + } + + public Task> FindRegisteredUrisAsync(TApplication application) + { + var redirectStore = GetRedirectUriStore(); + return redirectStore.FindRegisteredUrisAsync(application, CancellationToken); + } + + public Task> FindRegisteredLogoutUrisAsync(TApplication application) + { + var redirectStore = GetRedirectUriStore(); + return redirectStore.FindRegisteredLogoutUrisAsync(application, CancellationToken); + } + + public async Task UnregisterRedirectUriAsync(TApplication application, string redirectUri) + { + var redirectStore = GetRedirectUriStore(); + var result = await redirectStore.UnregisterRedirectUriAsync(application, redirectUri, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await redirectStore.UpdateAsync(application, CancellationToken); + } + + public async Task UpdateRedirectUriAsync(TApplication application, string oldRedirectUri, string newRedirectUri) + { + var redirectStore = GetRedirectUriStore(); + var result = await redirectStore.UpdateRedirectUriAsync(application, oldRedirectUri, newRedirectUri, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await redirectStore.UpdateAsync(application, CancellationToken); + } + + public async Task ValidateClientCredentialsAsync(string clientId, string clientSecret) + { + var application = await FindByClientIdAsync(clientId); + if (application == null) + { + return false; + } + + var clientSecretStore = GetClientSecretStore(); + if (!await clientSecretStore.HasClientSecretAsync(application, CancellationToken)) + { + // Should we fail if clientSecret != null? + return true; + } + + if (clientSecret == null) + { + return false; + } + + var result = await VerifyClientSecretAsync(clientSecretStore, application, clientSecret); + if (result == PasswordVerificationResult.SuccessRehashNeeded) + { + await UpdateClientSecretHashAsync(clientSecretStore, application, clientSecret); + await UpdateAsync(application); + return true; + } + + return result == PasswordVerificationResult.Success; + } + + private async Task UpdateClientSecretHashAsync( + IApplicationClientSecretStore clientSecretStore, + TApplication application, + string clientSecret) + { + var hash = PasswordHasher.HashPassword(application, clientSecret); + await clientSecretStore.SetClientSecretHashAsync(application, hash, CancellationToken); + return IdentityServiceResult.Success; + } + + protected virtual async Task VerifyClientSecretAsync( + IApplicationClientSecretStore store, + TApplication application, + string clientSecret) + { + var hash = await store.GetClientSecretHashAsync(application, CancellationToken); + if (hash == null) + { + return PasswordVerificationResult.Failed; + } + + return PasswordHasher.VerifyHashedPassword(application, hash, clientSecret); + } + + private IApplicationClientSecretStore GetClientSecretStore() + { + if (Store is IApplicationClientSecretStore cast) + { + return cast; + } + + throw new NotSupportedException(); + } + + public Task> FindScopesAsync(TApplication application) + { + var scopeStore = GetScopeStore(); + return scopeStore.FindScopesAsync(application, CancellationToken); + } + + public async Task AddScopeAsync(TApplication application, string scope) + { + var scopeStore = GetScopeStore(); + var result = await scopeStore.AddScopeAsync(application, scope, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await scopeStore.UpdateAsync(application, CancellationToken); + } + + public async Task RemoveScopeAsync(TApplication application, string scope) + { + var scopeStore = GetScopeStore(); + var result = await scopeStore.RemoveScopeAsync(application, scope, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await scopeStore.UpdateAsync(application, CancellationToken); + } + + public async Task UpdateScopeAsync(TApplication application, string oldScope, string newScope) + { + var scopeStore = GetScopeStore(); + var result = await scopeStore.UpdateScopeAsync(application, oldScope, newScope, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await scopeStore.UpdateAsync(application, CancellationToken); + } + + private IApplicationScopeStore GetScopeStore() + { + if (Store is IApplicationScopeStore cast) + { + return cast; + } + + throw new NotSupportedException(); + } + + public virtual Task AddClaimAsync(TApplication application, Claim claim) + { + ThrowIfDisposed(); + var claimStore = GetApplicationClaimStore(); + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + return AddClaimsAsync(application, new Claim[] { claim }); + } + + public virtual async Task AddClaimsAsync(TApplication application, IEnumerable claims) + { + ThrowIfDisposed(); + var claimStore = GetApplicationClaimStore(); + if (claims == null) + { + throw new ArgumentNullException(nameof(claims)); + } + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + await claimStore.AddClaimsAsync(application, claims, CancellationToken); + return await UpdateAsync(application); + } + + public virtual async Task ReplaceClaimAsync(TApplication application, Claim claim, Claim newClaim) + { + ThrowIfDisposed(); + var claimStore = GetApplicationClaimStore(); + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + if (newClaim == null) + { + throw new ArgumentNullException(nameof(newClaim)); + } + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + await claimStore.ReplaceClaimAsync(application, claim, newClaim, CancellationToken); + return await UpdateAsync(application); + } + + public virtual Task RemoveClaimAsync(TApplication application, Claim claim) + { + ThrowIfDisposed(); + var claimStore = GetApplicationClaimStore(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + return RemoveClaimsAsync(application, new Claim[] { claim }); + } + + public virtual async Task RemoveClaimsAsync(TApplication application, IEnumerable claims) + { + ThrowIfDisposed(); + var claimStore = GetApplicationClaimStore(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + if (claims == null) + { + throw new ArgumentNullException(nameof(claims)); + } + + await claimStore.RemoveClaimsAsync(application, claims, CancellationToken); + return await UpdateAsync(application); + } + + public virtual async Task> GetClaimsAsync(TApplication application) + { + ThrowIfDisposed(); + var claimStore = GetApplicationClaimStore(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + return await claimStore.GetClaimsAsync(application, CancellationToken); + } + + private IApplicationClaimStore GetApplicationClaimStore() + { + if (Store is IApplicationClaimStore cast) + { + return cast; + } + + throw new NotSupportedException(); + } + + protected void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/AuthorizationStatus.cs b/src/Microsoft.AspNetCore.Identity.Service/AuthorizationStatus.cs new file mode 100644 index 0000000000..06b5b041bf --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/AuthorizationStatus.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public enum AuthorizationStatus + { + Forbidden, + LoginRequired, + Authorized + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/AuthorizeResult.cs b/src/Microsoft.AspNetCore.Identity.Service/AuthorizeResult.cs new file mode 100644 index 0000000000..2b746375e9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/AuthorizeResult.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizeResult + { + private static AuthorizeResult RequireLogin = new AuthorizeResult(); + + private AuthorizeResult(AuthorizationRequestError error) + { + Error = error; + Status = AuthorizationStatus.Forbidden; + } + + private AuthorizeResult(ClaimsPrincipal user, ClaimsPrincipal application) + { + User = user; + Application = application; + Status = AuthorizationStatus.Authorized; + } + + private AuthorizeResult() + { + Status = AuthorizationStatus.LoginRequired; + } + + public static AuthorizeResult Forbidden(AuthorizationRequestError error) + { + return new AuthorizeResult(error); + } + + public static AuthorizeResult Authorized(ClaimsPrincipal user, ClaimsPrincipal application) + { + return new AuthorizeResult(user, application); + } + + public static AuthorizeResult LoginRequired() => RequireLogin; + + public AuthorizationStatus Status { get; set; } + + public AuthorizationRequestError Error { get; set; } + + public ClaimsPrincipal User { get; set; } + + public ClaimsPrincipal Application { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/Claims/PairwiseSubTokenClaimProvider.cs b/src/Microsoft.AspNetCore.Identity.Service/Claims/PairwiseSubTokenClaimProvider.cs new file mode 100644 index 0000000000..7858741c12 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/Claims/PairwiseSubTokenClaimProvider.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class PairwiseSubClaimProvider : ITokenClaimsProvider + { + private readonly IdentityOptions _options; + + public PairwiseSubClaimProvider(IOptions options) + { + _options = options.Value; + } + public int Order => 200; + + public Task OnGeneratingClaims(TokenGeneratingContext context) + { + if(context.CurrentToken.Equals(TokenTypes.IdToken) || + context.CurrentToken.Equals(TokenTypes.AccessToken)) + { + var userId = context.User.FindFirstValue(_options.ClaimsIdentity.UserIdClaimType); + var applicationId = context.Application.FindFirstValue(IdentityServiceClaimTypes.ObjectId); + var unHashedSubjectBits = Encoding.ASCII.GetBytes($"{userId}/{applicationId}"); + var hashing = CryptographyHelpers.CreateSHA256(); + var subject = Base64UrlEncoder.Encode(hashing.ComputeHash(unHashedSubjectBits)); + Claim existingClaim = null; + foreach (var claim in context.CurrentClaims) + { + if (claim.Type.Equals(IdentityServiceClaimTypes.Subject,StringComparison.Ordinal)) + { + existingClaim = claim; + } + } + + if (existingClaim != null) + { + context.CurrentClaims.Remove(existingClaim); + } + + context.CurrentClaims.Add(new Claim(IdentityServiceClaimTypes.Subject, subject)); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/ClientApplicationValidator.cs b/src/Microsoft.AspNetCore.Identity.Service/ClientApplicationValidator.cs new file mode 100644 index 0000000000..b9f6877a9c --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/ClientApplicationValidator.cs @@ -0,0 +1,191 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ClientApplicationValidator : IClientIdValidator, IRedirectUriResolver, IScopeResolver + where TApplication : class + { + private readonly IOptions _options; + private readonly SessionManager _sessionManager; + private readonly ApplicationManager _applicationManager; + private readonly ProtocolErrorProvider _errorProvider; + + public ClientApplicationValidator( + IOptions options, + SessionManager sessionManager, + ApplicationManager applicationManager, + ProtocolErrorProvider errorProvider) + { + _options = options; + _sessionManager = sessionManager; + _applicationManager = applicationManager; + _errorProvider = errorProvider; + } + + public Task ValidateClientCredentialsAsync(string clientId, string clientSecret) + { + return _applicationManager.ValidateClientCredentialsAsync(clientId, clientSecret); + } + + public async Task ValidateClientIdAsync(string clientId) + { + return await _applicationManager.FindByClientIdAsync(clientId) != null; + } + + public async Task ResolveLogoutUriAsync(string clientId, string logoutUrl) + { + if (logoutUrl == null) + { + return RedirectUriResolutionResult.Valid(logoutUrl); + } + + var sessions = await _sessionManager.GetCurrentSessions(); + if (clientId == null) + { + foreach (var identity in sessions.Identities) + { + if (identity.HasClaim(IdentityServiceClaimTypes.LogoutRedirectUri, logoutUrl)) + { + return RedirectUriResolutionResult.Valid(logoutUrl); + } + } + + return RedirectUriResolutionResult.Invalid(null); + } + + foreach (var identity in sessions.Identities) + { + if (identity.HasClaim(IdentityServiceClaimTypes.ClientId, clientId) && + identity.HasClaim(IdentityServiceClaimTypes.LogoutRedirectUri, logoutUrl)) + { + return RedirectUriResolutionResult.Valid(logoutUrl); + } + } + + return RedirectUriResolutionResult.Invalid(null); + } + + public async Task ResolveRedirectUriAsync(string clientId, string redirectUrl) + { + if (clientId == null) + { + throw new ArgumentNullException(nameof(clientId)); + } + + var app = await _applicationManager.FindByClientIdAsync(clientId); + if (app == null) + { + return RedirectUriResolutionResult.Invalid(_errorProvider.InvalidClientId(clientId)); + } + + var redirectUris = await _applicationManager.FindRegisteredUrisAsync(app); + if (redirectUrl == null && redirectUris.Count() == 1) + { + return RedirectUriResolutionResult.Valid(redirectUris.Single()); + } + + foreach (var uri in redirectUris) + { + if (string.Equals(uri, redirectUrl, StringComparison.Ordinal)) + { + return RedirectUriResolutionResult.Valid(redirectUrl); + } + } + + return RedirectUriResolutionResult.Invalid(_errorProvider.InvalidRedirectUri(redirectUrl)); + } + + public async Task ResolveScopesAsync(string clientId, IEnumerable scopes) + { + var authorizedParty = await _applicationManager.FindByClientIdAsync(clientId); + var authorizedPartyScopes = await _applicationManager.FindScopesAsync(authorizedParty); + + var result = new List(); + + string resourceName = null; + TApplication resourceApplication = null; + IEnumerable resourceApplicationScopes = null; + + foreach (var scope in scopes) + { + var (wellFormed, canonical, name, scopeValue) = ParseScope(scope); + if (!wellFormed) + { + return ScopeResolutionResult.Invalid(_errorProvider.InvalidScope(scope)); + } + + if (canonical && authorizedPartyScopes.Any(s => s.Equals(scope, StringComparison.Ordinal))) + { + result.Add(ApplicationScope.CanonicalScopes[scope]); + } + if (canonical) + { + // We purposely ignore canonical scopes not allowed by the client application. + continue; + } + + resourceName = resourceName ?? name; + if (resourceName != null && !resourceName.Equals(name, StringComparison.Ordinal)) + { + return ScopeResolutionResult.Invalid(_errorProvider.MultipleResourcesNotSupported(resourceName, name)); + } + + if (resourceApplicationScopes == null) + { + resourceApplication = await _applicationManager.FindByNameAsync(resourceName); + if (resourceApplication == null) + { + return ScopeResolutionResult.Invalid(_errorProvider.InvalidScope(scope)); + } + + resourceApplicationScopes = await _applicationManager.FindScopesAsync(resourceApplication); + } + + if (!resourceApplicationScopes.Contains(scopeValue, StringComparer.Ordinal)) + { + return ScopeResolutionResult.Invalid(_errorProvider.InvalidScope(scope)); + } + else + { + var resourceClientId = await _applicationManager.GetApplicationClientIdAsync(resourceApplication); + result.Add(new ApplicationScope(resourceClientId, scopeValue)); + } + } + + return ScopeResolutionResult.Valid(result); + } + + private (bool wellFormed, bool canonical, string name, string value) ParseScope(string scope) + { + if (ApplicationScope.CanonicalScopes.TryGetValue(scope, out var canonicalScope)) + { + return (true, true, null, scope); + } + + var prefix = _options.Value.Issuer; + if (scope.StartsWith(prefix)) + { + var start = prefix.EndsWith("/") ? prefix.Length : prefix.Length + 1; + var end = scope.IndexOf('/', start); + if (end == -1 | end == scope.Length - 1) + { + return (false, false, null, null); + } + var applicationName = scope.Substring(start, end - start); + var scopeValue = scope.Substring(end + 1); + return (true, false, applicationName, scopeValue); + } + else + { + return (false, false, null, null); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/Configuration/IdentityServiceOptionsSetup.cs b/src/Microsoft.AspNetCore.Identity.Service/Configuration/IdentityServiceOptionsSetup.cs new file mode 100644 index 0000000000..84bb3b9730 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/Configuration/IdentityServiceOptionsSetup.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity.Service.Configuration +{ + public class IdentityServiceOptionsSetup : IConfigureOptions + { + private readonly IOptions _options; + + public IdentityServiceOptionsSetup(IOptions options) + { + _options = options; + } + + public void Configure(IdentityServiceOptions options) + { + options.IdTokenOptions.UserClaims + .AddSingle(IdentityServiceClaimTypes.Subject, _options.Value.ClaimsIdentity.UserIdClaimType); + + options.IdTokenOptions.UserClaims + .AddSingle(IdentityServiceClaimTypes.Name, _options.Value.ClaimsIdentity.UserNameClaimType); + + options.AccessTokenOptions.UserClaims + .AddSingle(IdentityServiceClaimTypes.Name, _options.Value.ClaimsIdentity.UserNameClaimType); + + options.LoginPolicy = new AuthorizationPolicyBuilder(options.LoginPolicy) + .AddAuthenticationSchemes(IdentityCookieOptions.ApplicationScheme) + .Build(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimStore.cs b/src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimStore.cs new file mode 100644 index 0000000000..cf4b0b0ec7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimStore.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IApplicationClaimStore : IApplicationStore where TApplication : class + { + Task> GetClaimsAsync(TApplication user, CancellationToken cancellationToken); + Task AddClaimsAsync(TApplication user, IEnumerable claims, CancellationToken cancellationToken); + Task ReplaceClaimAsync(TApplication user, Claim claim, Claim newClaim, CancellationToken cancellationToken); + Task RemoveClaimsAsync(TApplication user, IEnumerable claims, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimsPrincipalFactory.cs b/src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimsPrincipalFactory.cs new file mode 100644 index 0000000000..e89b06f9ef --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimsPrincipalFactory.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IApplicationClaimsPrincipalFactory + { + Task CreateAsync(TApplication application); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IApplicationClientSecretStore.cs b/src/Microsoft.AspNetCore.Identity.Service/IApplicationClientSecretStore.cs new file mode 100644 index 0000000000..3fa3aa3615 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IApplicationClientSecretStore.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IApplicationClientSecretStore + : IApplicationStore where TApplication : class + { + Task SetClientSecretHashAsync(TApplication application, string clientSecretHash, CancellationToken cancellationToken); + Task GetClientSecretHashAsync(TApplication application, CancellationToken cancellationToken); + Task HasClientSecretAsync(TApplication application, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IApplicationScopeStore.cs b/src/Microsoft.AspNetCore.Identity.Service/IApplicationScopeStore.cs new file mode 100644 index 0000000000..660af0eeba --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IApplicationScopeStore.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IApplicationScopeStore : IApplicationStore + where TApplication : class + { + Task> FindScopesAsync(TApplication application, CancellationToken cancellationToken); + Task AddScopeAsync(TApplication application, string scope, CancellationToken cancellationToken); + Task UpdateScopeAsync(TApplication application, string oldScope, string newScope, CancellationToken cancellationToken); + Task RemoveScopeAsync(TApplication application, string scope, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IApplicationStore.cs b/src/Microsoft.AspNetCore.Identity.Service/IApplicationStore.cs new file mode 100644 index 0000000000..5d91e97fd8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IApplicationStore.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IApplicationStore : IDisposable where TApplication : class + { + Task CreateAsync(TApplication application, CancellationToken cancellationToken); + Task UpdateAsync(TApplication application, CancellationToken cancellationToken); + Task DeleteAsync(TApplication application, CancellationToken cancellationToken); + Task FindByIdAsync(string applicationId, CancellationToken cancellationToken); + Task> FindByUserIdAsync(string applicationId, CancellationToken cancellationToken); + Task FindByClientIdAsync(string clientId, CancellationToken cancellationToken); + Task FindByNameAsync(string name, CancellationToken cancellationToken); + Task GetApplicationIdAsync(TApplication application, CancellationToken cancellationToken); + Task GetApplicationNameAsync(TApplication application, CancellationToken cancellationToken); + Task GetApplicationClientIdAsync(TApplication application, CancellationToken cancellationToken); + Task GetApplicationUserIdAsync(TApplication application, CancellationToken cancellationToken); + + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IApplicationValidator.cs b/src/Microsoft.AspNetCore.Identity.Service/IApplicationValidator.cs new file mode 100644 index 0000000000..2940fda849 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IApplicationValidator.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IApplicationValidator + where TApplication : class + { + Task ValidateAsync(ApplicationManager manager, TApplication application); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IQueryableApplicationStore.cs b/src/Microsoft.AspNetCore.Identity.Service/IQueryableApplicationStore.cs new file mode 100644 index 0000000000..1561c5693b --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IQueryableApplicationStore.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IQueryableApplicationStore : IApplicationStore where TApplication : class + { + IQueryable Applications { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IRedirectUriStore.cs b/src/Microsoft.AspNetCore.Identity.Service/IRedirectUriStore.cs new file mode 100644 index 0000000000..27029ddc70 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IRedirectUriStore.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public interface IRedirectUriStore : IApplicationStore + where TApplication : class + { + Task> FindRegisteredUrisAsync(TApplication app, CancellationToken cancellationToken); + Task> FindRegisteredLogoutUrisAsync(TApplication app, CancellationToken cancellationToken); + Task RegisterRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken); + Task RegisterLogoutRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken); + Task UnregisterRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken); + Task UnregisterLogoutRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken); + Task UpdateRedirectUriAsync(TApplication app, string oldRedirectUri, string newRedirectUri, CancellationToken cancellationToken); + Task UpdateLogoutRedirectUriAsync(TApplication app, string oldRedirectUri, string newRedirectUri, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceBuilder.cs b/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceBuilder.cs new file mode 100644 index 0000000000..35087642b1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceBuilder.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Identity.Service +{ + internal class IdentityServiceBuilder : IIdentityServiceBuilder + { + public IdentityServiceBuilder(IServiceCollection services) + { + Services = services; + } + + public IServiceCollection Services { get; } + + public Type ApplicationType => typeof(TApplication); + public Type UserType => typeof(TUser); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceError.cs b/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceError.cs new file mode 100644 index 0000000000..38b80f9dd2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceError.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceError + { + public IdentityServiceError() + { + } + + public string Code { get; set; } + public string Description { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceResult.cs b/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceResult.cs new file mode 100644 index 0000000000..9605a3d933 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceResult.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceResult + { + private static readonly IdentityServiceResult _success = new IdentityServiceResult { Succeeded = true }; + private List _errors = new List(); + + public bool Succeeded { get; protected set; } + + public IEnumerable Errors => _errors; + + public static IdentityServiceResult Success => _success; + + public static IdentityServiceResult Failed(params IdentityServiceError[] errors) + { + var result = new IdentityServiceResult { Succeeded = false }; + if (errors != null) + { + result._errors.AddRange(errors); + } + return result; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceServiceCollectionExtensions.cs new file mode 100644 index 0000000000..4441e14cce --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceServiceCollectionExtensions.cs @@ -0,0 +1,142 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.Service; +using Microsoft.AspNetCore.Identity.Service.Claims; +using Microsoft.AspNetCore.Identity.Service.Configuration; +using Microsoft.AspNetCore.Identity.Service.Core; +using Microsoft.AspNetCore.Identity.Service.Metadata; +using Microsoft.AspNetCore.Identity.Service.Serialization; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class IdentityServiceServiceCollectionExtensions + { + public static IIdentityServiceBuilder AddIdentityService( + this IServiceCollection services, + Action configure) + where TUser : class + where TApplication : class + { + if (services == null) + { + throw new NullReferenceException(nameof(services)); + } + + if (configure == null) + { + throw new NullReferenceException(nameof(configure)); + } + + services.AddOptions(); + services.AddWebEncoders(); + services.AddDataProtection(); + services.AddAuthentication(); + + services.TryAdd(CreateServices()); + + // Configuration + services.AddTransient, IdentityServiceOptionsDefaultSetup>(); + services.AddTransient, IdentityServiceOptionsSetup>(); + + services.AddCookieAuthentication(IdentityServiceOptions.CookieAuthenticationScheme, options => + { + options.CookieHttpOnly = true; + options.CookieSecure = CookieSecurePolicy.Always; + options.CookiePath = "/tfp/IdentityService/signinsignup"; + options.CookieName = IdentityServiceOptions.AuthenticationCookieName; + }); + services.ConfigureApplicationCookie(options => + { + options.LoginPath = $"/tfp/IdentityService/Account/Login"; + options.AccessDeniedPath = $"/tfp/IdentityService/Account/Denied"; + options.CookiePath = $"/tfp/IdentityService"; + }); + services.ConfigureApplicationCookie(options => options.CookiePath = $"/tfp/IdentityService"); + services.Configure(IdentityCookieOptions.TwoFactorRememberMeScheme, options => options.CookiePath = $"/tfp/IdentityService"); + services.Configure(IdentityCookieOptions.TwoFactorUserIdScheme, options => options.CookiePath = $"/tfp/IdentityService"); + + services.AddTransient, IdentityServiceAuthorizationOptionsSetup>(); + + // Other stuff + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton, PasswordHasher>(); + services.AddScoped(); + services.AddScoped(); + + // Session + services.AddTransient>(); + services.AddTransient>(); + services.AddTransient>(); + services.TryAddSingleton(); + + services.Configure(configure); + + return new IdentityServiceBuilder(services); + } + + private static IEnumerable CreateServices() + where TUser : class + where TApplication : class + { + yield return ServiceDescriptor.Scoped, ApplicationManager>(); + + // Protocol services + yield return ServiceDescriptor.Transient(); + yield return ServiceDescriptor.Transient(); + yield return ServiceDescriptor.Transient(); + yield return ServiceDescriptor.Transient(); + yield return ServiceDescriptor.Transient(); + yield return ServiceDescriptor.Transient(); + + // Infrastructure services + yield return ServiceDescriptor.Singleton(); + yield return ServiceDescriptor.Transient(); + yield return ServiceDescriptor.Singleton(); + yield return ServiceDescriptor.Singleton(); + yield return ServiceDescriptor.Singleton(); + yield return ServiceDescriptor.Transient, SecureDataFormat>(); + yield return ServiceDescriptor.Transient, SecureDataFormat>(); + yield return ServiceDescriptor.Singleton(sp => sp.GetDataProtectionProvider().CreateProtector("IdentityProvider")); + yield return ServiceDescriptor.Transient(); + yield return ServiceDescriptor.Transient, TokenDataSerializer>(); + yield return ServiceDescriptor.Transient, TokenDataSerializer>(); + yield return ServiceDescriptor.Transient, ApplicationClaimsPrincipalFactory>(); + + // Validation + yield return ServiceDescriptor.Transient(); + yield return ServiceDescriptor.Transient(); + yield return ServiceDescriptor.Transient(); + yield return ServiceDescriptor.Transient>(); + yield return ServiceDescriptor.Transient>(); + + // Metadata + yield return ServiceDescriptor.Singleton(); + yield return ServiceDescriptor.Singleton(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/LogoutResult.cs b/src/Microsoft.AspNetCore.Identity.Service/LogoutResult.cs new file mode 100644 index 0000000000..cd0b672580 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/LogoutResult.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class LogoutResult + { + private LogoutResult(string logoutRedirectUri) + { + Status = logoutRedirectUri == null ? LogoutStatus.LocalLogoutPage : LogoutStatus.RedirectToLogoutUri; + LogoutRedirect = logoutRedirectUri; + } + + public string LogoutRedirect { get; } + public LogoutStatus Status { get; } + + public static LogoutResult Redirect(string logoutRedirectUri) => new LogoutResult(logoutRedirectUri); + public static LogoutResult RedirectToLocalLogoutPage() => new LogoutResult(null); + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/LogoutStatus.cs b/src/Microsoft.AspNetCore.Identity.Service/LogoutStatus.cs new file mode 100644 index 0000000000..8e9c36d503 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/LogoutStatus.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Service +{ + public enum LogoutStatus + { + LocalLogoutPage, + RedirectToLogoutUri + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/Microsoft.AspNetCore.Identity.Service.csproj b/src/Microsoft.AspNetCore.Identity.Service/Microsoft.AspNetCore.Identity.Service.csproj new file mode 100644 index 0000000000..b092981067 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/Microsoft.AspNetCore.Identity.Service.csproj @@ -0,0 +1,25 @@ + + + + + + ASP.NET Core Identity Service implementation based on Entity Framework. + netcoreapp2.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Identity.Service/Session.cs b/src/Microsoft.AspNetCore.Identity.Service/Session.cs new file mode 100644 index 0000000000..9f1a8bf24d --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/Session.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class Session + { + public Session(ClaimsPrincipal user, ClaimsPrincipal application) + { + User = user; + Application = application; + } + + public ClaimsPrincipal User { get; } + public ClaimsPrincipal Application { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/SessionManager.cs b/src/Microsoft.AspNetCore.Identity.Service/SessionManager.cs new file mode 100644 index 0000000000..7b302d3cc7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/SessionManager.cs @@ -0,0 +1,281 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public abstract class SessionManager + { + private readonly IOptions _options; + private readonly IOptions _identityOptions; + private readonly CookieAuthenticationOptions _sessionCookieOptions; + private readonly ITimeStampManager _timeStampManager; + private readonly IHttpContextAccessor _contextAccessor; + protected readonly ProtocolErrorProvider _errorProvider; + + private HttpContext _context; + + public SessionManager( + IOptions options, + IOptions identityOptions, + IOptionsSnapshot cookieOptions, + ITimeStampManager timeStampManager, + IHttpContextAccessor contextAccessor, + ProtocolErrorProvider errorProvider) + { + _options = options; + _identityOptions = identityOptions; + _timeStampManager = timeStampManager; + _contextAccessor = contextAccessor; + _errorProvider = errorProvider; + _sessionCookieOptions = cookieOptions.Get(IdentityServiceOptions.CookieAuthenticationScheme); + } + + public HttpContext Context + { + get + { + if (_context == null) + { + _context = _contextAccessor.HttpContext; + } + if (_context == null) + { + throw new InvalidOperationException($"{nameof(HttpContext)} can't be null."); + } + + return _context; + } + set + { + _context = value; + } + } + + public async Task GetCurrentSessions() => + await GetPrincipal(_options.Value.SessionPolicy) ?? new ClaimsPrincipal(new ClaimsIdentity()); + + public async Task GetCurrentLoggedInUser() => + await GetPrincipal(_options.Value.LoginPolicy) ?? new ClaimsPrincipal(new ClaimsIdentity()); + + private async Task GetPrincipal(AuthorizationPolicy policy) + { + ClaimsPrincipal newPrincipal = null; + for (var i = 0; i < policy.AuthenticationSchemes.Count; i++) + { + var scheme = policy.AuthenticationSchemes[i]; + var result = await Context.AuthenticateAsync(scheme); + if (result != null) + { + newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal); + } + } + + return newPrincipal; + } + + public async Task StartSessionAsync(ClaimsPrincipal user, ClaimsPrincipal application) + { + var policy = _options.Value.SessionPolicy; + ClaimsPrincipal newPrincipal = await GetCurrentSessions(); + + newPrincipal = FilterExistingIdentities(); + newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, CreatePrincipal()); + + for (var i = 0; i < policy.AuthenticationSchemes.Count; i++) + { + var scheme = policy.AuthenticationSchemes[i]; + await Context.SignInAsync(scheme, newPrincipal); + } + + ClaimsPrincipal FilterExistingIdentities() + { + var scheme = IdentityServiceOptions.CookieAuthenticationScheme; + string userIdClaimType = _identityOptions.Value.ClaimsIdentity.UserIdClaimType; + var userId = user.FindFirstValue(userIdClaimType); + var clientId = application.FindFirstValue(IdentityServiceClaimTypes.ClientId); + + var filteredIdentities = newPrincipal.Identities + .Where(i => scheme.Equals(i.AuthenticationType, StringComparison.Ordinal) && + !IsUserSesionForApplication(i, userId, clientId)); + + return new ClaimsPrincipal(filteredIdentities); + } + + ClaimsPrincipal CreatePrincipal() + { + var principal = new ClaimsPrincipal(); + var userId = user.FindFirstValue(_identityOptions.Value.ClaimsIdentity.UserIdClaimType); + var clientId = application.FindFirstValue(IdentityServiceClaimTypes.ClientId); + var logoutUris = application.FindAll(IdentityServiceClaimTypes.LogoutRedirectUri); + + var duration = _sessionCookieOptions.ExpireTimeSpan; + var expiration = _timeStampManager.GetTimeStampInEpochTime(duration); + + var identity = new ClaimsIdentity( + new List(logoutUris) + { + new Claim(IdentityServiceClaimTypes.UserId,userId), + new Claim(IdentityServiceClaimTypes.ClientId,clientId), + new Claim(IdentityServiceClaimTypes.Expires,expiration) + }, + IdentityServiceOptions.CookieAuthenticationScheme); + + principal.AddIdentity(identity); + + return principal; + } + + } + + public async Task EndSessionAsync(LogoutRequest request) + { + var loginPolicy = _options.Value.LoginPolicy; + for (int i = 0; i < loginPolicy.AuthenticationSchemes.Count; i++) + { + var scheme = loginPolicy.AuthenticationSchemes[i]; + await Context.SignOutAsync(scheme); + } + + var policy = _options.Value.SessionPolicy; + + for (var i = 0; i < policy.AuthenticationSchemes.Count; i++) + { + var scheme = policy.AuthenticationSchemes[i]; + await Context.SignOutAsync(scheme); + } + + var postLogoutUri = request.LogoutRedirectUri; + var state = request.Message.State; + var redirectUri = request.Message.State == null ? + postLogoutUri : + QueryHelpers.AddQueryString(postLogoutUri, OpenIdConnectParameterNames.State, state); + + return LogoutResult.Redirect(redirectUri); + } + + private bool IsUserSesionForApplication(ClaimsIdentity identity, string userId, string clientId) + { + var userIdClaimType = _identityOptions.Value.ClaimsIdentity.UserIdClaimType; + return identity.Claims.SingleOrDefault(c => ClaimMatches(c, userIdClaimType, userId)) != null && + identity.Claims.SingleOrDefault(c => ClaimMatches(c, IdentityServiceClaimTypes.ClientId, clientId)) != null; + + bool ClaimMatches(Claim claim, string type, string value) => + claim.Type.Equals(type, StringComparison.Ordinal) && claim.Value.Equals(value, StringComparison.Ordinal); + } + + protected bool IsAuthenticatedWithApplication(ClaimsPrincipal loggedUser, ClaimsPrincipal sessions, OpenIdConnectMessage message) + { + string userIdClaimType = _identityOptions.Value.ClaimsIdentity.UserIdClaimType; + var userId = loggedUser.FindFirstValue(userIdClaimType); + var clientId = message.ClientId; + + return sessions.Identities.Any(i => IsUserSesionForApplication(i, userId, clientId)) || + loggedUser.Identities.Any(i => i.IsAuthenticated); + } + + public abstract Task CreateSessionAsync(string userId, string clientId); + + public abstract Task IsAuthorizedAsync(AuthorizationRequest request); + } + + public class SessionManager : SessionManager where TUser : class where TApplication : class + { + private readonly UserManager _userManager; + private readonly IUserClaimsPrincipalFactory _userPrincipalFactory; + private readonly ApplicationManager _applicationManager; + private readonly IApplicationClaimsPrincipalFactory _applicationPrincipalFactory; + + public SessionManager( + IOptions options, + IOptions identityOptions, + IOptionsSnapshot cookieOptions, + ITimeStampManager timeStampManager, + UserManager userManager, + IUserClaimsPrincipalFactory userPrincipalFactory, + IApplicationClaimsPrincipalFactory applicationPrincipalFactory, + ApplicationManager applicationManager, + IHttpContextAccessor contextAccessor, + ProtocolErrorProvider errorProvider) + : base(options, identityOptions, cookieOptions, timeStampManager, contextAccessor, errorProvider) + { + _userManager = userManager; + _userPrincipalFactory = userPrincipalFactory; + _applicationManager = applicationManager; + _applicationPrincipalFactory = applicationPrincipalFactory; + } + + public override async Task IsAuthorizedAsync(AuthorizationRequest request) + { + var message = request.Message; + var sessions = await GetCurrentSessions(); + var loggedUser = await GetCurrentLoggedInUser(); + + var hasASession = IsAuthenticatedWithApplication(loggedUser, sessions, message); + var isLoggedIn = loggedUser.Identities.Any(i => i.IsAuthenticated); + if (!(hasASession || isLoggedIn) && PromptIsForbidden(message)) + { + return AuthorizeResult.Forbidden(RequiresLogin(request)); + } + + if (!(hasASession || isLoggedIn) || PromptIsMandatory(message)) + { + return AuthorizeResult.LoginRequired(); + } + + var user = await _userManager.GetUserAsync(loggedUser); + var userPrincipal = await _userPrincipalFactory.CreateAsync(user); + + var application = await _applicationManager.FindByClientIdAsync(message.ClientId); + var applicationPrincipal = await _applicationPrincipalFactory.CreateAsync(application); + + return AuthorizeResult.Authorized(userPrincipal, applicationPrincipal); + } + + public override async Task CreateSessionAsync(string userId, string clientId) + { + var user = await _userManager.FindByIdAsync(userId); + var userPrincipal = await _userPrincipalFactory.CreateAsync(user); + + var application = await _applicationManager.FindByClientIdAsync(clientId); + var applicationPrincipal = await _applicationPrincipalFactory.CreateAsync(application); + + return new Session(userPrincipal, applicationPrincipal); + } + + private bool PromptIsMandatory(OpenIdConnectMessage message) + { + return message.Prompt != null && message.Prompt.Contains(PromptValues.Login); + } + + private bool PromptIsForbidden(OpenIdConnectMessage message) + { + return message.Prompt != null && message.Prompt.Contains(PromptValues.None); + } + + private AuthorizationRequestError RequiresLogin(AuthorizationRequest request) + { + var error = _errorProvider.RequiresLogin(); + error.State = request.Message.State; + + return new AuthorizationRequestError( + error, + request.RequestGrants.RedirectUri, + request.RequestGrants.ResponseMode); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test.csproj b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test.csproj new file mode 100644 index 0000000000..9e98f2ed76 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test.csproj @@ -0,0 +1,22 @@ + + + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/TokenGeneratingContextTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/TokenGeneratingContextTest.cs new file mode 100644 index 0000000000..c7321a17c9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/TokenGeneratingContextTest.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenGeneratingContextTest + { + [Theory] + [InlineData("code", "code")] + [InlineData("token", "access_token")] + [InlineData("id_token", "id_token")] + [InlineData("code token", "code access_token")] + [InlineData("code id_token", "code id_token")] + [InlineData("token id_token", "access_token id_token")] + [InlineData("code token id_token", "code access_token id_token")] + public void CreateTokenGenerationContext_CorrectlyCreatesContext_FromTheAuthorizationRequest( + string responseTypes, + string expectedRequestedTokens) + { + // Arrange + var authorizationRequest = new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://localhost:123/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { responseTypes }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { OpenIdConnectResponseMode.FormPost }, + [OpenIdConnectParameterNames.Scope] = new[] { "openid" }, + [OpenIdConnectParameterNames.Nonce] = new[] { "asdf" }, + [OpenIdConnectParameterNames.State] = new[] { "state" } + }; + var message = new OpenIdConnectMessage(authorizationRequest); + + var requestGrants = new RequestGrants + { + RedirectUri = "http://localhost:123/callback", + ResponseMode = OpenIdConnectResponseMode.FormPost, + Scopes = new List { ApplicationScope.OpenId }, + Tokens = expectedRequestedTokens.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) + }; + + var request = AuthorizationRequest.Valid(message, requestGrants); + + var user = new ClaimsPrincipal(); + var application = new ClaimsPrincipal(); + + // Act + var context = request.CreateTokenGeneratingContext(user, application); + + // Assert + Assert.NotNull(context); + Assert.Equal(expectedRequestedTokens.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries), context.RequestGrants.Tokens); + Assert.Equal("http://localhost:123/callback", context.RequestParameters.RedirectUri); + Assert.Equal("openid", context.RequestParameters.Scope); + Assert.Equal("asdf", context.RequestParameters.Nonce); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/AccessTokenTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/AccessTokenTest.cs new file mode 100644 index 0000000000..42d25a166a --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/AccessTokenTest.cs @@ -0,0 +1,317 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AccessTokenTest + { + [Fact] + public void CreateAccessToken_Fails_IfMissingIssuerClaim() + { + // Arrange + var claims = new List(); + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfMultipleIssuerClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer,"issuer"), + new Claim(IdentityServiceClaimTypes.Issuer,"issuer"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfMissingSubjectClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Subject, "subject") + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfMultipleSubjectClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Subject, "subject") + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfMissingAudienceClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereIsMoreThanOneAudienceClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Audience, "audience2"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereIsNoScopeClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereAreMultipleScopeClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "read"), + new Claim(IdentityServiceClaimTypes.Scope, "write"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereIsNoAuthorizedPartyClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "scope"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereAreMultipleAuthorizedPartyClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "read"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, "authorizedParty1"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, "authorizedParty2"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereIsNoTokenId() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "read"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, "authorizedParty1"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereAreMultipletokenIdClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "read"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, "authorizedParty1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "id1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "id2"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereIsNoIssuedAt() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "read"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, "authorizedParty1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereAreMultipleIssuedAtClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "read"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, "authorizedParty1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt1"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt2"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereIsNoExpires() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "read"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, "authorizedParty1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereAreMultipleExpiresClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "read"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, "authorizedParty1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereIsNoNotBefore() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "read"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, "authorizedParty1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + + [Fact] + public void CreateAccessToken_Fails_IfThereAreMultipleNotBeforeClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Scope, "read"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, "authorizedParty1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + new Claim(IdentityServiceClaimTypes.NotBefore, "notBefore"), + new Claim(IdentityServiceClaimTypes.NotBefore, "notBefore"), + }; + + // Act & Assert + Assert.Throws(() => new AccessToken(claims)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/AuthorizationCodeTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/AuthorizationCodeTest.cs new file mode 100644 index 0000000000..c80f818918 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/AuthorizationCodeTest.cs @@ -0,0 +1,285 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationCodeTest + { + [Fact] + public void CreateAuthorizationCode_Fails_IfMissingUserIdClaim() + { + // Arrange + var claims = new List(); + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfMultipleUserClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId,"userId"), + new Claim(IdentityServiceClaimTypes.UserId,"userId"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfMissingClientIdClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId") + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfMultipleClientIdClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId") + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereIsMoreThanOneRedirectUri() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri2"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereIsNoScopeClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereAreMultipleScopeClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid profile"), + new Claim(IdentityServiceClaimTypes.Scope, "offline_access"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereIsNoGrantedToken() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereIsNoTokenId() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereAreMultipletokenIdClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "id1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "id2"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereIsNoIssuedAt() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereAreMultipleIssuedAtClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt1"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt2"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereIsNoExpires() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereAreMultipleExpiresClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereIsNoNotBefore() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + + [Fact] + public void CreateAuthorizationCode_Fails_IfThereAreMultipleNotBeforeClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + new Claim(IdentityServiceClaimTypes.NotBefore, "notBefore"), + new Claim(IdentityServiceClaimTypes.NotBefore, "notBefore"), + }; + + // Act & Assert + Assert.Throws(() => new AuthorizationCode(claims)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/IdTokenTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/IdTokenTest.cs new file mode 100644 index 0000000000..2c3de1a577 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/IdTokenTest.cs @@ -0,0 +1,313 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdTokenTest + { + [Fact] + public void CreateIdToken_Fails_IfMissingIssuerClaim() + { + // Arrange + var claims = new List(); + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfMultipleIssuerClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer,"issuer"), + new Claim(IdentityServiceClaimTypes.Issuer,"issuer"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfMissingSubjectClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Subject, "subject") + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfMultipleSubjectClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Subject, "subject") + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfMissingAudienceClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereAreMultipleAudienceClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Audience, "audience2"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereAreMultipleNonceClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce2"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereAreMultipleCodeHashClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash2"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereAreMultipleAccessTokenHashClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash1"), + new Claim(IdentityServiceClaimTypes.AccessTokenHash, "athash2"), + new Claim(IdentityServiceClaimTypes.AccessTokenHash, "athash2"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereIsNoTokenId() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash1"), + new Claim(IdentityServiceClaimTypes.AccessTokenHash, "athash2"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereAreMultipletokenIdClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash1"), + new Claim(IdentityServiceClaimTypes.AccessTokenHash, "athash2"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "id1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "id2"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereIsNoIssuedAt() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash1"), + new Claim(IdentityServiceClaimTypes.AccessTokenHash, "athash2"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereAreMultipleIssuedAtClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash1"), + new Claim(IdentityServiceClaimTypes.AccessTokenHash, "athash2"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt1"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt2"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereIsNoExpires() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash1"), + new Claim(IdentityServiceClaimTypes.AccessTokenHash, "athash2"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereAreMultipleExpiresClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash1"), + new Claim(IdentityServiceClaimTypes.AccessTokenHash, "athash2"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereIsNoNotBefore() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash1"), + new Claim(IdentityServiceClaimTypes.AccessTokenHash, "athash2"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + + [Fact] + public void CreateIdToken_Fails_IfThereAreMultipleNotBeforeClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.Issuer, "issuer"), + new Claim(IdentityServiceClaimTypes.Subject, "subject"), + new Claim(IdentityServiceClaimTypes.Audience, "audience1"), + new Claim(IdentityServiceClaimTypes.Nonce, "nonce1"), + new Claim(IdentityServiceClaimTypes.CodeHash, "chash1"), + new Claim(IdentityServiceClaimTypes.AccessTokenHash, "athash2"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + new Claim(IdentityServiceClaimTypes.NotBefore, "notBefore"), + new Claim(IdentityServiceClaimTypes.NotBefore, "notBefore"), + }; + + // Act & Assert + Assert.Throws(() => new IdToken(claims)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/RefreshTokenTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/RefreshTokenTest.cs new file mode 100644 index 0000000000..7cd39dd1fc --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Abstractions.Test/Tokens/RefreshTokenTest.cs @@ -0,0 +1,269 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class RefreshTokenTest + { + [Fact] + public void CreateRefreshToken_Fails_IfMissingUserIdClaim() + { + // Arrange + var claims = new List(); + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfMultipleUserClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId,"userId"), + new Claim(IdentityServiceClaimTypes.UserId,"userId"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfMissingClientIdClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId") + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfMultipleClientIdClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId") + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereIsNoScopeClaim() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereAreMultipleScopeClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid profile"), + new Claim(IdentityServiceClaimTypes.Scope, "offline_access"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereIsNoGrantedToken() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereIsNoTokenId() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereAreMultipletokenIdClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "id1"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "id2"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereIsNoIssuedAt() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereAreMultipleIssuedAtClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt1"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt2"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereIsNoExpires() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereAreMultipleExpiresClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereIsNoNotBefore() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + + [Fact] + public void CreateRefreshToken_Fails_IfThereAreMultipleNotBeforeClaims() + { + // Arrange + var claims = new List() + { + new Claim(IdentityServiceClaimTypes.UserId, "userId"), + new Claim(IdentityServiceClaimTypes.ClientId, "clientId"), + new Claim(IdentityServiceClaimTypes.RedirectUri, "redirectUri1"), + new Claim(IdentityServiceClaimTypes.Scope, "openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken, "access_token"), + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "tuid"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + new Claim(IdentityServiceClaimTypes.NotBefore, "notBefore"), + new Claim(IdentityServiceClaimTypes.NotBefore, "notBefore"), + }; + + // Act & Assert + Assert.Throws(() => new RefreshToken(claims)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationCodeExchangeIntegrationTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationCodeExchangeIntegrationTest.cs new file mode 100644 index 0000000000..9d6e804e30 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationCodeExchangeIntegrationTest.cs @@ -0,0 +1,240 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Buffers; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity.Service.Claims; +using Microsoft.AspNetCore.Identity.Service.Core; +using Microsoft.AspNetCore.Identity.Service.Serialization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationCodeExchangeIntegrationTest + { + [Fact] + public async Task ValidAuthorizationCode_ProducesAccessTokenIdTokenAndRefreshToken() + { + // Arrange + var tokenManager = GetTokenManager(); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Form = new FormCollection(new Dictionary + { + ["grant_type"] = "authorization_code", + ["code"] = await CreateAuthorizationCode(tokenManager), + ["client_id"] = "s6BhdRkqt", + ["redirect_uri"] = "https://client.example.org/cb", + ["scope"] = "openid offline_access" + }); + + var factory = CreateRequestFactory(tokenManager); + + var user = CreateUser("user"); + var application = CreateApplication("s6BhdRkqt"); + var responseGenerator = CreateTokenResponseFactory(); + + // Act + var result = await factory.CreateTokenRequestAsync(httpContext.Request.Form.ToDictionary(kvp => kvp.Key, kvp => (string[])kvp.Value)); + + var context = result.CreateTokenGeneratingContext(user, application); + + await tokenManager.IssueTokensAsync(context); + + var response = await responseGenerator.CreateTokenResponseAsync(context); + + // Assert + Assert.Equal(5, response.Parameters.Count); + Assert.Equal("Bearer", response.TokenType); + Assert.NotNull(response.IdToken); + Assert.Contains(response.Parameters, kvp => kvp.Key == "id_token_expires_in"); + Assert.Equal("7200", response.Parameters["id_token_expires_in"]); + Assert.NotNull(response.RefreshToken); + Assert.Contains(response.Parameters, kvp => kvp.Key == "refresh_token_expires_in"); + Assert.Equal("2592000", response.Parameters["refresh_token_expires_in"]); + } + + private ITokenResponseFactory CreateTokenResponseFactory() => + new DefaultTokenResponseFactory(new ITokenResponseParameterProvider[]{ + new DefaultTokenResponseParameterProvider(new TimeStampManager()) + }); + + private async Task CreateAuthorizationCode(ITokenManager tokenManager) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = QueryString.FromUriComponent(@"?response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=openid%20profile%20email%20offline_access&nonce=n-0S6_WzA2Mj&state=af0ifjsldkj"); + var requestParameters = httpContext.Request.Query.ToDictionary(kvp => kvp.Key, kvp => (string[])kvp.Value); + + var requestFactory = CreateAuthorizationRequestFactory(); + + var user = CreateUser("user"); + var application = CreateApplication("s6BhdRkqt"); + + var queryExecutor = new QueryResponseGenerator(); + + // Act + var result = await requestFactory.CreateAuthorizationRequestAsync(requestParameters); + var authorization = result.Message; + + var tokenContext = result.CreateTokenGeneratingContext(user, application); + + await tokenManager.IssueTokensAsync(tokenContext); + + return tokenContext.AuthorizationCode.SerializedValue; + } + + private IAuthorizationRequestFactory CreateAuthorizationRequestFactory() + { + var clientIdValidatorMock = new Mock(); + clientIdValidatorMock + .Setup(m => m.ValidateClientIdAsync(It.IsAny())) + .ReturnsAsync(true); + + var redirectUriValidatorMock = new Mock(); + redirectUriValidatorMock + .Setup(m => m.ResolveRedirectUriAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((string clientId, string redirectUrl) => RedirectUriResolutionResult.Valid(redirectUrl)); + + var scopeValidatorMock = new Mock(); + scopeValidatorMock + .Setup(m => m.ResolveScopesAsync(It.IsAny(), It.IsAny>())) + .ReturnsAsync( + (string clientId, IEnumerable scopes) => + ScopeResolutionResult.Valid(scopes.Select(s => ApplicationScope.CanonicalScopes.TryGetValue(s, out var parsedScope) ? parsedScope : new ApplicationScope(clientId, s)))); + + return new AuthorizationRequestFactory( + clientIdValidatorMock.Object, + redirectUriValidatorMock.Object, + scopeValidatorMock.Object, + Enumerable.Empty(), + new ProtocolErrorProvider()); + } + + private static ClaimsPrincipal CreateApplication(string clientId) => + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, clientId) })); + + private static ClaimsPrincipal CreateUser(string userName) => + new ClaimsPrincipal(new ClaimsIdentity(new[] { + new Claim(ClaimTypes.Name, userName), + new Claim(ClaimTypes.NameIdentifier, userName)})); + + private static TokenManager GetTokenManager() + { + var options = CreateOptions(); + var claimsManager = CreateClaimsManager(options); + + var factory = new LoggerFactory(); + var protector = new EphemeralDataProtectionProvider(factory).CreateProtector("test"); + var codeSerializer = new TokenDataSerializer(options, ArrayPool.Shared); + var codeDataFormat = new SecureDataFormat(codeSerializer, protector); + var refreshTokenSerializer = new TokenDataSerializer(options, ArrayPool.Shared); + var refreshTokenDataFormat = new SecureDataFormat(refreshTokenSerializer, protector); + + var timeStampManager = new TimeStampManager(); + var credentialsPolicy = GetCredentialsPolicy(options, timeStampManager); + var codeIssuer = new AuthorizationCodeIssuer(claimsManager, codeDataFormat, new ProtocolErrorProvider()); + var accessTokenIssuer = new JwtAccessTokenIssuer(claimsManager, credentialsPolicy, new JwtSecurityTokenHandler(), options); + var idTokenIssuer = new JwtIdTokenIssuer(claimsManager, credentialsPolicy, new JwtSecurityTokenHandler(), options); + var refreshTokenIssuer = new RefreshTokenIssuer(claimsManager, refreshTokenDataFormat); + + return new TokenManager( + codeIssuer, + accessTokenIssuer, + idTokenIssuer, + refreshTokenIssuer, + new ProtocolErrorProvider()); + } + + private static DefaultSigningCredentialsPolicyProvider GetCredentialsPolicy(IOptionsSnapshot options, TimeStampManager timeStampManager) => + new DefaultSigningCredentialsPolicyProvider( + new List { + new DefaultSigningCredentialsSource(options, timeStampManager) + }, + timeStampManager, + new HostingEnvironment()); + + private static ITokenClaimsManager CreateClaimsManager( + IOptions options) + { + return new DefaultTokenClaimsManager( + new List{ + new DefaultTokenClaimsProvider(options), + new GrantedTokensTokenClaimsProvider(), + new NonceTokenClaimsProvider(), + new ScopesTokenClaimsProvider(), + new TimestampsTokenClaimsProvider(new TimeStampManager(),options), + new TokenHashTokenClaimsProvider(new TokenHasher()) + }); + } + + private static IOptionsSnapshot CreateOptions() + { + var identityServiceOptions = new IdentityServiceOptions(); + var optionsSetup = new IdentityServiceOptionsDefaultSetup(); + optionsSetup.Configure(identityServiceOptions); + + SigningCredentials signingCredentials = new SigningCredentials(CryptoUtilities.CreateTestKey(), "RS256"); + identityServiceOptions.SigningKeys.Add(signingCredentials); + identityServiceOptions.Issuer = "http://server.example.com"; + identityServiceOptions.IdTokenOptions.UserClaims.AddSingle( + IdentityServiceClaimTypes.Subject, + ClaimTypes.NameIdentifier); + + identityServiceOptions.RefreshTokenOptions.UserClaims.AddSingle( + IdentityServiceClaimTypes.Subject, + ClaimTypes.NameIdentifier); + + var mock = new Mock>(); + mock.Setup(m => m.Value).Returns(identityServiceOptions); + mock.Setup(m => m.Get(It.IsAny())).Returns(identityServiceOptions); + + return mock.Object; + } + + private ITokenRequestFactory CreateRequestFactory(ITokenManager tokenManager) + { + var clientIdValidatorMock = new Mock(); + clientIdValidatorMock + .Setup(m => m.ValidateClientIdAsync(It.IsAny())) + .ReturnsAsync(true); + + clientIdValidatorMock + .Setup(m => m.ValidateClientCredentialsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + var redirectUriValidatorMock = new Mock(); + redirectUriValidatorMock + .Setup(m => m.ResolveRedirectUriAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((string clientId, string redirectUrl) => RedirectUriResolutionResult.Valid(redirectUrl)); + + var scopeValidatorMock = new Mock(); + scopeValidatorMock + .Setup(m => m.ResolveScopesAsync(It.IsAny(), It.IsAny>())) + .ReturnsAsync( + (string clientId, IEnumerable scopes) => + ScopeResolutionResult.Valid(scopes.Select(s => ApplicationScope.CanonicalScopes.TryGetValue(s, out var parsedScope) ? parsedScope : new ApplicationScope(clientId, s)))); + + return new TokenRequestFactory( + clientIdValidatorMock.Object, + redirectUriValidatorMock.Object, + scopeValidatorMock.Object, + Enumerable.Empty(), + tokenManager, + new TimeStampManager(), + new ProtocolErrorProvider()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationCodeIssuerTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationCodeIssuerTest.cs new file mode 100644 index 0000000000..1570baea36 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationCodeIssuerTest.cs @@ -0,0 +1,238 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity.Service.Claims; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationCodeIssuerTest + { + [Fact] + public async Task AuthorizationCodeIssuer_Fails_IfUserIsMissingUserId() + { + // Arrange + var dataFormat = GetDataFormat(); + var issuer = new AuthorizationCodeIssuer(GetClaimsManager(), dataFormat, new ProtocolErrorProvider()); + var context = GetTokenGenerationContext(); + + context.InitializeForToken(TokenTypes.AuthorizationCode); + + // Act + var exception = await Assert.ThrowsAsync( + () => issuer.CreateAuthorizationCodeAsync(context)); + + // Assert + Assert.Equal($"Missing '{ClaimTypes.NameIdentifier}' claim from the user.", exception.Message); + } + + [Fact] + public async Task AuthorizationCodeIssuer_Fails_IfApplicationIsMissingClientId() + { + // Arrange + var dataFormat = GetDataFormat(); + var options = GetOptions(); + var timeManager = GetTimeManager(); + var issuer = new AuthorizationCodeIssuer(GetClaimsManager(), dataFormat, new ProtocolErrorProvider()); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") }))); + + context.InitializeForToken(TokenTypes.AuthorizationCode); + + // Act + var exception = await Assert.ThrowsAsync( + () => issuer.CreateAuthorizationCodeAsync(context)); + + // Assert + Assert.Equal($"Missing '{IdentityServiceClaimTypes.ClientId}' claim from the application.", exception.Message); + } + + [Fact] + public async Task AuthorizationCodeIssuer_ProtectsAuthorizationCode() + { + // Arrange + var dataFormat = GetDataFormat(); + var options = GetOptions(); + var timeManager = GetTimeManager(); + var issuer = new AuthorizationCodeIssuer(GetClaimsManager(), dataFormat, new ProtocolErrorProvider()); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") }))); + + context.InitializeForToken(TokenTypes.AuthorizationCode); + + // Act + await issuer.CreateAuthorizationCodeAsync(context); + + // Assert + Assert.NotNull(context.AuthorizationCode); + Assert.Equal("protected authorization code", context.AuthorizationCode.SerializedValue); + } + + [Fact] + public async Task AuthorizationCodeIssuer_IncludesNonceWhenPresent() + { + // Arrange + var dataFormat = GetDataFormat(); + var options = GetOptions(); + var timeManager = GetTimeManager(); + var issuer = new AuthorizationCodeIssuer(GetClaimsManager(), dataFormat, new ProtocolErrorProvider()); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") }))); + + context.InitializeForToken(TokenTypes.AuthorizationCode); + + // Act + await issuer.CreateAuthorizationCodeAsync(context); + + // Assert + Assert.NotNull(context.AuthorizationCode); + var result = Assert.IsType(context.AuthorizationCode.Token); + Assert.NotNull(result); + Assert.Equal("asdf", result.Nonce); + } + + [Fact] + public async Task AuthorizationCodeIssuer_DoesNotIncludeNonceWhenAbsent() + { + // Arrange + var dataFormat = GetDataFormat(); + var issuer = new AuthorizationCodeIssuer(GetClaimsManager(), dataFormat, new ProtocolErrorProvider()); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") })), + nonce: null); + + context.InitializeForToken(TokenTypes.AuthorizationCode); + + // Act + await issuer.CreateAuthorizationCodeAsync(context); + + // Assert + Assert.NotNull(context.AuthorizationCode); + var result = Assert.IsType(context.AuthorizationCode.Token); + Assert.NotNull(result); + Assert.Equal(null, result.Nonce); + } + + [Fact] + public async Task AuthorizationCodeIssuer_IncludesAllRequiredData() + { + // Arrange + var dataFormat = GetDataFormat(); + var expectedDateTime = new DateTimeOffset(2000, 01, 01, 0, 0, 0, TimeSpan.FromHours(1)); + var timeManager = GetTimeManager(expectedDateTime, expectedDateTime.AddHours(1), expectedDateTime); + + var issuer = new AuthorizationCodeIssuer(GetClaimsManager(timeManager), dataFormat, new ProtocolErrorProvider()); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") }))); + + context.InitializeForToken(TokenTypes.AuthorizationCode); + + // Act + await issuer.CreateAuthorizationCodeAsync(context); + + // Assert + Assert.NotNull(context.AuthorizationCode); + var code = Assert.IsType(context.AuthorizationCode.Token); + Assert.NotNull(code); + Assert.NotNull(code.Id); + Assert.Equal("user", code.UserId); + Assert.Equal("clientId", code.ClientId); + Assert.Equal("http://www.example.com/callback", code.RedirectUri); + Assert.Equal(new[] { "openid" }, code.Scopes); + Assert.Equal("asdf", code.Nonce); + Assert.Equal(expectedDateTime, code.IssuedAt); + Assert.Equal(expectedDateTime.AddHours(1), code.Expires); + Assert.Equal(expectedDateTime, code.NotBefore); + } + + private TokenGeneratingContext GetTokenGenerationContext( + ClaimsPrincipal user = null, + ClaimsPrincipal application = null, + string nonce = "asdf") => + new TokenGeneratingContext( + user ?? new ClaimsPrincipal(new ClaimsIdentity()), + application ?? new ClaimsPrincipal(new ClaimsIdentity()), + new OpenIdConnectMessage + { + Code = "code", + Scope = "openid", + Nonce = nonce, + RedirectUri = "http://www.example.com/callback" + }, + new RequestGrants + { + Scopes = new ApplicationScope[] { ApplicationScope.OpenId }, + RedirectUri = "http://www.example.com/callback", + Tokens = new string[] { TokenTypes.AuthorizationCode } + }); + + private ITimeStampManager GetTimeManager( + DateTimeOffset? issuedAt = null, + DateTimeOffset? expires = null, + DateTimeOffset? notBefore = null) + { + issuedAt = issuedAt ?? DateTimeOffset.Now; + expires = expires ?? DateTimeOffset.Now; + notBefore = notBefore ?? DateTimeOffset.Now; + + var manager = new Mock(); + + manager.Setup(m => m.GetCurrentTimeStampInEpochTime()) + .Returns(issuedAt.Value.ToUnixTimeSeconds().ToString()); + + manager.SetupSequence(t => t.GetTimeStampInEpochTime(It.IsAny())) + .Returns(notBefore.Value.ToUnixTimeSeconds().ToString()) + .Returns(expires.Value.ToUnixTimeSeconds().ToString()); + + return manager.Object; + } + + private IOptions GetOptions() + { + var IdentityServiceOptions = new IdentityServiceOptions(); + + var optionsSetup = new IdentityServiceOptionsDefaultSetup(); + optionsSetup.Configure(IdentityServiceOptions); + + var mock = new Mock>(); + mock.Setup(m => m.Value).Returns(IdentityServiceOptions); + + return mock.Object; + } + + private ISecureDataFormat GetDataFormat() + { + var mock = new Mock>(); + mock.Setup(s => s.Protect(It.IsAny())) + .Returns("protected authorization code"); + + return mock.Object; + } + + private ITokenClaimsManager GetClaimsManager(ITimeStampManager timeManager = null) + { + var options = GetOptions(); + + return new DefaultTokenClaimsManager(new ITokenClaimsProvider[] + { + new DefaultTokenClaimsProvider(options), + new GrantedTokensTokenClaimsProvider(), + new NonceTokenClaimsProvider(), + new ScopesTokenClaimsProvider(), + new TimestampsTokenClaimsProvider(timeManager ?? new TimeStampManager(), options), + new TokenHashTokenClaimsProvider(new TokenHasher()) + }); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationRequestFactoryTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationRequestFactoryTest.cs new file mode 100644 index 0000000000..f149355b78 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizationRequestFactoryTest.cs @@ -0,0 +1,826 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizationRequestFactoryTest + { + public static ProtocolErrorProvider ProtocolErrorProvider = new ProtocolErrorProvider(); + + [Fact] + public async Task FailsToCreateAuthorizationRequest_IfState_HasMultipleValues() + { + // Arrange + var parameters = new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "a", "b" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.State), null, null); + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Null(result.Error.RedirectUri); + Assert.Null(result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_IfClientId_IsMissing() + { + // Arrange + var parameters = new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.ClientId), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Null(result.Error.RedirectUri); + Assert.Null(result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_IfMultipleClientIds_ArePresent() + { + // Arrange + var parameters = new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a", "b" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.ClientId), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Null(result.Error.RedirectUri); + Assert.Null(result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_IfClientIdValidation_Fails() + { + // Arrange + var parameters = new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidClientId("a"), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(validClientId: false); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Null(result.Error.RedirectUri); + Assert.Null(result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_IfMultipleRedirectUris_ArePresent() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "a", "b" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.RedirectUri), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Null(result.Error.RedirectUri); + Assert.Null(result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_RedirectUri_IsNotAbsolute() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "/callback" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidUriFormat("/callback"), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(validRedirectUri: false); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Null(result.Error.RedirectUri); + Assert.Null(result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_RedirectUris_ContainsFragment() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback#fragment" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidUriFormat("http://www.example.com/callback#fragment"), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(validRedirectUri: false); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Null(result.Error.RedirectUri); + Assert.Null(result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_RedirectUris_IsNotValid() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidRedirectUri("http://www.example.com/callback"), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(validRedirectUri: false); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Null(result.Error.RedirectUri); + Assert.Null(result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_ResponseTypeIsMissing() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.ResponseType), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.Query, result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_ResponseType_HasMultipleValues() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code", "token id_token" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.ResponseType), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.Query, result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_ResponseType_HasInvalidValues() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "invalid" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidParameterValue("invalid", OpenIdConnectParameterNames.ResponseType), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.Query, result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_ResponseType_ContainsOtherValuesAlongWithNone() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code none" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.ResponseTypeNoneNotAllowed(), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.Fragment, result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_ResponseMode_ContainsMultipleValues() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code" }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "query", "fragment" } + }; + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.ResponseMode), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.Query, result.Error.ResponseMode); + } + + [Theory] + [InlineData("code", "query")] + [InlineData("token", "fragment")] + [InlineData("id_token", "fragment")] + [InlineData("code id_token", "fragment")] + [InlineData("code token", "fragment")] + [InlineData("code token id_token", "fragment")] + public async Task FailsToCreateAuthorizationRequest_ResponseMode_ContainsInvalidValues(string responseType, string errorResponseMode) + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { responseType }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "invalid" } + }; + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidParameterValue("invalid", OpenIdConnectParameterNames.ResponseMode), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(errorResponseMode, result.Error.ResponseMode); + } + + [Theory] + [InlineData("token", "query")] + [InlineData("id_token", "query")] + [InlineData("code id_token", "query")] + [InlineData("code token", "query")] + [InlineData("code token id_token", "query")] + public async Task FailsToCreateAuthorizationRequest_ResponseModeAndResponseType_AreIncompatible(string responseType, string responseMode) + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { responseType }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { responseMode } + }; + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidResponseTypeModeCombination(responseType, responseMode), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.Query, result.Error.ResponseMode); + } + + [Theory] + [InlineData("token")] + [InlineData("id_token")] + [InlineData("token id_token")] + [InlineData("code token")] + [InlineData("code id_token")] + [InlineData("code token id_token")] + public async Task FailsToCreateAuthorizationRequest_NonceIsRequired_ForHybridAndImplicitFlows(string responseType) + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { responseType }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" } + }; + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.Nonce), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode); + } + + [Theory] + [InlineData("code")] + [InlineData("token")] + [InlineData("id_token")] + [InlineData("code token")] + [InlineData("code id_token")] + [InlineData("token id_token")] + [InlineData("code token id_token")] + public async Task FailsToCreateAuthorizationRequest_NonceFails_IfMultipleNoncesArePresent(string responseType) + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { responseType }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" }, + [OpenIdConnectParameterNames.Nonce] = new[] { "asdf", "qwert" } + }; + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.Nonce), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task FailsToCreateAuthorizationRequest_IfScope_IsMissingOrEmpty(string scope) + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code" }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" }, + }; + + if (scope != null) + { + parameters[OpenIdConnectParameterNames.Scope] = new[] { scope }; + } + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.Scope), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode); + } + + [Theory] + [InlineData("id_token")] + [InlineData("code id_token")] + [InlineData("token id_token")] + [InlineData("code id_token token")] + public async Task FailsToCreateAuthorizationRequest_IfRequestAsksForIdToken_ButOpenIdScopeIsMissing(string responseType) + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { responseType }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" }, + [OpenIdConnectParameterNames.Scope] = new[] { "offline_access" }, + [OpenIdConnectParameterNames.Nonce] = new[] { "nonce" } + }; + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.MissingOpenIdScope(), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_Scope_HasMultipleValues() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code" }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" }, + [OpenIdConnectParameterNames.Scope] = new[] { "openid", "profile" }, + }; + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.Scope), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_IfScopesResolver_DeterminesThereAreInvalidScopes() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code" }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" }, + [OpenIdConnectParameterNames.Scope] = new[] { "openid invalid" }, + }; + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidScope("openid"), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(validClientId: true, validRedirectUri: true, validScopes: false); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_Prompt_IncludesNoneAndOtherValues() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code" }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" }, + [OpenIdConnectParameterNames.Nonce] = new[] { "asdf" }, + [OpenIdConnectParameterNames.Scope] = new[] { " openid profile " }, + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.Prompt] = new[] { "none consent " } + }; + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.PromptNoneMustBeTheOnlyValue("none consent"), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode); + } + + [Fact] + public async Task FailsToCreateAuthorizationRequest_Prompt_IncludesUnknownValue() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code" }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" }, + [OpenIdConnectParameterNames.Nonce] = new[] { "asdf" }, + [OpenIdConnectParameterNames.Scope] = new[] { " openid profile " }, + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.Prompt] = new[] { "login consent select_account unknown" } + }; + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidPromptValue("unknown"), null, null); + expectedError.Message.State = "state"; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance); + Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri); + Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode); + } + + [Theory] + [InlineData("none")] + [InlineData("login")] + [InlineData("consent")] + [InlineData("select_account")] + [InlineData("login consent")] + [InlineData("login select_account")] + [InlineData("consent select_account")] + [InlineData("login consent select_account")] + public async Task SuccessfullyCreatesARequest_WithAnyValidCombinationOfPromptValues(string promptValues) + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code" }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" }, + [OpenIdConnectParameterNames.Nonce] = new[] { "asdf" }, + [OpenIdConnectParameterNames.Scope] = new[] { " openid profile " }, + [OpenIdConnectParameterNames.State] = new[] { "state" }, + [OpenIdConnectParameterNames.Prompt] = new[] { promptValues } + }; + + var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidPromptValue("unknown"), null, null); + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.True(result.IsValid); + } + + [Fact] + public async Task CreatesAnAuthorizationRequest_IfAllParameters_AreCorrect() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code" }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" }, + [OpenIdConnectParameterNames.Nonce] = new[] { "asdf" }, + [OpenIdConnectParameterNames.Scope] = new[] { " openid profile " }, + [OpenIdConnectParameterNames.State] = new[] { "state" }, + }; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.True(result.IsValid); + var request = result.Message; + Assert.NotNull(request); + Assert.Equal("a", request.ClientId); + Assert.Equal("http://www.example.com/callback", request.RedirectUri); + Assert.Equal("code", request.ResponseType); + Assert.Equal(OpenIdConnectResponseMode.FormPost, request.ResponseMode); + Assert.Equal("asdf", request.Nonce); + Assert.Equal("state", request.State); + Assert.Equal(new[] { ApplicationScope.OpenId, ApplicationScope.Profile }, result.RequestGrants.Scopes); + Assert.Equal(new[] { TokenTypes.AuthorizationCode }, result.RequestGrants.Tokens); + Assert.Equal(OpenIdConnectResponseMode.FormPost, result.RequestGrants.ResponseMode); + Assert.Equal("http://www.example.com/callback", result.RequestGrants.RedirectUri); + Assert.Empty(result.RequestGrants.Claims); + } + + [Fact] + public async Task CreateAuthorizationRequest_AsignsRegisteredRedirectUriIfMissing() + { + // Arrange + var parameters = + new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "a" }, + [OpenIdConnectParameterNames.ResponseType] = new[] { "code" }, + [OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" }, + [OpenIdConnectParameterNames.Nonce] = new[] { "asdf" }, + [OpenIdConnectParameterNames.Scope] = new[] { " openid profile " }, + [OpenIdConnectParameterNames.State] = new[] { "state" }, + }; + + var factory = CreateAuthorizationRequestFactory(); + + // Act + var result = await factory.CreateAuthorizationRequestAsync(parameters); + + // Assert + Assert.True(result.IsValid); + var request = result.Message; + Assert.NotNull(request); + Assert.Equal("a", request.ClientId); + Assert.Null(request.RedirectUri); + Assert.Equal("code", request.ResponseType); + Assert.Equal(OpenIdConnectResponseMode.FormPost, request.ResponseMode); + Assert.Equal("asdf", request.Nonce); + Assert.Equal("state", request.State); + Assert.Equal(new[] { ApplicationScope.OpenId, ApplicationScope.Profile }, result.RequestGrants.Scopes); + Assert.Equal(new[] { TokenTypes.AuthorizationCode }, result.RequestGrants.Tokens); + Assert.Equal(OpenIdConnectResponseMode.FormPost, result.RequestGrants.ResponseMode); + Assert.Equal("http://www.example.com/registered", result.RequestGrants.RedirectUri); + Assert.Empty(result.RequestGrants.Claims); + } + + private static IAuthorizationRequestFactory CreateAuthorizationRequestFactory(bool validClientId = true, bool validRedirectUri = true, bool validScopes = true) + { + var clientIdValidator = new Mock(); + clientIdValidator + .Setup(c => c.ValidateClientIdAsync(It.IsAny())) + .ReturnsAsync(validClientId); + + var redirectUriValidatorMock = new Mock(); + redirectUriValidatorMock + .Setup(m => m.ResolveRedirectUriAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((string clientId, string redirectUrl) => validRedirectUri ? + RedirectUriResolutionResult.Valid(redirectUrl ?? "http://www.example.com/registered") : + RedirectUriResolutionResult.Invalid(ProtocolErrorProvider.InvalidRedirectUri(redirectUrl))); + + var scopeValidatorMock = new Mock(); + scopeValidatorMock + .Setup(m => m.ResolveScopesAsync(It.IsAny(), It.IsAny>())) + .ReturnsAsync( + (string clientId, IEnumerable scopes) => validScopes ? + ScopeResolutionResult.Valid(scopes.Select(s => ApplicationScope.CanonicalScopes.TryGetValue(s, out var parsedScope) ? parsedScope : new ApplicationScope(clientId, s))) : + ScopeResolutionResult.Invalid(ProtocolErrorProvider.InvalidScope(scopes.First()))); + + return new AuthorizationRequestFactory( + clientIdValidator.Object, + redirectUriValidatorMock.Object, + scopeValidatorMock.Object, + Enumerable.Empty(), + new ProtocolErrorProvider()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizeIntegrationTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizeIntegrationTest.cs new file mode 100644 index 0000000000..246d13bc1b --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/AuthorizeIntegrationTest.cs @@ -0,0 +1,218 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity.Service.Claims; +using Microsoft.AspNetCore.Identity.Service.Core; +using Microsoft.AspNetCore.Identity.Service.Serialization; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class AuthorizeIntegrationTest + { + [Fact] + public async Task Spec_Code_Sample() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = QueryString.FromUriComponent(@"?response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=openid%20profile%20email&nonce=n-0S6_WzA2Mj&state=af0ifjsldkj"); + var requestParameters = httpContext.Request.Query.ToDictionary(kvp => kvp.Key, kvp => (string[])kvp.Value); + + var requestFactory = CreateRequestFactory(); + var tokenIssuer = GetTokenIssuer(); + + var user = CreateUser("user"); + var application = CreateApplication("s6BhdRkqt"); + var responseFactory = CreateAuthorizationResponseFactory(); + + var queryExecutor = new QueryResponseGenerator(); + + // Act + var result = await requestFactory.CreateAuthorizationRequestAsync(requestParameters); + var authorization = result.Message; + + var tokenContext = result.CreateTokenGeneratingContext(user, application); + + await tokenIssuer.IssueTokensAsync(tokenContext); + + var response = await responseFactory.CreateAuthorizationResponseAsync(tokenContext); + + queryExecutor.GenerateResponse(httpContext, response.RedirectUri, response.Message.Parameters); + + // Assert + Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); + Assert.NotNull(httpContext.Response.Headers[HeaderNames.Location]); + var uri = new Uri(httpContext.Response.Headers[HeaderNames.Location]); + + Assert.False(string.IsNullOrEmpty(uri.Query)); + var parameters = QueryHelpers.ParseQuery(uri.Query); + + Assert.Equal(2, parameters.Count); + var idTokenKvp = Assert.Single(parameters, kvp => kvp.Key == "code"); + var stateKvp = Assert.Single(parameters, kvp => kvp.Key == "state"); + Assert.Equal("af0ifjsldkj", stateKvp.Value); + } + + [Fact] + public async Task Spec_IdToken_Sample() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = QueryString.FromUriComponent("?response_type=id_token&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&scope=openid%20profile%20email&nonce=n-0S6_WzA2Mj&state=af0ifjsldkj"); + var requestParameters = httpContext.Request.Query.ToDictionary(kvp => kvp.Key, kvp => (string[])kvp.Value); + + var requestFactory = CreateRequestFactory(); + var tokenIssuer = GetTokenIssuer(); + var fragmentExecutor = new FragmentResponseGenerator(UrlEncoder.Default); + + var user = CreateUser("248289761001"); + var application = CreateApplication("s6BhdRkqt"); + var responseFactory = CreateAuthorizationResponseFactory(); + + // Act + var result = await requestFactory.CreateAuthorizationRequestAsync(requestParameters); + + var tokenContext = result.CreateTokenGeneratingContext(user, application); + + await tokenIssuer.IssueTokensAsync(tokenContext); + + var response = await responseFactory.CreateAuthorizationResponseAsync(tokenContext); + + fragmentExecutor.GenerateResponse(httpContext, response.RedirectUri, response.Message.Parameters); + + // Assert + Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); + Assert.NotNull(httpContext.Response.Headers[HeaderNames.Location]); + var uri = new Uri(httpContext.Response.Headers[HeaderNames.Location]); + + Assert.False(string.IsNullOrEmpty(uri.Fragment)); + var parameters = QueryHelpers.ParseQuery(uri.Fragment.Substring(1)); + + Assert.Equal(2, parameters.Count); + var idTokenKvp = Assert.Single(parameters, kvp => kvp.Key == "id_token"); + var stateKvp = Assert.Single(parameters, kvp => kvp.Key == "state"); + Assert.Equal("af0ifjsldkj", stateKvp.Value); + } + + private static ClaimsPrincipal CreateApplication(string clientId) => + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, clientId) })); + + private static ClaimsPrincipal CreateUser(string userName) => + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userName) })); + + private static TokenManager GetTokenIssuer() + { + var options = CreateOptions(); + var claimsManager = GetClaimsManager(options); + + var protector = new EphemeralDataProtectionProvider(new LoggerFactory()).CreateProtector("test"); + var codeSerializer = new TokenDataSerializer(options, ArrayPool.Shared); + var codeDataFormat = new SecureDataFormat(codeSerializer, protector); + var refreshTokenSerializer = new TokenDataSerializer(options, ArrayPool.Shared); + var refreshTokenDataFormat = new SecureDataFormat(refreshTokenSerializer, protector); + + var timeStampManager = new TimeStampManager(); + var credentialsPolicy = GetCredentialsPolicy(options, timeStampManager); + var codeIssuer = new AuthorizationCodeIssuer(claimsManager, codeDataFormat, new ProtocolErrorProvider()); + var accessTokenIssuer = new JwtAccessTokenIssuer(claimsManager, credentialsPolicy, new JwtSecurityTokenHandler(), options); + var idTokenIssuer = new JwtIdTokenIssuer(claimsManager, credentialsPolicy, new JwtSecurityTokenHandler(), options); + var refreshTokenIssuer = new RefreshTokenIssuer(claimsManager, refreshTokenDataFormat); + + return new TokenManager( + codeIssuer, + accessTokenIssuer, + idTokenIssuer, + refreshTokenIssuer, + new ProtocolErrorProvider()); + } + + private static DefaultSigningCredentialsPolicyProvider GetCredentialsPolicy(IOptionsSnapshot options, TimeStampManager timeStampManager) => + new DefaultSigningCredentialsPolicyProvider( + new List { + new DefaultSigningCredentialsSource(options, timeStampManager) + }, + timeStampManager, + new HostingEnvironment()); + + private static ITokenClaimsManager GetClaimsManager( + IOptions options) + { + return new DefaultTokenClaimsManager( + new List{ + new DefaultTokenClaimsProvider(options), + new GrantedTokensTokenClaimsProvider(), + new NonceTokenClaimsProvider(), + new ScopesTokenClaimsProvider(), + new TimestampsTokenClaimsProvider(new TimeStampManager(),options), + new TokenHashTokenClaimsProvider(new TokenHasher()) + }); + } + + private static IOptionsSnapshot CreateOptions() + { + var identityServiceOptions = new IdentityServiceOptions(); + var optionsSetup = new IdentityServiceOptionsDefaultSetup(); + optionsSetup.Configure(identityServiceOptions); + + identityServiceOptions.SigningKeys.Add(new SigningCredentials(CryptoUtilities.CreateTestKey(), "RS256")); + identityServiceOptions.Issuer = "http://server.example.com"; + identityServiceOptions.IdTokenOptions.UserClaims.AddSingle("sub", ClaimTypes.NameIdentifier); + + var mock = new Mock>(); + mock.Setup(m => m.Get(It.IsAny())).Returns(identityServiceOptions); + mock.Setup(m => m.Value).Returns(identityServiceOptions); + + return mock.Object; + } + + private IAuthorizationRequestFactory CreateRequestFactory() + { + var clientIdValidatorMock = new Mock(); + clientIdValidatorMock + .Setup(m => m.ValidateClientIdAsync(It.IsAny())) + .ReturnsAsync(true); + + var redirectUriValidatorMock = new Mock(); + redirectUriValidatorMock + .Setup(m => m.ResolveRedirectUriAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((string clientId, string redirectUrl) => RedirectUriResolutionResult.Valid(redirectUrl)); + + var scopeValidatorMock = new Mock(); + scopeValidatorMock + .Setup(m => m.ResolveScopesAsync(It.IsAny(), It.IsAny>())) + .ReturnsAsync( + (string clientId, IEnumerable scopes) => + ScopeResolutionResult.Valid(scopes.Select(s => ApplicationScope.CanonicalScopes.TryGetValue(s, out var parsedScope) ? parsedScope : new ApplicationScope(clientId, s)))); + + return new AuthorizationRequestFactory( + clientIdValidatorMock.Object, + redirectUriValidatorMock.Object, + scopeValidatorMock.Object, + Enumerable.Empty(), + new ProtocolErrorProvider()); + } + + private static DefaultAuthorizationResponseFactory CreateAuthorizationResponseFactory() => + new DefaultAuthorizationResponseFactory(new IAuthorizationResponseParameterProvider[] { + new DefaultAuthorizationResponseParameterProvider(new TimeStampManager()) + }); + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/DefaultTokenClaimsManagerTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/DefaultTokenClaimsManagerTest.cs new file mode 100644 index 0000000000..5d62b43621 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/DefaultTokenClaimsManagerTest.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Service.Claims; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultTokenClaimsManagerTest + { + [Fact] + public async Task DefaultTokenClaimsManager_CallsProviders_InAscendingOrderAsync() + { + // Arrange + var context = new TokenGeneratingContext(new ClaimsPrincipal(), new ClaimsPrincipal(), new OpenIdConnectMessage(), new RequestGrants()); + var resultsList = new List(); + + var firstProvider = new Mock(); + firstProvider.SetupGet(p => p.Order).Returns(100); + firstProvider.Setup(p => p.OnGeneratingClaims(It.IsAny())) + .Callback(() => resultsList.Add("first")) + .Returns(Task.CompletedTask); + + var secondProvider = new Mock(); + secondProvider.SetupGet(p => p.Order).Returns(101); + secondProvider.Setup(p => p.OnGeneratingClaims(It.IsAny())) + .Callback(() => resultsList.Add("second")) + .Returns(Task.CompletedTask); + + var manager = new DefaultTokenClaimsManager(new[] { secondProvider.Object, firstProvider.Object }); + + // Act + await manager.CreateClaimsAsync(context); + + // Assert + Assert.Equal(new List { "first", "second" }, resultsList); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/DefaultTokenClaimsProviderTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/DefaultTokenClaimsProviderTest.cs new file mode 100644 index 0000000000..5ffc1555e6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/DefaultTokenClaimsProviderTest.cs @@ -0,0 +1,213 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class DefaultTokenClaimsProviderTest + { + [Theory] + [InlineData(TokenTypes.AuthorizationCode)] + [InlineData(TokenTypes.AccessToken)] + [InlineData(TokenTypes.IdToken)] + [InlineData(TokenTypes.RefreshToken)] + public async Task OnGeneratingClaims_AddsTokenIdForAllTokens(string tokenType) + { + // Arrange + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage(), + new RequestGrants()); + + var options = new IdentityServiceOptions() + { + Issuer = "http://www.example.com/Identity" + }; + var claimsProvider = new DefaultTokenClaimsProvider(Options.Create(options)); + + context.InitializeForToken(tokenType); + + // Act + await claimsProvider.OnGeneratingClaims(context); + + // Assert + Assert.Single( + context.CurrentClaims, + c => c.Type.Equals(IdentityServiceClaimTypes.TokenUniqueId, StringComparison.Ordinal)); + } + + [Theory] + [InlineData(TokenTypes.AccessToken)] + [InlineData(TokenTypes.IdToken)] + public async Task OnGeneratingClaims_AddsIssuerForAccessTokenAndIdToken(string tokenType) + { + // Arrange + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage(), + new RequestGrants()); + + var options = new IdentityServiceOptions() + { + Issuer = "http://www.example.com/Identity" + }; + var claimsProvider = new DefaultTokenClaimsProvider(Options.Create(options)); + + context.InitializeForToken(tokenType); + + // Act + await claimsProvider.OnGeneratingClaims(context); + + // Assert + Assert.Single( + context.CurrentClaims, + c => c.Type.Equals(IdentityServiceClaimTypes.Issuer, StringComparison.Ordinal)); + } + + [Fact] + public async Task OnGeneratingClaims_AddsRedirectUriIfPresentOnTheRequest() + { + // Arrange + var expectedRedirectUri = "http://wwww.example.com/callback"; + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() { RedirectUri = expectedRedirectUri }, + new RequestGrants()); + + var options = new IdentityServiceOptions() + { + Issuer = "http://www.example.com/Identity" + }; + var claimsProvider = new DefaultTokenClaimsProvider(Options.Create(options)); + + context.InitializeForToken(TokenTypes.AuthorizationCode); + + // Act + await claimsProvider.OnGeneratingClaims(context); + + // Assert + Assert.Single( + context.CurrentClaims, + c => c.Type.Equals(IdentityServiceClaimTypes.RedirectUri, StringComparison.Ordinal) && + c.Value.Equals(expectedRedirectUri)); + } + + [Theory] + [InlineData(TokenTypes.AuthorizationCode)] + [InlineData(TokenTypes.AccessToken)] + [InlineData(TokenTypes.IdToken)] + [InlineData(TokenTypes.RefreshToken)] + public async Task OnGeneratingClaims_MapsClaimsFromUsersApplicationsAndAmbient(string tokenType) + { + // Arrange + var expectedClaims = new List + { + new Claim("user-single","us"), + new Claim("user-single-claim","usa"), + new Claim("user-multiple","um1"), + new Claim("user-multiple","um2"), + new Claim("user-multiple-claim","uma1"), + new Claim("user-multiple-claim","uma2"), + new Claim("application-single","as"), + new Claim("application-single-claim","asa"), + new Claim("application-multiple","am1"), + new Claim("application-multiple","am2"), + new Claim("application-multiple-claim","ama1"), + new Claim("application-multiple-claim","ama2"), + new Claim("context-single", "cs"), + new Claim("context-single-claim", "csa"), + new Claim("context-multiple", "cm1"), + new Claim("context-multiple", "cm2"), + new Claim("context-multiple-claim", "cma1"), + new Claim("context-multiple-claim", "cma2"), + }; + + var user = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim("user-single","us"), + new Claim("user-single-aliased","usa"), + new Claim("user-multiple","um1"), + new Claim("user-multiple","um2"), + new Claim("user-multiple-aliased","uma1"), + new Claim("user-multiple-aliased","uma2"), + })); + + var application = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim("application-single","as"), + new Claim("application-single-aliased","asa"), + new Claim("application-multiple","am1"), + new Claim("application-multiple","am2"), + new Claim("application-multiple-aliased","ama1"), + new Claim("application-multiple-aliased","ama2"), + })); + + var context = new TokenGeneratingContext( + user, + application, + new OpenIdConnectMessage(), + new RequestGrants()); + + context.AmbientClaims.Add(new Claim("context-single", "cs")); + context.AmbientClaims.Add(new Claim("context-single-aliased", "csa")); + context.AmbientClaims.Add(new Claim("context-multiple", "cm1")); + context.AmbientClaims.Add(new Claim("context-multiple", "cm2")); + context.AmbientClaims.Add(new Claim("context-multiple-aliased", "cma1")); + context.AmbientClaims.Add(new Claim("context-multiple-aliased", "cma2")); + + var options = new IdentityServiceOptions() + { + Issuer = "http://www.example.com/Identity" + }; + + CreateTestMapping(options.AuthorizationCodeOptions); + CreateTestMapping(options.AccessTokenOptions); + CreateTestMapping(options.IdTokenOptions); + CreateTestMapping(options.RefreshTokenOptions); + + var claimsProvider = new DefaultTokenClaimsProvider(Options.Create(options)); + + context.InitializeForToken(tokenType); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims.Where(c => + c.Type != IdentityServiceClaimTypes.Issuer && + c.Type != IdentityServiceClaimTypes.TokenUniqueId).ToList(); + + // Assert + Assert.Equal(expectedClaims.Count, claims.Count); + foreach (var claim in expectedClaims) + { + Assert.Contains(claims, c => c.Type.Equals(claim.Type) && c.Value.Equals(claim.Value)); + } + } + + private void CreateTestMapping(TokenOptions tokenOptions) + { + tokenOptions.ContextClaims.AddSingle("context-single"); + tokenOptions.ContextClaims.AddSingle("context-single-claim", "context-single-aliased"); + tokenOptions.ContextClaims.Add(new TokenValueDescriptor("context-multiple", TokenValueCardinality.Many)); + tokenOptions.ContextClaims.Add(new TokenValueDescriptor("context-multiple-claim", "context-multiple-aliased", TokenValueCardinality.Many)); + tokenOptions.UserClaims.AddSingle("user-single"); + tokenOptions.UserClaims.AddSingle("user-single-claim", "user-single-aliased"); + tokenOptions.UserClaims.Add(new TokenValueDescriptor("user-multiple", TokenValueCardinality.Many)); + tokenOptions.UserClaims.Add(new TokenValueDescriptor("user-multiple-claim", "user-multiple-aliased", TokenValueCardinality.Many)); + tokenOptions.ApplicationClaims.AddSingle("application-single"); + tokenOptions.ApplicationClaims.AddSingle("application-single-claim", "application-single-aliased"); + tokenOptions.ApplicationClaims.Add(new TokenValueDescriptor("application-multiple", TokenValueCardinality.Many)); + tokenOptions.ApplicationClaims.Add(new TokenValueDescriptor("application-multiple-claim", "application-multiple-aliased", TokenValueCardinality.Many)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/GrantedTokensTokenClaimsProviderTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/GrantedTokensTokenClaimsProviderTest.cs new file mode 100644 index 0000000000..c0010fa715 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/GrantedTokensTokenClaimsProviderTest.cs @@ -0,0 +1,101 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class GrantedTokensTokenClaimsProviderTest + { + [Theory] + [InlineData("openid", "id_token")] + [InlineData("offline_access", "refresh_token")] + [InlineData("custom", "access_token")] + [InlineData("openid offline_access", "id_token refresh_token")] + [InlineData("openid custom", "id_token access_token")] + [InlineData("offline_access custom", "refresh_token access_token")] + [InlineData("openid offline_access custom", "id_token refresh_token access_token")] + public async Task OnGeneratingClaims_AddsGrantedTokensForAuthorizationCode( + string scopes, + string tokens) + { + // Arrange + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage(), + new RequestGrants() + { + Scopes = scopes.Split(' ').Select(CreateScope).ToList() + }); + + var expectedTokens = tokens.Split(' ').OrderBy(t => t).ToArray(); + + var claimsProvider = new GrantedTokensTokenClaimsProvider(); + + context.InitializeForToken(TokenTypes.AuthorizationCode); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var granted = context.CurrentClaims + .Where(c => c.Type.Equals(IdentityServiceClaimTypes.GrantedToken)) + .OrderBy(c => c.Value) + .Select(c => c.Value) + .ToArray(); + + // Assert + Assert.Equal(expectedTokens, granted); + } + + [Fact] + public async Task OnGeneratingClaims_AddsGrantedTokensForRefreshToken() + { + // Arrange + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage(), + new RequestGrants() + { + Tokens = new List + { + TokenTypes.AccessToken, + TokenTypes.IdToken, + TokenTypes.RefreshToken + } + }); + + var expectedTokens = new[] + { + TokenTypes.AccessToken, + TokenTypes.IdToken, + TokenTypes.RefreshToken + }; + + var claimsProvider = new GrantedTokensTokenClaimsProvider(); + + context.InitializeForToken(TokenTypes.RefreshToken); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var granted = context.CurrentClaims + .Where(c => c.Type.Equals(IdentityServiceClaimTypes.GrantedToken)) + .OrderBy(c => c.Value) + .Select(c => c.Value) + .ToArray(); + + // Assert + Assert.Equal(expectedTokens, granted); + } + + private static ApplicationScope CreateScope(string scope) + { + return ApplicationScope.CanonicalScopes.TryGetValue(scope, out var canonical) ? canonical : new ApplicationScope("clientId", scope); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/NonceTokenClaimsProviderTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/NonceTokenClaimsProviderTest.cs new file mode 100644 index 0000000000..5071d767cb --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/NonceTokenClaimsProviderTest.cs @@ -0,0 +1,147 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Service; +using Microsoft.AspNetCore.Identity.Service.Claims; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Claims +{ + public class NonceTokenClaimsProviderTest + { + [Theory] + [InlineData(TokenTypes.IdToken)] + [InlineData(TokenTypes.AccessToken)] + [InlineData(TokenTypes.AuthorizationCode)] + public async Task OnGeneratingClaims_AddsNonceToCodeAccessAndIdToken_WhenPresentInTheRequest(string tokenType) + { + // Arrange + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + Nonce = "nonce-value", + RequestType = OpenIdConnectRequestType.Authentication + }, + new RequestGrants() + { + Claims = new List { new Claim(IdentityServiceClaimTypes.Nonce, "invalid-nonce") } + }); + + var claimsProvider = new NonceTokenClaimsProvider(); + + context.InitializeForToken(tokenType); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Nonce) && c.Value.Equals("nonce-value")); + } + + [Theory] + [InlineData(TokenTypes.IdToken)] + [InlineData(TokenTypes.AccessToken)] + [InlineData(TokenTypes.AuthorizationCode)] + public async Task OnGeneratingClaims_DoesNotAddNonce_WhenNotPresentInTheRequest(string tokenType) + { + // Arrange + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + RequestType = OpenIdConnectRequestType.Authentication + }, + new RequestGrants() + { + // Makes sure we don't add the nonce in an authorization request + // even if for some reason ends up in the claims grant (which is not + // used in authentication). + Claims = new List { new Claim(IdentityServiceClaimTypes.Nonce, "nonce-value") } + }); + + var claimsProvider = new NonceTokenClaimsProvider(); + + context.InitializeForToken(tokenType); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + Assert.DoesNotContain(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Nonce)); + } + + [Theory] + [InlineData(TokenTypes.IdToken)] + [InlineData(TokenTypes.AccessToken)] + [InlineData(TokenTypes.AuthorizationCode)] + public async Task OnGeneratingClaims_AddsNonce_WhenPresentInTheGrantClaimsOfATokenRequest(string tokenType) + { + // Arrange + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + RequestType = OpenIdConnectRequestType.Token, + // Makes sure we ignore the value from the request + // for non authorization requests even when its present. + Nonce = "invalid-value" + }, + new RequestGrants() + { + Claims = new List { new Claim(IdentityServiceClaimTypes.Nonce, "nonce-value") } + }); + + var claimsProvider = new NonceTokenClaimsProvider(); + + context.InitializeForToken(tokenType); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Nonce) && c.Value.Equals("nonce-value")); + } + + [Theory] + [InlineData(TokenTypes.IdToken)] + [InlineData(TokenTypes.AccessToken)] + [InlineData(TokenTypes.AuthorizationCode)] + public async Task OnGeneratingClaims_DoesNotAddNonce_WhenNotPresentInTheGrantClaimsOfATokenRequest(string tokenType) + { + // Arrange + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + RequestType = OpenIdConnectRequestType.Token, + // Makes sure we ignore the value from the request + // for non authorization requests even when its present. + Nonce = "invalid-value" + }, + new RequestGrants()); + + var claimsProvider = new NonceTokenClaimsProvider(); + + context.InitializeForToken(tokenType); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + Assert.DoesNotContain(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Nonce)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/ScopeTokenClaimsProviderTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/ScopeTokenClaimsProviderTest.cs new file mode 100644 index 0000000000..072ede05a6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/ScopeTokenClaimsProviderTest.cs @@ -0,0 +1,212 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class ScopeTokenClaimsProviderTest + { + [Fact] + public async Task OnGeneratingClaims_AddsAllScopesToAuthorizationCode() + { + // Arrange + var applicationScopes = new List { ApplicationScope.OpenId, new ApplicationScope("resourceId", "custom") }; + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + RequestType = OpenIdConnectRequestType.Authentication + }, + new RequestGrants() + { + Scopes = applicationScopes.ToList() + }); + + var claimsProvider = new ScopesTokenClaimsProvider(); + + context.InitializeForToken(TokenTypes.AuthorizationCode); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Scope) && c.Value.Equals("openid custom")); + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Resource) && c.Value.Equals("resourceId")); + } + + [Fact] + public async Task OnGeneratingClaims_AddsCustomScopesFromRequest_ToAccessTokenOnAuthorization() + { + // Arrange + var applicationScopes = new List { ApplicationScope.OpenId, new ApplicationScope("resourceId", "custom") }; + var expectedResourceValue = applicationScopes.FirstOrDefault(s => s.ClientId != null)?.ClientId; + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + ClientId = "clientId" + }, + new RequestGrants() + { + Scopes = applicationScopes.ToList() + }); + + var claimsProvider = new ScopesTokenClaimsProvider(); + + context.InitializeForToken(TokenTypes.AccessToken); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Scope) && c.Value.Equals("custom")); + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Audience) && c.Value.Equals("resourceId")); + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.AuthorizedParty) && c.Value.Equals("clientId")); + } + + [Fact] + public async Task OnGeneratingClaims_AddsCustomScopesFromRequest_ToAccessTokenOnTokenRequest() + { + // Arrange + var applicationScopes = new List { + ApplicationScope.OpenId, + new ApplicationScope("resourceId", "custom"), + new ApplicationScope("resourceId","custom2") + }; + + var expectedResourceValue = applicationScopes.FirstOrDefault(s => s.ClientId != null)?.ClientId; + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + ClientId = "clientId" + }, + new RequestGrants() + { + Scopes = applicationScopes.ToList(), + // This is just to prove that we always pick the values for the scope related claims + // from the set of granted scopes (for access tokens). + Claims = new List + { + new Claim(IdentityServiceClaimTypes.Resource, "ridClaim"), + new Claim(IdentityServiceClaimTypes.Scope, "custom3") + } + }); + + var claimsProvider = new ScopesTokenClaimsProvider(); + + context.InitializeForToken(TokenTypes.AccessToken); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Scope) && c.Value.Equals("custom custom2")); + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Audience) && c.Value.Equals("resourceId")); + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.AuthorizedParty) && c.Value.Equals("clientId")); + } + + [Fact] + public async Task OnGeneratingClaims_AddsAllScopesFromGrantClaims_ToRefreshTokenOnTokenRequest() + { + // Arrange + // This is just to prove that we always transfer the scope and resource claims from the grant + // into the refresh token untouched. + var applicationScopes = new List + { + ApplicationScope.OpenId, + new ApplicationScope("resourceId", "custom"), + new ApplicationScope("resourceId","custom2") + }; + + var expectedResourceValue = applicationScopes.FirstOrDefault(s => s.ClientId != null)?.ClientId; + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + ClientId = "clientId" + }, + new RequestGrants() + { + Scopes = applicationScopes.ToList(), + Claims = new List + { + new Claim(IdentityServiceClaimTypes.Resource, "ridClaim"), + new Claim(IdentityServiceClaimTypes.Scope, "openid custom3") + } + }); + + var claimsProvider = new ScopesTokenClaimsProvider(); + + context.InitializeForToken(TokenTypes.RefreshToken); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Scope) && c.Value.Equals("openid custom3")); + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Resource) && c.Value.Equals("ridClaim")); + } + + [Fact] + public async Task OnGeneratingClaims_DoesNotAddResourceClaim_ToRefreshTokenIfNotPresent() + { + // Arrange + // This is just to prove that we always transfer the scope and resource claims from the grant + // into the refresh token untouched. + var applicationScopes = new List + { + ApplicationScope.OpenId, + new ApplicationScope("resourceId", "custom"), + new ApplicationScope("resourceId","custom2") + }; + + var expectedResourceValue = applicationScopes.FirstOrDefault(s => s.ClientId != null)?.ClientId; + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + ClientId = "clientId" + }, + new RequestGrants() + { + Scopes = applicationScopes.ToList(), + Claims = new List + { + new Claim(IdentityServiceClaimTypes.Scope, "openid") + } + }); + + var claimsProvider = new ScopesTokenClaimsProvider(); + + context.InitializeForToken(TokenTypes.RefreshToken); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Scope) && c.Value.Equals("openid")); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/TimeStampTokenClaimsProviderTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/TimeStampTokenClaimsProviderTest.cs new file mode 100644 index 0000000000..04b491511b --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/TimeStampTokenClaimsProviderTest.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Service; +using Microsoft.AspNetCore.Identity.Service.Claims; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Claims +{ + public class TimeStampTokenClaimsProviderTest + { + public static TheoryData ExpectedTimeStampsData => + new TheoryData + { + { TokenTypes.AuthorizationCode, "946684800", "946681200","946688400" }, + { TokenTypes.AccessToken, "946684800", "946677600", "946692000" }, + { TokenTypes.IdToken, "946684800", "946674000", "946695600" }, + { TokenTypes.RefreshToken, "946684800", "946670400", "946699200" }, + }; + + [Theory] + [MemberData(nameof(ExpectedTimeStampsData))] + public async Task OnGeneratingClaims_AddsIssuedAtNotBeforeAndExpires_ForAllTokenTypes( + string tokenType, + string issuedAt, + string notBefore, + string expires) + { + // Arrange + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage { }, + new RequestGrants { }); + + // Reference time + var reference = new DateTimeOffset(2000, 01, 01, 0, 0, 0, TimeSpan.Zero); + + var timestampManager = new TestTimeStampManager(reference); + var options = new IdentityServiceOptions(); + SetTimeStampOptions(options.AuthorizationCodeOptions, 1); + SetTimeStampOptions(options.AccessTokenOptions, 2); + SetTimeStampOptions(options.IdTokenOptions, 3); + SetTimeStampOptions(options.RefreshTokenOptions, 4); + + var claimsProvider = new TimestampsTokenClaimsProvider(timestampManager, Options.Create(options)); + context.InitializeForToken(tokenType); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.IssuedAt) && c.Value.Equals(issuedAt)); + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.NotBefore) && c.Value.Equals(notBefore)); + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.Expires) && c.Value.Equals(expires)); + } + + private void SetTimeStampOptions(TokenOptions tokenOptions, int hours) + { + tokenOptions.NotValidAfter = TimeSpan.FromHours(hours); + tokenOptions.NotValidBefore = TimeSpan.FromHours(-hours); + } + + private class TestTimeStampManager : TimeStampManager + { + private readonly DateTimeOffset _reference; + + public TestTimeStampManager(DateTimeOffset reference) + { + _reference = reference; + } + + public override DateTimeOffset GetCurrentTimeStampUtc() + { + return _reference; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/TokenHashTokenClaimsProviderTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/TokenHashTokenClaimsProviderTest.cs new file mode 100644 index 0000000000..ad8d9a80bb --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Claims/TokenHashTokenClaimsProviderTest.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service.Claims +{ + public class TokenHashTokenClaimsProviderTest + { + [Theory] + [InlineData("access_token", "code")] + [InlineData("access_token", null)] + [InlineData(null, "code")] + [InlineData(null, null)] + public async Task OnGeneratingClaims_AddsAtHashAndCHashClaimsWhenAvailable( + string accessToken, + string code) + { + // Arrange + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage { }, + new RequestGrants { }); + + if (accessToken != null) + { + context.InitializeForToken(TokenTypes.AccessToken); + context.AddToken(new TokenResult(new TestToken(TokenTypes.AccessToken), accessToken)); + } + if (code != null) + { + context.InitializeForToken(TokenTypes.AuthorizationCode); + context.AddToken(new TokenResult(new TestToken(TokenTypes.AuthorizationCode), code)); + } + + // Reference time + var hasher = new Mock(); + hasher.Setup(h => h.HashToken("access_token", It.IsAny())) + .Returns("access_token_hash"); + hasher.Setup(h => h.HashToken("code", It.IsAny())) + .Returns("code_hash"); + + var claimsProvider = new TokenHashTokenClaimsProvider(hasher.Object); + context.InitializeForToken(TokenTypes.IdToken); + + // Act + await claimsProvider.OnGeneratingClaims(context); + var claims = context.CurrentClaims; + + // Assert + if (accessToken != null) + { + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.AccessTokenHash) && c.Value.Equals("access_token_hash")); + } + else + { + Assert.DoesNotContain(claims, c => c.Type.Equals(IdentityServiceClaimTypes.AccessTokenHash)); + } + + if (code != null) + { + Assert.Single(claims, c => c.Type.Equals(IdentityServiceClaimTypes.CodeHash) && c.Value.Equals("code_hash")); + } + else + { + Assert.DoesNotContain(claims, c => c.Type.Equals(IdentityServiceClaimTypes.CodeHash)); + } + } + + private class TestToken : Token + { + private readonly string _kind; + + public TestToken(string kind) + : base(new List + { + new Claim(IdentityServiceClaimTypes.TokenUniqueId, "id"), + new Claim(IdentityServiceClaimTypes.IssuedAt, "issuedAt"), + new Claim(IdentityServiceClaimTypes.Expires, "expires"), + new Claim(IdentityServiceClaimTypes.NotBefore, "notBefore"), + }) + { + _kind = kind; + } + + public override string Kind => _kind; + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/CryptoUtilities.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/CryptoUtilities.cs new file mode 100644 index 0000000000..ea6f0e0037 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/CryptoUtilities.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Cryptography; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Identity.Service +{ + internal static class CryptoUtilities + { + internal static SecurityKey CreateTestKey(string id = "Test") + { + using (var rsa = RSA.Create(2048)) + { + SecurityKey key; + var parameters = rsa.ExportParameters(includePrivateParameters: true); + key = new RsaSecurityKey(parameters); + key.KeyId = id; + return key; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultAuthorizationResponseFactoryTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultAuthorizationResponseFactoryTest.cs new file mode 100644 index 0000000000..773224a903 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultAuthorizationResponseFactoryTest.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultAuthorizationResponseFactoryTest + { + [Fact] + public async Task CreateAuthorizationResponse_CallsParameterProvidersInOrder() + { + // Arrange + var context = new TokenGeneratingContext(new ClaimsPrincipal(), new ClaimsPrincipal(), new OpenIdConnectMessage(), new RequestGrants()); + var resultsList = new List(); + + var firstProvider = new Mock(); + firstProvider.SetupGet(p => p.Order).Returns(100); + firstProvider.Setup(p => p.AddParameters(It.IsAny(), It.IsAny())) + .Callback(() => resultsList.Add("first")) + .Returns(Task.CompletedTask); + + var secondProvider = new Mock(); + secondProvider.SetupGet(p => p.Order).Returns(101); + secondProvider.Setup(p => p.AddParameters(It.IsAny(), It.IsAny())) + .Callback(() => resultsList.Add("second")) + .Returns(Task.CompletedTask); + + var responseFactory = new DefaultAuthorizationResponseFactory(new[] { secondProvider.Object, firstProvider.Object }); + + // Act + var response = await responseFactory.CreateAuthorizationResponseAsync(context); + + // Assert + Assert.Equal(new List { "first", "second" }, resultsList); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultAuthorizationResponseParameterProviderTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultAuthorizationResponseParameterProviderTest.cs new file mode 100644 index 0000000000..8aafe34c5c --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultAuthorizationResponseParameterProviderTest.cs @@ -0,0 +1,135 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultAuthorizationResponseParameterProviderTest + { + [Fact] + public async Task AddParameters_AddsCodeToResponse_WhenCodeIsEmitted() + { + // Arrange + var provider = new DefaultAuthorizationResponseParameterProvider(new TimeStampManager()); + var response = new AuthorizationResponse() + { + Message = new OpenIdConnectMessage(), + RedirectUri = "http://www.example.com/callback", + ResponseMode = "query" + }; + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + State = "state" + }, + new RequestGrants()); + + context.InitializeForToken(TokenTypes.AuthorizationCode); + context.AddToken(new TokenResult(new TestToken(TokenTypes.AuthorizationCode), "serialized_authorization_code")); + + // Act + await provider.AddParameters(context, response); + + // Assert + Assert.Equal("state", response.Message.State); + Assert.Equal("serialized_authorization_code", response.Message.Code); + } + + [Fact] + public async Task AddParameters_AddsAccessTokenToResponse_WhenAccessTokenIsEmitted() + { + // Arrange + var provider = new DefaultAuthorizationResponseParameterProvider(new TimeStampManager()); + var response = new AuthorizationResponse() + { + Message = new OpenIdConnectMessage(), + RedirectUri = "http://www.example.com/callback", + ResponseMode = "query" + }; + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + State = "state" + }, + new RequestGrants() + { + Scopes = { ApplicationScope.OpenId, new ApplicationScope("resourceId", "read") } + }); + + context.InitializeForToken(TokenTypes.AccessToken); + context.AddToken(new TokenResult(new TestToken(TokenTypes.AccessToken), "serialized_access_token")); + + // Act + await provider.AddParameters(context, response); + + // Assert + Assert.Equal("state", response.Message.State); + Assert.Equal("serialized_access_token", response.Message.AccessToken); + Assert.Equal("3600", response.Message.ExpiresIn); + Assert.Equal("openid read", response.Message.Scope); + Assert.Equal("Bearer", response.Message.TokenType); + } + + [Fact] + public async Task AddParameters_AddsIdTokenToResponse_WhenIdTokenIsEmitted() + { + // Arrange + var provider = new DefaultAuthorizationResponseParameterProvider(new TimeStampManager()); + var response = new AuthorizationResponse() + { + Message = new OpenIdConnectMessage(), + RedirectUri = "http://www.example.com/callback", + ResponseMode = "query" + }; + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage() + { + State = "state" + }, + new RequestGrants()); + + context.InitializeForToken(TokenTypes.IdToken); + context.AddToken(new TokenResult(new TestToken(TokenTypes.IdToken), "serialized_id_token")); + + // Act + await provider.AddParameters(context, response); + + // Assert + Assert.Equal("state", response.Message.State); + Assert.Equal("serialized_id_token", response.Message.IdToken); + } + + public class TestToken : Token + { + private readonly string _kind; + + public TestToken(string kind) + : base(new List + { + new Claim(IdentityServiceClaimTypes.TokenUniqueId,"tuid"), + new Claim(IdentityServiceClaimTypes.Expires,"946688400"), + new Claim(IdentityServiceClaimTypes.IssuedAt,"946684800"), + new Claim(IdentityServiceClaimTypes.NotBefore,"946684800"), + }) + { + _kind = kind; + } + + public override string Kind => _kind; + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultSigningCredentialsPolicyProviderTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultSigningCredentialsPolicyProviderTest.cs new file mode 100644 index 0000000000..65e69364dd --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultSigningCredentialsPolicyProviderTest.cs @@ -0,0 +1,277 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultSigningCredentialsPolicyProviderTest + { + [Fact] + public async Task GetAllCredentialsAsync_GetsCredentialsFromAllSources() + { + // Arrange + var descriptors = new List() + { + new SigningCredentialsDescriptor( + CreateRsaCredentials(), + "RSA", + DateTimeOffset.Now+TimeSpan.FromHours(1), + DateTimeOffset.Now+TimeSpan.FromHours(2), + new Dictionary()), + new SigningCredentialsDescriptor( + CreateRsaCredentials(), + "RSA", + DateTimeOffset.Now, + DateTimeOffset.Now+TimeSpan.FromHours(1), + new Dictionary()), + }; + + var expected = descriptors.ToList(); + expected.Reverse(); + + var mockSource = new Mock(); + mockSource.Setup(scs => scs.GetCredentials()) + .ReturnsAsync(descriptors); + + var sources = new List() + { + mockSource.Object + }; + + var policyProvider = new DefaultSigningCredentialsPolicyProvider(sources, new TimeStampManager(), new HostingEnvironment()); + + // Act + var credentials = await policyProvider.GetAllCredentialsAsync(); + + // Assert + Assert.Equal(expected, credentials); + } + + [Fact] + public async Task GetAllCredentialsAsync_RetrievesTheCredentialsIfAllOfThemAreExpired() + { + // Arrange + var descriptors1 = new List() + { + new SigningCredentialsDescriptor( + CreateRsaCredentials("First"), + "RSA", + DateTimeOffset.Now-TimeSpan.FromHours(2), + DateTimeOffset.Now-TimeSpan.FromHours(1), + new Dictionary()) + }; + + var descriptors2 = new List() + { + new SigningCredentialsDescriptor( + CreateRsaCredentials("First"), + "RSA", + DateTimeOffset.Now-TimeSpan.FromHours(2), + DateTimeOffset.Now-TimeSpan.FromHours(1), + new Dictionary()), + new SigningCredentialsDescriptor( + CreateRsaCredentials("Second"), + "RSA", + DateTimeOffset.Now, + DateTimeOffset.Now+TimeSpan.FromHours(1), + new Dictionary()) + }; + + var expected = descriptors2.ToList(); + + var mockSource = new Mock(); + mockSource.SetupSequence(s => s.GetCredentials()) + .ReturnsAsync(descriptors1) + .ReturnsAsync(descriptors2); + + var sources = new List() + { + mockSource.Object + }; + + var policyProvider = new DefaultSigningCredentialsPolicyProvider(sources, new TimeStampManager(), new HostingEnvironment()); + + // Act + var credentials = await policyProvider.GetAllCredentialsAsync(); + credentials = await policyProvider.GetAllCredentialsAsync(); + + // Assert + Assert.Equal(expected, credentials); + } + + [Fact] + public async Task GetAllCredentialsAsync_RetrievesCredentialsInOrder() + { + // Arrange + var reference = DateTimeOffset.UtcNow; + + var descriptors = new List() + { + new SigningCredentialsDescriptor( + CreateRsaCredentials("Fourth"), + "RSA", + expires: reference+TimeSpan.FromHours(3), + notBefore: reference+TimeSpan.FromHours(1), + metadata: new Dictionary()), + new SigningCredentialsDescriptor( + CreateRsaCredentials("Third"), + "RSA", + expires: reference+TimeSpan.FromHours(2), + notBefore: reference+TimeSpan.FromHours(1), + metadata: new Dictionary()), + new SigningCredentialsDescriptor( + CreateRsaCredentials("Second"), + "RSA", + expires: reference+TimeSpan.FromHours(2), + notBefore: reference, + metadata: new Dictionary()), + new SigningCredentialsDescriptor( + CreateRsaCredentials("First"), + "RSA", + expires: reference+TimeSpan.FromHours(1), + notBefore: reference, + metadata: new Dictionary()) + }; + + var mockSource = new Mock(); + mockSource.Setup(s => s.GetCredentials()) + .ReturnsAsync(descriptors); + + var expected = descriptors.ToList(); + expected.Reverse(); + + var sources = new List() + { + mockSource.Object + }; + + var policyProvider = new DefaultSigningCredentialsPolicyProvider(sources, new TimeStampManager(), new HostingEnvironment()); + + // Act + var credentials = await policyProvider.GetAllCredentialsAsync(); + + // Assert + Assert.Equal(expected, credentials); + } + + [Fact] + public async Task GetSigningCredentialsAsync_RetrievesTheCredentialWithEarliestExpirationAndAllowedUsage() + { + // Arrange + var reference = DateTimeOffset.UtcNow; + var expected = new SigningCredentialsDescriptor( + CreateRsaCredentials("First"), + "RSA", + expires: reference + TimeSpan.FromHours(1), + notBefore: reference, + metadata: new Dictionary()); + + var descriptors = new List() + { + new SigningCredentialsDescriptor( + CreateRsaCredentials("Fourth"), + "RSA", + expires: reference+TimeSpan.FromHours(3), + notBefore: reference+TimeSpan.FromHours(1), + metadata: new Dictionary()), + new SigningCredentialsDescriptor( + CreateRsaCredentials("Third"), + "RSA", + expires: reference+TimeSpan.FromHours(2), + notBefore: reference+TimeSpan.FromHours(1), + metadata: new Dictionary()), + new SigningCredentialsDescriptor( + CreateRsaCredentials("Second"), + "RSA", + expires: reference+TimeSpan.FromHours(2), + notBefore: reference, + metadata: new Dictionary()), + expected + }; + + var mockSource = new Mock(); + mockSource.Setup(s => s.GetCredentials()) + .ReturnsAsync(descriptors); + + var sources = new List() + { + mockSource.Object + }; + + var policyProvider = new DefaultSigningCredentialsPolicyProvider(sources, new TimeStampManager(), new HostingEnvironment()); + + // Act + var signingCredential = await policyProvider.GetSigningCredentialsAsync(); + + // Assert + Assert.Equal(expected, signingCredential); + } + + [Fact] + public async Task GetSigningCredentialsAsync_SkipsExpiredCredentials() + { + // Arrange + var reference = DateTimeOffset.UtcNow; + var expired = new SigningCredentialsDescriptor( + CreateRsaCredentials("First"), + "RSA", + expires: reference - TimeSpan.FromHours(1), + notBefore: reference - TimeSpan.FromHours(2), + metadata: new Dictionary()); + + var expected = new SigningCredentialsDescriptor( + CreateRsaCredentials("Second"), + "RSA", + expires: reference + TimeSpan.FromHours(2), + notBefore: reference, + metadata: new Dictionary()); + + + var descriptors = new List() + { + new SigningCredentialsDescriptor( + CreateRsaCredentials("Fourth"), + "RSA", + expires: reference+TimeSpan.FromHours(3), + notBefore: reference+TimeSpan.FromHours(1), + metadata: new Dictionary()), + new SigningCredentialsDescriptor( + CreateRsaCredentials("Third"), + "RSA", + expires: reference+TimeSpan.FromHours(2), + notBefore: reference+TimeSpan.FromHours(1), + metadata: new Dictionary()), + expected, + expired + }; + + var mockSource = new Mock(); + mockSource.Setup(s => s.GetCredentials()) + .ReturnsAsync(descriptors); + + var sources = new List() + { + mockSource.Object + }; + + var policyProvider = new DefaultSigningCredentialsPolicyProvider(sources, new TimeStampManager(), new HostingEnvironment()); + + // Act + var signingCredential = await policyProvider.GetSigningCredentialsAsync(); + + // Assert + Assert.Equal(expected, signingCredential); + } + + private SigningCredentials CreateRsaCredentials(string id = "Test") => + new SigningCredentials(CryptoUtilities.CreateTestKey(id), "RSA"); + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultSigningCredentialsSourceTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultSigningCredentialsSourceTest.cs new file mode 100644 index 0000000000..2f7d1cd451 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultSigningCredentialsSourceTest.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Service.Core; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultSigningCredentialsSourceTest + { + [Fact] + public async Task GetCredentialsAsync_ReadsCredentialsFromOptions() + { + // Arrange + var reference = new DateTimeOffset(2000,01,01,00,00,00,TimeSpan.Zero); + + var identityServiceOptions = new IdentityServiceOptions(); + identityServiceOptions.SigningKeys.Add(new SigningCredentials(CryptoUtilities.CreateTestKey("RSAKey"), "RS256")); + identityServiceOptions.SigningKeys.Add(new SigningCredentials(new X509SecurityKey(GetCertificate(reference)), "RS256")); + var mock = new Mock>(); + mock.Setup(m => m.Value).Returns(identityServiceOptions); + mock.Setup(m => m.Get(It.IsAny())).Returns(identityServiceOptions); + var source = new DefaultSigningCredentialsSource(mock.Object, new TestTimeStampManager(reference)); + + // Act + var credentials = (await source.GetCredentials()).ToList(); + + // Assert + Assert.Equal(2, credentials.Count); + var rsaDescriptor = Assert.Single(credentials, c => c.Id == "RSAKey"); + Assert.Equal("RSA", rsaDescriptor.Algorithm); + Assert.Equal(reference, rsaDescriptor.NotBefore); + Assert.Equal(reference.AddDays(1), rsaDescriptor.Expires); + Assert.Equal(identityServiceOptions.SigningKeys[0], rsaDescriptor.Credentials); + Assert.True(rsaDescriptor.Metadata.ContainsKey("n")); + Assert.True(rsaDescriptor.Metadata.ContainsKey("e")); + + var certificateDescriptor = Assert.Single(credentials, c => c.Id != "RSAKey"); + Assert.Equal("RSA", certificateDescriptor.Algorithm); + Assert.Equal(reference, certificateDescriptor.NotBefore); + Assert.Equal(reference.AddHours(1), certificateDescriptor.Expires); + Assert.Equal(identityServiceOptions.SigningKeys[1], certificateDescriptor.Credentials); + Assert.True(certificateDescriptor.Metadata.ContainsKey("n")); + Assert.True(certificateDescriptor.Metadata.ContainsKey("e")); + } + + private X509Certificate2 GetCertificate(DateTimeOffset reference) + { + // We are base64urlencoding a certificate that we generated on the fly due to + // the fact that .NET Framework doesn't have the ability to generate self-signed + // certificates on the flight. + // The snippet below shows how to create a self-signed certificate in .NET Core 2.0 + // and Base65UrlEncode it so that it can be put in a string in code. + // If for any reason this test needs to change, just regenerate a base 64 representation + // of the certificate with the snippet below in .NET Core 2.0+ and se it on the rawCertificate. + string rawCertificate = "MIICqTCCAZGgAwIBAgIJAIl7w4Jsl-LCMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0wMDAxMDEwMDAwMDBaFw0wMDAxMDEwMTAwMDBaMBQxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIgVCyY9FxKU5N0VSctfnyewoqsRg9S2Bf-bJOT-bl5BwmXhuOdOP2AVP0DbqhvuylsQ9gbJOK5qnzHjm0BtISFDLyA-V-cjodhYRsssPvTx0b04whFLHLH6uBkeHWUqDuP0ziQ1Ujb0nrmlUJ5XqYYBi1kfflH0imwxVxCCagTt-N3FyBfaU1dxR5MqN2U3Pj4Mmt0-sDlNoNDJPptqakHSnGPpP4KjM0h5jWmmjpRT_bKQYkQ2llDkQI7h1VUeT0tfGrGi8JowL0jrgfHJOFQ8r-xz_0IcAK4BdkmMAVR4SXDlK9lZX89pQfu20LK118B3NHtHJAm41NLYXr0LDv8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAM09oxd6ehkPwqb8L9KKFKIFFdAggPICUkfeb68Ac_-DpAiUgtBE4vOGaIcO85ED5vreqNPImiCEczhJaAsTdn7fcloFKBafWsiuk9n1GnLTP69TOHg6-a4hdIJJhIJmA3qXOFcwvMU75hSm9tYzMmoQDPr09bVvfkH5WBaobr8iNpB9R8gSoe8NjIH57Q1RvRHWJ35lIuyNOtXbZKH-AvsAoeZaiVlSWI3xu5hq5deuDeQ-P8FnhyRNPRvp83fgou1N1WMZNK9T_RUSrxhqitUr8B7wUe2lRo0mcu1WHQ9_G6_5uV2LgpAHMM1p_JFyhE00o4JMnkeH3oFeYq_ml7Q"; + return new X509Certificate2(Base64UrlEncoder.DecodeBytes(rawCertificate)); + + // var distinguishedName = new X500DistinguishedName("CN=localhost"); + // var rsaKey = RSA.Create(2048); + // var request = new CertificateRequest( + // distinguishedName, rsaKey, HashAlgorithmName.SHA256); + // var certificate = request.CreateSelfSigned(reference, reference + TimeSpan.FromHours(1)); + // var encoded = Base64UrlEncoder.Encode(certificate.Export(X509ContentType.Cert)); + //return certificate; + } + + private class TestTimeStampManager : TimeStampManager + { + private readonly DateTimeOffset _reference; + + public TestTimeStampManager(DateTimeOffset reference) + { + _reference = reference; + } + public override DateTimeOffset GetCurrentTimeStampUtc() => _reference; + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultTokenResponseFactoryTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultTokenResponseFactoryTest.cs new file mode 100644 index 0000000000..d7354f5cd5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultTokenResponseFactoryTest.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultTokenResponseFactoryTest + { + [Fact] + public async Task CreateTokenResponse_CallsParameterProvidersInOrder() + { + // Arrange + var context = new TokenGeneratingContext(new ClaimsPrincipal(), new ClaimsPrincipal(), new OpenIdConnectMessage(), new RequestGrants()); + var resultsList = new List(); + + var firstProvider = new Mock(); + firstProvider.SetupGet(p => p.Order).Returns(100); + firstProvider.Setup(p => p.AddParameters(It.IsAny(), It.IsAny())) + .Callback(() => resultsList.Add("first")) + .Returns(Task.CompletedTask); + + var secondProvider = new Mock(); + secondProvider.SetupGet(p => p.Order).Returns(101); + secondProvider.Setup(p => p.AddParameters(It.IsAny(), It.IsAny())) + .Callback(() => resultsList.Add("second")) + .Returns(Task.CompletedTask); + + var responseFactory = new DefaultTokenResponseFactory(new[] { secondProvider.Object, firstProvider.Object }); + + // Act + var response = await responseFactory.CreateTokenResponseAsync(context); + + // Assert + Assert.Equal(new List { "first", "second" }, resultsList); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultTokenResponseParameterProviderTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultTokenResponseParameterProviderTest.cs new file mode 100644 index 0000000000..8fca061f51 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/DefaultTokenResponseParameterProviderTest.cs @@ -0,0 +1,117 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class DefaultTokenResponseParameterProviderTest + { + [Fact] + public async Task AddParameters_AddsIdTokenToResponse_WhenEmitted() + { + // Arrange + var provider = new DefaultTokenResponseParameterProvider(new TimeStampManager()); + var response = new OpenIdConnectMessage(); + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage(), + new RequestGrants()); + + context.InitializeForToken(TokenTypes.IdToken); + context.AddToken(new TokenResult(new TestToken(TokenTypes.IdToken), "serialized_id_token")); + + // Act + await provider.AddParameters(context, response); + + // Assert + Assert.Equal("serialized_id_token", response.IdToken); + Assert.True(response.Parameters.ContainsKey("id_token_expires_in")); + Assert.Equal("3600", response.Parameters["id_token_expires_in"]); + Assert.Equal("Bearer", response.TokenType); + } + + [Fact] + public async Task AddParameters_AddsAccessTokenToResponse_WhenAccessTokenIsEmitted() + { + // Arrange + var provider = new DefaultTokenResponseParameterProvider(new TimeStampManager()); + var response = new OpenIdConnectMessage(); + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage(), + new RequestGrants() { + Scopes = new[] { ApplicationScope.OpenId, new ApplicationScope("resourceId", "read") } + }); + + context.InitializeForToken(TokenTypes.AccessToken); + context.AddToken(new TokenResult(new TestToken(TokenTypes.AccessToken), "serialized_access_token")); + + // Act + await provider.AddParameters(context, response); + + // Assert + Assert.Equal("serialized_access_token", response.AccessToken); + Assert.Equal("3600", response.ExpiresIn); + Assert.True(response.Parameters.ContainsKey("expires_on")); + Assert.Equal("946688400", response.Parameters["expires_on"]); + Assert.True(response.Parameters.ContainsKey("not_before")); + Assert.Equal("946684800", response.Parameters["not_before"]); + Assert.Equal("resourceId", response.Resource); + Assert.Equal("Bearer", response.TokenType); + } + + [Fact] + public async Task AddParameters_AddsRefreshTokenToResponse_WhenRefreshTokenIsEmitted() + { + // Arrange + var provider = new DefaultTokenResponseParameterProvider(new TimeStampManager()); + var response = new OpenIdConnectMessage(); + + var context = new TokenGeneratingContext( + new ClaimsPrincipal(), + new ClaimsPrincipal(), + new OpenIdConnectMessage(), + new RequestGrants()); + + context.InitializeForToken(TokenTypes.RefreshToken); + context.AddToken(new TokenResult(new TestToken(TokenTypes.RefreshToken), "serialized_refresh_token")); + + // Act + await provider.AddParameters(context, response); + + // Assert + Assert.Equal("serialized_refresh_token", response.RefreshToken); + Assert.True(response.Parameters.ContainsKey("refresh_token_expires_in")); + Assert.Equal("3600", response.Parameters["refresh_token_expires_in"]); + Assert.Equal("Bearer", response.TokenType); + } + + public class TestToken : Token + { + private readonly string _kind; + + public TestToken(string kind) + : base(new List + { + new Claim(IdentityServiceClaimTypes.TokenUniqueId,"tuid"), + new Claim(IdentityServiceClaimTypes.Expires,"946688400"), + new Claim(IdentityServiceClaimTypes.IssuedAt,"946684800"), + new Claim(IdentityServiceClaimTypes.NotBefore,"946684800"), + }) + { + _kind = kind; + } + + public override string Kind => _kind; + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/FormPostResponseGeneratorTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/FormPostResponseGeneratorTest.cs new file mode 100644 index 0000000000..38c2655c5a --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/FormPostResponseGeneratorTest.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class FormPostResponseGeneratorTest + { + [Fact] + public async Task GenerateResponse_EncodesParameters_OnAnAutoPostedForm() + { + // Arrange + var expectedBody = @" + + + Please wait while you're being redirected to the identity provider + + +
+ + + +
+ + +"; + var httpContext = new DefaultHttpContext(); + httpContext.Response.Body = new MemoryStream(); + var generator = new FormPostResponseGenerator(HtmlEncoder.Default); + var redirectUri = "http://www.example.com/callback"; + var response = new Dictionary + { + ["state"] = "<>&", + ["code"] = "serializedcode" + }; + + // Act + await generator.GenerateResponseAsync(httpContext, redirectUri, response); + + // Assert + Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); + Assert.Equal("text/html; charset=utf-8", httpContext.Response.ContentType); + var body = httpContext.Response.Body; + body.Seek(0, SeekOrigin.Begin); + + var bodyText = await new StreamReader(body).ReadToEndAsync(); + Assert.Equal(expectedBody, bodyText); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/FragmentResponseGeneratorTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/FragmentResponseGeneratorTest.cs new file mode 100644 index 0000000000..29566ef6ce --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/FragmentResponseGeneratorTest.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class FragmentResponseGeneratorTest + { + [Fact] + public void GenerateResponse_EncodesParameters_OnTheFragment() + { + // Arrange + var expectedLocation = "http://www.example.com/callback#state=%23%3F%26%3D&code=serializedcode"; + var httpContext = new DefaultHttpContext(); + var generator = new FragmentResponseGenerator(UrlEncoder.Default); + var parameters = new Dictionary + { + ["state"] = new[] { "#?&=" }, + ["code"] = new[] { "serializedcode" } + }; + + var response = new OpenIdConnectMessage(parameters); + response.RedirectUri = "http://www.example.com/callback"; + // Act + generator.GenerateResponse(httpContext, response.RedirectUri, response.Parameters); + + // Assert + Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); + Assert.Equal(expectedLocation, httpContext.Response.Headers[HeaderNames.Location]); + var uri = new Uri(httpContext.Response.Headers[HeaderNames.Location]); + + Assert.False(string.IsNullOrEmpty(uri.Fragment)); + var fragmentParameters = QueryHelpers.ParseQuery(uri.Fragment.Substring(1)); + + Assert.Equal(2, fragmentParameters.Count); + var codeKvp = Assert.Single(fragmentParameters, kvp => kvp.Key == "code"); + Assert.Equal("serializedcode", codeKvp.Value); + var stateKvp = Assert.Single(fragmentParameters, kvp => kvp.Key == "state"); + Assert.Equal("#?&=", stateKvp.Value); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/IdentityServiceErrorComparer.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/IdentityServiceErrorComparer.cs new file mode 100644 index 0000000000..4b8a61eb98 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/IdentityServiceErrorComparer.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityServiceErrorComparer : IEqualityComparer, + IEqualityComparer + { + public static IdentityServiceErrorComparer Instance { get; } = new IdentityServiceErrorComparer(); + + public bool Equals(AuthorizationRequestError left, AuthorizationRequestError right) + { + return (left == null && right == null) || left != null && right != null && + string.Equals(left.Message.Error, right.Message.Error, StringComparison.Ordinal) && + string.Equals(left.Message.ErrorDescription, right.Message.ErrorDescription, StringComparison.Ordinal) && + string.Equals(left.Message.ErrorUri, right.Message.ErrorUri, StringComparison.Ordinal) && + string.Equals(left.Message.State, right.Message.State, StringComparison.Ordinal); + } + + public bool Equals(OpenIdConnectMessage left, OpenIdConnectMessage right) + { + return (left == null && right == null) || left != null && right != null && + string.Equals(left.Error, right.Error, StringComparison.Ordinal) && + string.Equals(left.ErrorDescription, right.ErrorDescription, StringComparison.Ordinal) && + string.Equals(left.ErrorUri, right.ErrorUri, StringComparison.Ordinal) && + string.Equals(left.State, right.State, StringComparison.Ordinal); + } + + public int GetHashCode(AuthorizationRequestError obj) + { + return 1; // Minimal implementation that satisfies the contract. + } + + public int GetHashCode(OpenIdConnectMessage obj) + { + return 1; // Minimal implementation that satisfies the contract. + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtAccessTokenIssuerTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtAccessTokenIssuerTest.cs new file mode 100644 index 0000000000..d7b7faa0cc --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtAccessTokenIssuerTest.cs @@ -0,0 +1,232 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Identity.Service.Claims; +using Microsoft.AspNetCore.Identity.Service.Core; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class JtwAccessTokenIssuerTest + { + [Fact] + public async Task JwtAccessTokenIssuer_Fails_IfUserIsMissingUserId() + { + // Arrange + var options = GetOptions(); + var issuer = new JwtAccessTokenIssuer( + GetClaimsManager(), + GetSigningPolicy(options, new TimeStampManager()), new JwtSecurityTokenHandler(), options); + var context = GetTokenGenerationContext(); + + context.InitializeForToken(TokenTypes.AccessToken); + + // Act + var exception = await Assert.ThrowsAsync( + () => issuer.IssueAccessTokenAsync(context)); + + // Assert + Assert.Equal($"Missing '{ClaimTypes.NameIdentifier}' claim from the user.", exception.Message); + } + + [Fact] + public async Task JwtAccessTokenIssuer_SignsAccessToken() + { + // Arrange + var expectedDateTime = new DateTimeOffset(2000, 01, 01, 0, 0, 0, TimeSpan.FromHours(1)); + var now = DateTimeOffset.UtcNow; + var expires = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, TimeSpan.Zero); + var timeManager = GetTimeManager(expectedDateTime, expires, expectedDateTime); + + var options = GetOptions(); + + var handler = new JwtSecurityTokenHandler(); + + var tokenValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = options.Value.SigningKeys[0].Key, + ValidAudiences = new[] { "resourceId" }, + ValidIssuers = new[] { options.Value.Issuer } + }; + + var issuer = new JwtAccessTokenIssuer( + GetClaimsManager(timeManager), + GetSigningPolicy(options,timeManager), + new JwtSecurityTokenHandler(), options); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") }))); + + context.InitializeForToken(TokenTypes.AccessToken); + + // Act + await issuer.IssueAccessTokenAsync(context); + + // Assert + Assert.NotNull(context.AccessToken); + Assert.NotNull(context.AccessToken.SerializedValue); + + SecurityToken validatedToken; + Assert.NotNull(handler.ValidateToken(context.AccessToken.SerializedValue, tokenValidationParameters, out validatedToken)); + Assert.NotNull(validatedToken); + + var jwtToken = Assert.IsType(validatedToken); + var accessToken = Assert.IsType(context.AccessToken.Token); + Assert.Equal("http://www.example.com/issuer", jwtToken.Issuer); + var tokenAudience = Assert.Single(jwtToken.Audiences); + Assert.Equal("resourceId", tokenAudience); + var tokenAuthorizedParty = Assert.Single(jwtToken.Claims, c=> c.Type.Equals("azp")).Value; + Assert.Equal("clientId", tokenAuthorizedParty); + Assert.Equal("user", jwtToken.Subject); + + Assert.Equal(expires, jwtToken.ValidTo); + Assert.Equal(expectedDateTime.UtcDateTime, jwtToken.ValidFrom); + + var tokenScopes = jwtToken.Claims + .Where(c => c.Type == IdentityServiceClaimTypes.Scope) + .Select(c => c.Value).OrderBy(c => c) + .ToArray(); + + Assert.Equal(new[] { "all" }, tokenScopes); + } + + [Fact] + public async Task JwtAccessTokenIssuer_IncludesAllRequiredData() + { + // Arrange + var options = GetOptions(); + + var expectedDateTime = new DateTimeOffset(2000, 01, 01, 0, 0, 0, TimeSpan.FromHours(1)); + var timeManager = GetTimeManager(expectedDateTime, expectedDateTime.AddHours(1), expectedDateTime); + var issuer = new JwtAccessTokenIssuer( + GetClaimsManager(timeManager), + GetSigningPolicy(options,timeManager), + new JwtSecurityTokenHandler(), options); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") }))); + + context.InitializeForToken(TokenTypes.AccessToken); + + // Act + await issuer.IssueAccessTokenAsync(context); + + // Assert + Assert.NotNull(context.AccessToken); + var accessToken = Assert.IsType(context.AccessToken.Token); + Assert.NotNull(accessToken); + Assert.NotNull(accessToken.Id); + Assert.Equal("user", accessToken.Subject); + Assert.Equal("resourceId", accessToken.Audience); + Assert.Equal("clientId", accessToken.AuthorizedParty); + Assert.Equal(new[] { "all" }, accessToken.Scopes); + Assert.Equal(expectedDateTime, accessToken.IssuedAt); + Assert.Equal(expectedDateTime.AddHours(1), accessToken.Expires); + Assert.Equal(expectedDateTime, accessToken.NotBefore); + } + + private TokenGeneratingContext GetTokenGenerationContext( + ClaimsPrincipal user = null, + ClaimsPrincipal application = null) => + new TokenGeneratingContext( + user ?? new ClaimsPrincipal(new ClaimsIdentity()), + application ?? new ClaimsPrincipal(new ClaimsIdentity()), + new OpenIdConnectMessage + { + Code = "code", + ClientId = "clientId", + Scope = "openid profile https://www.example.com/ResourceApp/all", + Nonce = null, + RedirectUri = "http://www.example.com/callback" + }, + new RequestGrants + { + RedirectUri = "http://www.example.com/callback", + Scopes = new[] { ApplicationScope.OpenId, ApplicationScope.Profile, new ApplicationScope("resourceId", "all") }, + Tokens = new[] { TokenTypes.AuthorizationCode } + }); + + private ITimeStampManager GetTimeManager( + DateTimeOffset? issuedAt = null, + DateTimeOffset? expires = null, + DateTimeOffset? notBefore = null) + { + issuedAt = issuedAt ?? DateTimeOffset.Now; + expires = expires ?? DateTimeOffset.Now; + notBefore = notBefore ?? DateTimeOffset.Now; + + var manager = new Mock(); + + manager.Setup(m => m.GetCurrentTimeStampInEpochTime()) + .Returns(issuedAt.Value.ToUnixTimeSeconds().ToString()); + + manager.SetupSequence(t => t.GetTimeStampInEpochTime(It.IsAny())) + .Returns(notBefore.Value.ToUnixTimeSeconds().ToString()) + .Returns(expires.Value.ToUnixTimeSeconds().ToString()); + + manager.Setup(m => m.IsValidPeriod(It.IsAny(), It.IsAny())) + .Returns(true); + + return manager.Object; + } + + private IOptions GetOptions() + { + var IdentityServiceOptions = new IdentityServiceOptions() + { + Issuer = "http://www.example.com/issuer" + }; + IdentityServiceOptions.SigningKeys.Add(new SigningCredentials(CryptoUtilities.CreateTestKey(), "RS256")); + + var optionsSetup = new IdentityServiceOptionsDefaultSetup(); + optionsSetup.Configure(IdentityServiceOptions); + + var mock = new Mock>(); + mock.Setup(m => m.Value).Returns(IdentityServiceOptions); + + return mock.Object; + } + + private ITokenClaimsManager GetClaimsManager( + ITimeStampManager timeManager = null) + { + var options = GetOptions(); + return new DefaultTokenClaimsManager( + new List{ + new DefaultTokenClaimsProvider(options), + new GrantedTokensTokenClaimsProvider(), + new NonceTokenClaimsProvider(), + new ScopesTokenClaimsProvider(), + new TimestampsTokenClaimsProvider(timeManager ?? new TimeStampManager(),options), + new TokenHashTokenClaimsProvider(new TokenHasher()) + }); + } + + private ISigningCredentialsPolicyProvider GetSigningPolicy( + IOptions options, + ITimeStampManager timeStampManager) + { + var mock = new Mock>(); + mock.Setup(m => m.Value).Returns(options.Value); + mock.Setup(m => m.Get(It.IsAny())).Returns(options.Value); + + return new DefaultSigningCredentialsPolicyProvider( + new List { + new DefaultSigningCredentialsSource(mock.Object, timeStampManager) + }, + timeStampManager, + new HostingEnvironment()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtIdTokenIssuerTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtIdTokenIssuerTest.cs new file mode 100644 index 0000000000..f8e826cbcb --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtIdTokenIssuerTest.cs @@ -0,0 +1,349 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Identity.Service.Claims; +using Microsoft.AspNetCore.Identity.Service.Core; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class JwtIdTokenIssuerTest + { + [Fact] + public async Task JwtIdTokenIssuer_Fails_IfUserIsMissingUserId() + { + // Arrange + var options = GetOptions(); + var timeManager = GetTimeManager(); + var hasher = GetHasher(); + + var issuer = new JwtIdTokenIssuer(GetClaimsManager(), GetSigningPolicy(options, timeManager), new JwtSecurityTokenHandler(), options); + var context = GetTokenGenerationContext(); + context.InitializeForToken(TokenTypes.IdToken); + + // Act + var exception = await Assert.ThrowsAsync( + () => issuer.IssueIdTokenAsync(context)); + + // Assert + Assert.Equal($"Missing '{ClaimTypes.NameIdentifier}' claim from the user.", exception.Message); + } + + [Fact] + public async Task JwtIdTokenIssuer_Fails_IfApplicationIsMissingClientId() + { + // Arrange + var options = GetOptions(); + var timeManager = GetTimeManager(); + + var hasher = GetHasher(); + + var issuer = new JwtIdTokenIssuer(GetClaimsManager(), GetSigningPolicy(options, timeManager), new JwtSecurityTokenHandler(), options); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") }))); + + context.InitializeForToken(TokenTypes.IdToken); + + // Act + var exception = await Assert.ThrowsAsync( + () => issuer.IssueIdTokenAsync(context)); + + // Assert + Assert.Equal($"Missing '{IdentityServiceClaimTypes.ClientId}' claim from the application.", exception.Message); + } + + [Fact] + public async Task JwtIdTokenIssuer_SignsAccessToken() + { + // Arrange + var expectedDateTime = new DateTimeOffset(2000, 01, 01, 0, 0, 0, TimeSpan.FromHours(1)); + var now = DateTimeOffset.UtcNow; + var expires = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, TimeSpan.Zero); + var timeManager = GetTimeManager(expectedDateTime, expires, expectedDateTime); + + var hasher = GetHasher(); + var options = GetOptions(); + + var handler = new JwtSecurityTokenHandler(); + + var tokenValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = options.Value.SigningKeys[0].Key, + ValidAudiences = new[] { "clientId" }, + ValidIssuers = new[] { options.Value.Issuer } + }; + + var issuer = new JwtIdTokenIssuer(GetClaimsManager(timeManager), GetSigningPolicy(options, timeManager), new JwtSecurityTokenHandler(), options); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") }))); + + context.InitializeForToken(TokenTypes.IdToken); + + // Act + await issuer.IssueIdTokenAsync(context); + + // Assert + Assert.NotNull(context.IdToken); + Assert.NotNull(context.IdToken.SerializedValue); + + SecurityToken validatedToken; + Assert.NotNull(handler.ValidateToken(context.IdToken.SerializedValue, tokenValidationParameters, out validatedToken)); + Assert.NotNull(validatedToken); + + var jwtToken = Assert.IsType(validatedToken); + var result = Assert.IsType(context.IdToken.Token); + Assert.Equal("http://www.example.com/issuer", jwtToken.Issuer); + var tokenAudience = Assert.Single(jwtToken.Audiences); + Assert.Equal("clientId", tokenAudience); + Assert.Equal("user", jwtToken.Subject); + + Assert.Equal(expires, jwtToken.ValidTo); + Assert.Equal(expectedDateTime.UtcDateTime, jwtToken.ValidFrom); + } + + [Theory] + [InlineData(null, null, null)] + [InlineData("nonce", null, null)] + [InlineData("nonce", "code", null)] + [InlineData("nonce", "code", "accesstoken")] + [InlineData("nonce", "code", null)] + [InlineData("nonce", null, "accesstoken")] + public async Task JwtIdTokenIssuer_IncludesNonceAndTokenHashesWhenPresent(string nonce, string code, string accessToken) + { + // Arrange + var expectedCHash = code != null ? $"#{code}" : null; + var expectedAtHash = accessToken != null ? $"#{accessToken}" : null; + + var expectedDateTime = new DateTimeOffset(2000, 01, 01, 0, 0, 0, TimeSpan.FromHours(1)); + var expires = DateTimeOffset.UtcNow.AddHours(1); + var timeManager = GetTimeManager(expectedDateTime, expires, expectedDateTime); + + var options = GetOptions(); + + var handler = new JwtSecurityTokenHandler(); + + var tokenValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = options.Value.SigningKeys[0].Key, + ValidAudiences = new[] { "clientId" }, + ValidIssuers = new[] { options.Value.Issuer } + }; + + var issuer = new JwtIdTokenIssuer(GetClaimsManager(timeManager), GetSigningPolicy(options, timeManager), new JwtSecurityTokenHandler(), options); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") })), + nonce); + + if (code != null) + { + context.InitializeForToken(TokenTypes.AuthorizationCode); + context.AddToken(new TokenResult(new AuthorizationCode(GetAuthorizationCodeClaims()), "code")); + } + + if (accessToken != null) + { + context.InitializeForToken(TokenTypes.AccessToken); + context.AddToken(new TokenResult(new AccessToken(GetAccessTokenClaims()), "accesstoken")); + } + + context.InitializeForToken(TokenTypes.IdToken); + + // Act + await issuer.IssueIdTokenAsync(context); + + // Assert + Assert.NotNull(context.IdToken); + Assert.NotNull(context.IdToken.SerializedValue); + + SecurityToken validatedToken; + Assert.NotNull(handler.ValidateToken(context.IdToken.SerializedValue, tokenValidationParameters, out validatedToken)); + Assert.NotNull(validatedToken); + + var jwtToken = Assert.IsType(validatedToken); + var result = Assert.IsType(context.IdToken.Token); + Assert.Equal(nonce, result.Nonce); + Assert.Equal(nonce, jwtToken.Payload.Nonce); + Assert.Equal(expectedCHash, result.CodeHash); + Assert.Equal(expectedCHash, jwtToken.Payload.CHash); + Assert.Equal(expectedAtHash, result.AccessTokenHash); + Assert.Equal(expectedAtHash, jwtToken.Payload.Claims.FirstOrDefault(c => c.Type == "at_hash")?.Value); + } + + private IEnumerable GetAccessTokenClaims() => + new[] + { + new Claim(IdentityServiceClaimTypes.TokenUniqueId,"tokenId"), + new Claim(IdentityServiceClaimTypes.IssuedAt,"1000"), + new Claim(IdentityServiceClaimTypes.NotBefore,"1000"), + new Claim(IdentityServiceClaimTypes.Expires,"1000"), + new Claim(IdentityServiceClaimTypes.Issuer,"issuer"), + new Claim(IdentityServiceClaimTypes.Subject,"subject"), + new Claim(IdentityServiceClaimTypes.Audience,"audience"), + new Claim(IdentityServiceClaimTypes.AuthorizedParty,"authorizedparty"), + new Claim(IdentityServiceClaimTypes.Scope,"openid") + }; + + private IEnumerable GetAuthorizationCodeClaims() => + new[] + { + new Claim(IdentityServiceClaimTypes.TokenUniqueId,"tokenId"), + new Claim(IdentityServiceClaimTypes.IssuedAt,"1000"), + new Claim(IdentityServiceClaimTypes.NotBefore,"1000"), + new Claim(IdentityServiceClaimTypes.Expires,"1000"), + new Claim(IdentityServiceClaimTypes.UserId,"subject"), + new Claim(IdentityServiceClaimTypes.ClientId,"audience"), + new Claim(IdentityServiceClaimTypes.Scope,"openid"), + new Claim(IdentityServiceClaimTypes.GrantedToken,"accesstoken"), + new Claim(IdentityServiceClaimTypes.RedirectUri,"redirectUri"), + }; + + [Fact] + public async Task JwtIdTokenIssuer_IncludesAllRequiredData() + { + // Arrange + var options = GetOptions(); + var hasher = GetHasher(); + var expectedDateTime = new DateTimeOffset(2000, 01, 01, 0, 0, 0, TimeSpan.FromHours(1)); + var timeManager = GetTimeManager(expectedDateTime, expectedDateTime.AddHours(1), expectedDateTime); + var issuer = new JwtIdTokenIssuer(GetClaimsManager(timeManager), GetSigningPolicy(options, timeManager), new JwtSecurityTokenHandler(), options); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") }))); + + context.InitializeForToken(TokenTypes.IdToken); + + // Act + await issuer.IssueIdTokenAsync(context); + + // Assert + Assert.NotNull(context.IdToken); + var result = Assert.IsType(context.IdToken.Token); + Assert.NotNull(result); + Assert.NotNull(result.Id); + Assert.Equal("user", result.Subject); + Assert.Equal("clientId", result.Audience); + Assert.Equal(expectedDateTime, result.IssuedAt); + Assert.Equal(expectedDateTime.AddHours(1), result.Expires); + Assert.Equal(expectedDateTime, result.NotBefore); + Assert.Equal("asdf", result.Nonce); + } + + private ITokenHasher GetHasher() + { + var mock = new Mock(); + mock.Setup(t => t.HashToken("code", "RS256")) + .Returns("#code"); + + mock.Setup(t => t.HashToken("accesstoken", "RS256")) + .Returns("#accesstoken"); + + return mock.Object; + } + + private TokenGeneratingContext GetTokenGenerationContext( + ClaimsPrincipal user = null, + ClaimsPrincipal application = null, + string nonce = "asdf") => + new TokenGeneratingContext( + user ?? new ClaimsPrincipal(new ClaimsIdentity()), + application ?? new ClaimsPrincipal(new ClaimsIdentity()), + new OpenIdConnectMessage + { + Code = "code", + Scope = "openid profile", + Nonce = nonce, + RedirectUri = "http://www.example.com/callback" + }, + new RequestGrants + { + RedirectUri = "http://www.example.com/callback", + Scopes = new[] { ApplicationScope.OpenId, ApplicationScope.Profile }, + Tokens = new[] { TokenTypes.AuthorizationCode } + }); + + private ITimeStampManager GetTimeManager( + DateTimeOffset? issuedAt = null, + DateTimeOffset? expires = null, + DateTimeOffset? notBefore = null) + { + issuedAt = issuedAt ?? DateTimeOffset.Now; + expires = expires ?? DateTimeOffset.Now; + notBefore = notBefore ?? DateTimeOffset.Now; + + var manager = new Mock(); + + manager.Setup(m => m.GetCurrentTimeStampInEpochTime()) + .Returns(issuedAt.Value.ToUnixTimeSeconds().ToString()); + + manager.SetupSequence(t => t.GetTimeStampInEpochTime(It.IsAny())) + .Returns(notBefore.Value.ToUnixTimeSeconds().ToString()) + .Returns(expires.Value.ToUnixTimeSeconds().ToString()); + + manager.Setup(m => m.IsValidPeriod(It.IsAny(), It.IsAny())) + .Returns(true); + + return manager.Object; + } + + private IOptions GetOptions() + { + var identityServiceOptions = new IdentityServiceOptions() + { + Issuer = "http://www.example.com/issuer" + }; + var optionsSetup = new IdentityServiceOptionsDefaultSetup(); + optionsSetup.Configure(identityServiceOptions); + + identityServiceOptions.SigningKeys.Add(new SigningCredentials(CryptoUtilities.CreateTestKey(), "RS256")); + identityServiceOptions.IdTokenOptions.UserClaims.AddSingle("sub", ClaimTypes.NameIdentifier); + + var mock = new Mock>(); + mock.Setup(m => m.Value).Returns(identityServiceOptions); + + return mock.Object; + } + + private ITokenClaimsManager GetClaimsManager( + ITimeStampManager timeManager = null) + { + var options = GetOptions(); + return new DefaultTokenClaimsManager( + new List{ + new DefaultTokenClaimsProvider(options), + new GrantedTokensTokenClaimsProvider(), + new NonceTokenClaimsProvider(), + new ScopesTokenClaimsProvider(), + new TimestampsTokenClaimsProvider(timeManager ?? new TimeStampManager(),options), + new TokenHashTokenClaimsProvider(GetHasher()) + }); + } + + private ISigningCredentialsPolicyProvider GetSigningPolicy( + IOptions options, + ITimeStampManager timeManager) + { + var mock = new Mock>(); + mock.Setup(m => m.Value).Returns(options.Value); + mock.Setup(m => m.Get(It.IsAny())).Returns(options.Value); + return new DefaultSigningCredentialsPolicyProvider( + new List { + new DefaultSigningCredentialsSource(mock.Object, timeManager) + }, + timeManager, + new HostingEnvironment()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtRefreshTokenIssuerTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtRefreshTokenIssuerTest.cs new file mode 100644 index 0000000000..5e1964c809 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/JwtRefreshTokenIssuerTest.cs @@ -0,0 +1,226 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity.Service.Claims; +using Microsoft.AspNetCore.Identity.Service.Serialization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class JwtRefreshTokenIssuerTest + { + [Fact] + public async Task JwtRefreshTokenIssuer_Fails_IfUserIsMissingUserId() + { + // Arrange + var dataFormat = GetDataFormat(); + var options = GetOptions(); + var timeManager = GetTimeManager(); + var issuer = new RefreshTokenIssuer(GetClaimsManager(), dataFormat); + var context = GetTokenGenerationContext(); + + context.InitializeForToken(TokenTypes.RefreshToken); + + // Act + var exception = await Assert.ThrowsAsync( + () => issuer.IssueRefreshTokenAsync(context)); + + // Assert + Assert.Equal($"Missing '{ClaimTypes.NameIdentifier}' claim from the user.", exception.Message); + } + + [Fact] + public async Task JwtRefreshTokenIssuer_Fails_IfApplicationIsMissingClientId() + { + // Arrange + var dataFormat = GetDataFormat(); + var options = GetOptions(); + var timeManager = GetTimeManager(); + var issuer = new RefreshTokenIssuer(GetClaimsManager(), dataFormat); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") }))); + + context.InitializeForToken(TokenTypes.RefreshToken); + + // Act + var exception = await Assert.ThrowsAsync( + () => issuer.IssueRefreshTokenAsync(context)); + + // Assert + Assert.Equal($"Missing '{IdentityServiceClaimTypes.ClientId}' claim from the application.", exception.Message); + } + + [Fact] + public async Task JwtRefreshTokenIssuer_ExchangeRefreshTokenAsync_ReadTheRefreshTokenCorrectly() + { + // Arrange + var options = GetOptions(); + var protector = new EphemeralDataProtectionProvider(new LoggerFactory()).CreateProtector("test"); + var refreshTokenSerializer = new TokenDataSerializer(options, ArrayPool.Shared); + var dataFormat = new SecureDataFormat(refreshTokenSerializer, protector); + + var expectedDateTime = new DateTimeOffset(2000, 01, 01, 0, 0, 0, TimeSpan.FromHours(1)); + var now = DateTimeOffset.UtcNow; + var expires = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, TimeSpan.Zero); + var timeManager = GetTimeManager(expectedDateTime, expires, expectedDateTime); + + var issuer = new RefreshTokenIssuer(GetClaimsManager(timeManager), dataFormat); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") }))); + + context.InitializeForToken(TokenTypes.RefreshToken); + + await issuer.IssueRefreshTokenAsync(context); + + var message = new OpenIdConnectMessage(); + message.ClientId = "clientId"; + message.RefreshToken = context.RefreshToken.SerializedValue; + + // Act + var grant = await issuer.ExchangeRefreshTokenAsync(message); + + // Assert + Assert.NotNull(grant); + Assert.NotNull(grant.Token); + var refreshToken = Assert.IsType(grant.Token); + + Assert.Equal("clientId", refreshToken.ClientId); + Assert.Equal("user", refreshToken.UserId); + + Assert.Equal(expectedDateTime, refreshToken.IssuedAt); + Assert.Equal(expires, refreshToken.Expires); + Assert.Equal(expectedDateTime, refreshToken.NotBefore); + + Assert.Equal(new[] { "openid profile" }, refreshToken.Scopes.ToArray()); + } + + [Fact] + public async Task JwtRefreshTokenIssuer_IncludesAllRequiredData() + { + // Arrange + var dataFormat = GetDataFormat(); + var options = GetOptions(); + var expectedDateTime = new DateTimeOffset(2000, 01, 01, 0, 0, 0, TimeSpan.FromHours(1)); + var timeManager = GetTimeManager(expectedDateTime, expectedDateTime.AddHours(1), expectedDateTime); + var issuer = new RefreshTokenIssuer(GetClaimsManager(timeManager), dataFormat); + var context = GetTokenGenerationContext( + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user") })), + new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(IdentityServiceClaimTypes.ClientId, "clientId") }))); + + context.InitializeForToken(TokenTypes.RefreshToken); + + // Act + await issuer.IssueRefreshTokenAsync(context); + + // Assert + Assert.NotNull(context.RefreshToken); + var RefreshToken = Assert.IsType(context.RefreshToken.Token); + Assert.NotNull(RefreshToken); + Assert.NotNull(RefreshToken.Id); + Assert.Equal("user", RefreshToken.UserId); + Assert.Equal("clientId", RefreshToken.ClientId); + Assert.Equal(new[] { "openid profile" }, RefreshToken.Scopes); + Assert.Equal(expectedDateTime, RefreshToken.IssuedAt); + Assert.Equal(expectedDateTime.AddHours(1), RefreshToken.Expires); + Assert.Equal(expectedDateTime, RefreshToken.NotBefore); + } + + private TokenGeneratingContext GetTokenGenerationContext( + ClaimsPrincipal user = null, + ClaimsPrincipal application = null) => + new TokenGeneratingContext( + user ?? new ClaimsPrincipal(new ClaimsIdentity()), + application ?? new ClaimsPrincipal(new ClaimsIdentity()), + new OpenIdConnectMessage + { + Code = "code", + Scope = "openid profile", + Nonce = null, + RedirectUri = "http://www.example.com/callback", + RequestType = OpenIdConnectRequestType.Token, + }, + new RequestGrants + { + RedirectUri = "http://www.example.com/callback", + Scopes = new ApplicationScope[] { ApplicationScope.OpenId, ApplicationScope.Profile }, + Tokens = new[] { TokenTypes.AuthorizationCode }, + Claims = new[] {new Claim("scp","openid profile")} + }); + + private ITimeStampManager GetTimeManager( + DateTimeOffset? issuedAt = null, + DateTimeOffset? expires = null, + DateTimeOffset? notBefore = null) + { + issuedAt = issuedAt ?? DateTimeOffset.Now; + expires = expires ?? DateTimeOffset.Now; + notBefore = notBefore ?? DateTimeOffset.Now; + + var manager = new Mock(); + + manager.Setup(m => m.GetCurrentTimeStampInEpochTime()) + .Returns(issuedAt.Value.ToUnixTimeSeconds().ToString()); + + manager.SetupSequence(t => t.GetTimeStampInEpochTime(It.IsAny())) + .Returns(notBefore.Value.ToUnixTimeSeconds().ToString()) + .Returns(expires.Value.ToUnixTimeSeconds().ToString()); + + return manager.Object; + } + + private ISecureDataFormat GetDataFormat() + { + var mock = new Mock>(); + mock.Setup(s => s.Protect(It.IsAny())) + .Returns("protected refresh token"); + + return mock.Object; + } + + private IOptions GetOptions() + { + var IdentityServiceOptions = new IdentityServiceOptions() + { + Issuer = "http://www.example.com/issuer" + }; + IdentityServiceOptions.SigningKeys.Add(new SigningCredentials(CryptoUtilities.CreateTestKey(), "RS256")); + + var optionsSetup = new IdentityServiceOptionsDefaultSetup(); + optionsSetup.Configure(IdentityServiceOptions); + + var mock = new Mock>(); + mock.Setup(m => m.Value).Returns(IdentityServiceOptions); + + return mock.Object; + } + + private ITokenClaimsManager GetClaimsManager( + ITimeStampManager timeManager = null) + { + var options = GetOptions(); + return new DefaultTokenClaimsManager( + new List{ + new DefaultTokenClaimsProvider(options), + new GrantedTokensTokenClaimsProvider(), + new NonceTokenClaimsProvider(), + new ScopesTokenClaimsProvider(), + new TimestampsTokenClaimsProvider(timeManager ?? new TimeStampManager(),options), + new TokenHashTokenClaimsProvider(new TokenHasher()) + }); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Microsoft.AspNetCore.Identity.Service.Core.Test.csproj b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Microsoft.AspNetCore.Identity.Service.Core.Test.csproj new file mode 100644 index 0000000000..f9a1b145e1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/Microsoft.AspNetCore.Identity.Service.Core.Test.csproj @@ -0,0 +1,24 @@ + + + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/QueryResponseGeneratorTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/QueryResponseGeneratorTest.cs new file mode 100644 index 0000000000..0dbafa6a26 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/QueryResponseGeneratorTest.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class QueryResponseGeneratorTest + { + [Fact] + public void GenerateResponse_EncodesParameters_OnTheQuery() + { + // Arrange + var expectedLocation = "http://www.example.com/callback?state=%23%3F%26%3D&code=serializedcode"; + + var httpContext = new DefaultHttpContext(); + var generator = new QueryResponseGenerator(); + var redirectUri = "http://www.example.com/callback"; + var parameters = new Dictionary + { + ["state"] = new[] { "#?&=" }, + ["code"] = new[] { "serializedcode" } + }; + var response = new OpenIdConnectMessage(parameters); + response.RedirectUri = redirectUri; + + // Act + generator.GenerateResponse(httpContext, response.RedirectUri, response.Parameters); + + // Assert + Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); + + Assert.Equal(expectedLocation, httpContext.Response.Headers[HeaderNames.Location]); + + var uri = new Uri(httpContext.Response.Headers[HeaderNames.Location]); + + Assert.False(string.IsNullOrEmpty(uri.Query)); + var queryParameters = QueryHelpers.ParseQuery(uri.Query); + + Assert.Equal(2, queryParameters.Count); + var codeKvp = Assert.Single(queryParameters, kvp => kvp.Key == "code"); + Assert.Equal("serializedcode", codeKvp.Value); + var stateKvp = Assert.Single(queryParameters, kvp => kvp.Key == "state"); + Assert.Equal("#?&=", stateKvp.Value); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/TokenHasherTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/TokenHasherTest.cs new file mode 100644 index 0000000000..a0ab8441db --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/TokenHasherTest.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenHasherTest + { + public static TheoryData WellKnownHashes => + new TheoryData() + { + { "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk", "LDktKdoQak3Pk0cnXxCltA" }, + { "jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y", "77QmUPtjPfzWtF2AnpK9RQ" } + }; + + [Theory] + [MemberData(nameof(WellKnownHashes))] + public void TokenHasher_CanHashTokensSignedWithRS256(string input, string expectedHash) + { + // Arrange + var hasher = new TokenHasher(); + + // Act + var hash = hasher.HashToken(input, "RS256"); + + // Assert + Assert.Equal(expectedHash, hash); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Core.Test/TokenRequestFactoryTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/TokenRequestFactoryTest.cs new file mode 100644 index 0000000000..6345a99ee7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Core.Test/TokenRequestFactoryTest.cs @@ -0,0 +1,510 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class TokenRequestFactoryTest + { + public static ProtocolErrorProvider ProtocolErrorProvider = new ProtocolErrorProvider(); + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfRequestDoesNotContainGrantType() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" }, + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true), + Mock.Of(), + Mock.Of(), + Enumerable.Empty(), + Mock.Of(), + Mock.Of(), + new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.GrantType); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfGrantTypeIsNotSupported() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" }, + [OpenIdConnectParameterNames.GrantType] = new[] { "unsupported" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true), + Mock.Of(), Mock.Of(), + Enumerable.Empty(), + Mock.Of(), + Mock.Of(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.InvalidGrantType("unsupported"); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfProvidedGrantIsNotValid() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" }, + [OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" }, + [OpenIdConnectParameterNames.Code] = new[] { "invalid" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true), + Mock.Of(), Mock.Of(), + Enumerable.Empty(), + GetTestTokenManager(), + Mock.Of(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.InvalidGrant(); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfGrantHasExpired() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" }, + [OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" }, + [OpenIdConnectParameterNames.Code] = new[] { "valid" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true), + Mock.Of(), Mock.Of(), + Enumerable.Empty(), + GetTestTokenManager(GetExpiredToken()), + Mock.Of(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.InvalidLifetime(); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfClientIdIsMissing() + { + // Arrange + var requestParameters = new Dictionary + { + }; + + var tokenRequestFactory = new TokenRequestFactory( + Mock.Of(), + Mock.Of(), Mock.Of(), + Enumerable.Empty(), + GetTestTokenManager(GetValidToken()), + new TimeStampManager(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.ClientId); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfClientIdDoesntMatchTheClientIdOnTheGrant() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" }, + [OpenIdConnectParameterNames.Code] = new[] { "valid" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "otherClientId" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true), + Mock.Of(), Mock.Of(), + Enumerable.Empty(), + GetTestTokenManager(GetValidToken()), + new TimeStampManager(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.InvalidGrant(); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfClientIdIsNotValid() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: false), + Mock.Of(), Mock.Of(), + Enumerable.Empty(), + GetTestTokenManager(GetValidToken()), + new TimeStampManager(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.InvalidClientId("clientId"); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfClientCredentialsValidationFails() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: false), + Mock.Of(), Mock.Of(), + Enumerable.Empty(), + GetTestTokenManager(GetValidToken()), + new TimeStampManager(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.InvalidClientCredentials(); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfMultipleScopesArePresent() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" }, + [OpenIdConnectParameterNames.Code] = new[] { "valid" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" }, + [OpenIdConnectParameterNames.Scope] = new[] { "openid", "profile" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true), + Mock.Of(), + Mock.Of(), + Enumerable.Empty(), + GetTestTokenManager(GetValidToken()), + new TimeStampManager(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.Scope); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfRequestContainsInvalidScopes() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" }, + [OpenIdConnectParameterNames.Code] = new[] { "valid" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" }, + [OpenIdConnectParameterNames.Scope] = new[] { "invalid openid" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true), + Mock.Of(), + GetScopeResolver(hasInvalidScopes: true), + Enumerable.Empty(), + GetTestTokenManager(GetValidToken(),null,null,Enumerable.Empty(), new[] { "openid" }), + new TimeStampManager(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.InvalidScope("invalid"); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfRequestContains_ScopesNotPresentInTheAuthorizationRequest() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" }, + [OpenIdConnectParameterNames.Code] = new[] { "valid" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" }, + [OpenIdConnectParameterNames.Scope] = new[] { "invalid openid" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true), + Mock.Of(), + GetScopeResolver(hasInvalidScopes: false), + Enumerable.Empty(), + GetTestTokenManager(GetValidToken(), null, null, Enumerable.Empty(), new[] { "openid" }), + new TimeStampManager(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.UnauthorizedScope(); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfRedirectUriIsNotPresent() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" }, + [OpenIdConnectParameterNames.Code] = new[] { "valid" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true), + GetRedirectUriValidator(isRedirectUriValid: false), + Mock.Of(), + Enumerable.Empty(), + GetTestTokenManager(GetValidToken()), + new TimeStampManager(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + private IRedirectUriResolver GetRedirectUriValidator(bool isRedirectUriValid) + { + var mock = new Mock(); + mock.Setup(m => m.ResolveRedirectUriAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((string clientId, string redirectUri) => isRedirectUriValid ? + RedirectUriResolutionResult.Valid(redirectUri) : + RedirectUriResolutionResult.Invalid(ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri))); + + return mock.Object; + } + + [Fact] + public async Task CreateTokenRequestAsyncFails_IfRedirectUriDoesNotMatch() + { + // Arrange + var requestParameters = new Dictionary + { + [OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" }, + [OpenIdConnectParameterNames.Code] = new[] { "valid" }, + [OpenIdConnectParameterNames.ClientId] = new[] { "clientId" } + }; + + var tokenRequestFactory = new TokenRequestFactory( + GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true), + Mock.Of(), Mock.Of(), + Enumerable.Empty(), + GetTestTokenManager(GetValidToken()), + new TimeStampManager(), new ProtocolErrorProvider()); + + var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri); + + // Act + var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters); + + // Assert + Assert.NotNull(tokenRequest); + Assert.False(tokenRequest.IsValid); + Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance); + } + + private IClientIdValidator GetClientIdValidator(bool isClientIdValid = false, bool areClientCredentialsValid = false) + { + var clientIdValidator = new Mock(); + + clientIdValidator + .Setup(cv => cv.ValidateClientIdAsync(It.IsAny())) + .ReturnsAsync(isClientIdValid); + + clientIdValidator + .Setup(cv => cv.ValidateClientCredentialsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(areClientCredentialsValid); + + return clientIdValidator.Object; + } + + private Token GetValidToken() + { + var notBefore = EpochTime.GetIntDate(DateTime.UtcNow - TimeSpan.FromMinutes(20)).ToString(); + var expires = EpochTime.GetIntDate(DateTime.UtcNow + TimeSpan.FromMinutes(10)).ToString(); + var issuedAt = EpochTime.GetIntDate(DateTime.UtcNow).ToString(); + var authorizedParty = "clientId"; + return new TestToken(new Claim[] + { + new Claim(IdentityServiceClaimTypes.TokenUniqueId, Guid.NewGuid().ToString()), + new Claim(IdentityServiceClaimTypes.RedirectUri, "https://www.example.com"), + new Claim(IdentityServiceClaimTypes.NotBefore,notBefore), + new Claim(IdentityServiceClaimTypes.Expires,expires), + new Claim(IdentityServiceClaimTypes.IssuedAt,issuedAt), + new Claim(IdentityServiceClaimTypes.AuthorizedParty, authorizedParty) + }); + } + + private ITokenManager GetTestTokenManager( + Token token = null, + string userId = null, + string clientId = null, + IEnumerable grantedTokens = null, + IEnumerable grantedScopes = null) + { + userId = userId ?? "userId"; + clientId = clientId ?? "clientId"; + grantedTokens = grantedTokens ?? Enumerable.Empty(); + var parsedScopes = grantedScopes?.Select(s => ApplicationScope.CanonicalScopes.TryGetValue(s, out var found) ? found : new ApplicationScope(clientId, s)) ?? + new[] { ApplicationScope.OpenId, ApplicationScope.OfflineAccess }; + + var mock = new Mock(); + if (token == null) + { + mock.Setup(m => m.ExchangeTokenAsync(It.IsAny())) + .ReturnsAsync(AuthorizationGrant.Invalid(ProtocolErrorProvider.InvalidGrant())); + } + else + { + mock.Setup(m => m.ExchangeTokenAsync(It.IsAny())) + .ReturnsAsync(AuthorizationGrant.Valid( + userId, + clientId, + grantedTokens, + parsedScopes, + token)); + } + + return mock.Object; + } + + private Token GetExpiredToken() + { + var notBefore = EpochTime.GetIntDate(DateTime.UtcNow - TimeSpan.FromMinutes(20)).ToString(); + var expires = EpochTime.GetIntDate(DateTime.UtcNow - TimeSpan.FromMinutes(10)).ToString(); + + return new TestToken(new Claim[] + { + new Claim(IdentityServiceClaimTypes.TokenUniqueId,Guid.NewGuid().ToString()), + new Claim(IdentityServiceClaimTypes.IssuedAt,"946684800"), // 01/01/2000 + new Claim(IdentityServiceClaimTypes.NotBefore,notBefore), + new Claim(IdentityServiceClaimTypes.Expires,expires) + }); + } + + private IScopeResolver GetScopeResolver(bool hasInvalidScopes) + { + var mock = new Mock(); + mock.Setup(m => m.ResolveScopesAsync(It.IsAny(), It.IsAny>())) + .ReturnsAsync((string clientId, IEnumerable scopes) => !hasInvalidScopes ? + ScopeResolutionResult.Valid(scopes.Select(s => CreateScope(s))) : + ScopeResolutionResult.Invalid(ProtocolErrorProvider.InvalidScope(scopes.First()))); + + return mock.Object; + + ApplicationScope CreateScope(string s) => + ApplicationScope.CanonicalScopes.TryGetValue(s, out var parsedScope) ? parsedScope : new ApplicationScope("resourceId", s); + } + + private class TestToken : Token + { + public TestToken(IEnumerable claims) + : base(claims) + { + } + + public override string Kind => "Test"; + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryContext.cs b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryContext.cs new file mode 100644 index 0000000000..ae091e3f7b --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryContext.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test +{ + public class InMemoryContext : InMemoryContext + { + public InMemoryContext(DbContextOptions options) : base(options) + { } + } + + public class InMemoryContext : + InMemoryContext + where TUser : IdentityUser + where TApplication : IdentityServiceApplication + { + public InMemoryContext(DbContextOptions options) : base(options) + { } + } + + public class InMemoryContext : IdentityServiceDbContext + where TUser : IdentityUser + where TRole : IdentityRole + where TUserKey : IEquatable + where TApplication : IdentityServiceApplication + where TApplicationKey : IEquatable + { + public InMemoryContext(DbContextOptions options) : base(options) + { } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseInMemoryDatabase("Scratch"); + } + } + + public abstract class InMemoryContext< + TUser, + TRole, + TUserKey, + TUserClaim, + TUserRole, + TUserLogin, + TRoleClaim, + TUserToken, + TApplication, + TScope, + TApplicationClaim, + TRedirectUri, + TApplicationKey> : + IdentityServiceDbContext + where TUser : IdentityUser + where TRole : IdentityRole + where TUserKey : IEquatable + where TUserClaim : IdentityUserClaim + where TUserRole : IdentityUserRole + where TUserLogin : IdentityUserLogin + where TRoleClaim : IdentityRoleClaim + where TUserToken : IdentityUserToken + where TApplication : IdentityServiceApplication + where TScope : IdentityServiceScope + where TApplicationClaim : IdentityServiceApplicationClaim + where TRedirectUri : IdentityServiceRedirectUri + where TApplicationKey : IEquatable + { + public InMemoryContext(DbContextOptions options) : base(options) + { } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseInMemoryDatabase("Scratch"); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryEFApplicationStoreTest.cs b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryEFApplicationStoreTest.cs new file mode 100644 index 0000000000..e6b3d1f087 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryEFApplicationStoreTest.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity.Service.Specification.Tests; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test +{ + public class InMemoryEFApplicationStoreTest : IdentityServiceSpecificationTestBase + { + protected override void AddApplicationStore(IServiceCollection services, object context = null) + { + services.AddSingleton>( + new ApplicationStore, IdentityServiceApplicationClaim, IdentityServiceRedirectUri, InMemoryContext, string, string>((InMemoryContext)context)); + } + + protected override IdentityServiceApplication CreateTestApplication() + { + return new IdentityServiceApplication + { + Id = Guid.NewGuid().ToString(), + ClientId = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + }; + } + + protected override object CreateTestContext() + { + return new InMemoryContext(new DbContextOptionsBuilder().Options); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test.csproj b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test.csproj new file mode 100644 index 0000000000..f69d5e95ea --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test.csproj @@ -0,0 +1,31 @@ + + + + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/ApplicationStoreTest.cs b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/ApplicationStoreTest.cs new file mode 100644 index 0000000000..20be012c33 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/ApplicationStoreTest.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test; +using Microsoft.AspNetCore.Identity.Service.Specification.Tests; +using Microsoft.AspNetCore.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test +{ + public class ApplicationStoreTest : IdentityServiceSpecificationTestBase, IClassFixture + { + private readonly ScratchDatabaseFixture _fixture; + + public ApplicationStoreTest(ScratchDatabaseFixture fixture) + { + _fixture = fixture; + } + + protected override void AddApplicationStore(IServiceCollection services, object context = null) + { + services.AddSingleton>( + new ApplicationStore, IdentityServiceApplicationClaim, IdentityServiceRedirectUri, IdentityServiceDbContext, string, string>((IdentityServiceDbContext)context)); + } + + public IdentityServiceDbContext CreateContext(bool delete = false) + { + var db = DbUtil.Create(_fixture.ConnectionString); + if (delete) + { + db.Database.EnsureDeleted(); + } + db.Database.EnsureCreated(); + return db; + } + + protected override IdentityServiceApplication CreateTestApplication() + { + return new IdentityServiceApplication + { + Id = Guid.NewGuid().ToString(), + ClientId = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString() + }; + } + + protected override object CreateTestContext() + { + return CreateContext(); + } + + private class TestContext : IdentityServiceDbContext + { + public TestContext(DbContextOptions options) : base(options) { } + } + + protected override bool ShouldSkipDbTests() => TestPlatformHelper.IsMono || !TestPlatformHelper.IsWindows; + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test.csproj b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test.csproj new file mode 100644 index 0000000000..efc879aef0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test.csproj @@ -0,0 +1,39 @@ + + + + + + netcoreapp2.0 + + + + + + PreserveNewest + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/DbUtil.cs b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/DbUtil.cs new file mode 100644 index 0000000000..4aab1f2072 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/DbUtil.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test +{ + public static class DbUtil + { + public static IServiceCollection ConfigureDbServices(string connectionString, IServiceCollection services = null) + { + return ConfigureDbServices(connectionString, services); + } + + public static IServiceCollection ConfigureDbServices(string connectionString, IServiceCollection services = null) where TContext : DbContext + { + if (services == null) + { + services = new ServiceCollection(); + } + services.AddSingleton(); + services.AddDbContext(options => options.UseSqlServer(connectionString)); + return services; + } + + public static TContext Create(string connectionString) where TContext : DbContext + { + var serviceProvider = ConfigureDbServices(connectionString).BuildServiceProvider(); + return serviceProvider.GetRequiredService(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/ScratchDatabaseFixture.cs b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/ScratchDatabaseFixture.cs new file mode 100644 index 0000000000..95e08de988 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/ScratchDatabaseFixture.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test.Utilities; +using Microsoft.EntityFrameworkCore.Internal; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test +{ + public class ScratchDatabaseFixture : IDisposable + { + private LazyRef _testStore; + + public ScratchDatabaseFixture() + { + _testStore = new LazyRef(() => SqlServerTestStore.CreateScratch()); + } + + public string ConnectionString => _testStore.Value.Connection.ConnectionString; + + public void Dispose() + { + if (_testStore.HasValue) + { + _testStore.Value?.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/SqlServerTestStore.cs b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/SqlServerTestStore.cs new file mode 100644 index 0000000000..dab5854282 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/SqlServerTestStore.cs @@ -0,0 +1,165 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Data.Common; +using System.Data.SqlClient; +using System.IO; +using System.Threading; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test.Utilities +{ + public class SqlServerTestStore : IDisposable + { + public const int CommandTimeout = 90; + + public static string CreateConnectionString(string name) + { + var connStrBuilder = new SqlConnectionStringBuilder(TestEnvironment.Config["Test:SqlServer:DefaultConnectionString"]) + { + InitialCatalog = name + }; + + return connStrBuilder.ConnectionString; + } + + public static SqlServerTestStore CreateScratch(bool createDatabase = true) + => new SqlServerTestStore(GetScratchDbName()).CreateTransient(createDatabase); + + private SqlConnection _connection; + private readonly string _name; + private bool _deleteDatabase; + + private SqlServerTestStore(string name) + { + _name = name; + } + + private static string GetScratchDbName() + { + string name; + do + { + name = "Scratch_" + Guid.NewGuid(); + } while (DatabaseExists(name) + || DatabaseFilesExist(name)); + + return name; + } + + private static void WaitForExists(SqlConnection connection) + { + var retryCount = 0; + while (true) + { + try + { + connection.Open(); + + connection.Close(); + + return; + } + catch (SqlException e) + { + if (++retryCount >= 30 + || (e.Number != 233 && e.Number != -2 && e.Number != 4060)) + { + throw; + } + + SqlConnection.ClearPool(connection); + + Thread.Sleep(100); + } + } + } + + private SqlServerTestStore CreateTransient(bool createDatabase) + { + _connection = new SqlConnection(CreateConnectionString(_name)); + + if (createDatabase) + { + using (var master = new SqlConnection(CreateConnectionString("master"))) + { + master.Open(); + using (var command = master.CreateCommand()) + { + command.CommandTimeout = CommandTimeout; + command.CommandText = $"{Environment.NewLine}CREATE DATABASE [{_name}]"; + + command.ExecuteNonQuery(); + + WaitForExists(_connection); + } + } + _connection.Open(); + } + + _deleteDatabase = true; + return this; + } + + private static bool DatabaseExists(string name) + { + using (var master = new SqlConnection(CreateConnectionString("master"))) + { + master.Open(); + + using (var command = master.CreateCommand()) + { + command.CommandTimeout = CommandTimeout; + command.CommandText = $@"SELECT COUNT(*) FROM sys.databases WHERE name = N'{name}'"; + + return (int) command.ExecuteScalar() > 0; + } + } + } + + private static bool DatabaseFilesExist(string name) + { + var userFolder = Environment.GetEnvironmentVariable("USERPROFILE") ?? + Environment.GetEnvironmentVariable("HOME"); + return userFolder != null + && (File.Exists(Path.Combine(userFolder, name + ".mdf")) + || File.Exists(Path.Combine(userFolder, name + "_log.ldf"))); + } + + private void DeleteDatabase(string name) + { + using (var master = new SqlConnection(CreateConnectionString("master"))) + { + master.Open(); + + using (var command = master.CreateCommand()) + { + command.CommandTimeout = CommandTimeout; + // Query will take a few seconds if (and only if) there are active connections + + // SET SINGLE_USER will close any open connections that would prevent the drop + command.CommandText + = string.Format(@"IF EXISTS (SELECT * FROM sys.databases WHERE name = N'{0}') + BEGIN + ALTER DATABASE [{0}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE [{0}]; + END", name); + + command.ExecuteNonQuery(); + } + } + } + + public DbConnection Connection => _connection; + + public void Dispose() + { + _connection.Dispose(); + + if (_deleteDatabase) + { + DeleteDatabase(_name); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/TestEnvironment.cs b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/TestEnvironment.cs new file mode 100644 index 0000000000..6d069f38b6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/Utilities/TestEnvironment.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test.Utilities +{ + public static class TestEnvironment + { + public static IConfiguration Config { get; } + + static TestEnvironment() + { + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("config.json", optional: true) + .AddJsonFile("config.test.json", optional: true) + .AddEnvironmentVariables(); + + Config = configBuilder.Build(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/config.json b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/config.json new file mode 100644 index 0000000000..a39052b5ed --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/config.json @@ -0,0 +1,7 @@ +{ + "Test": { + "SqlServer": { + "DefaultConnectionString": "Server=(localdb)\\MSSqlLocaldb;Integrated Security=true;MultipleActiveResultSets=true;Connect Timeout=30" + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStore.cs b/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStore.cs new file mode 100644 index 0000000000..7b08ee1579 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStore.cs @@ -0,0 +1,230 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service.InMemory.Test +{ + public class InMemoryStore + : IRedirectUriStore, + IApplicationClaimStore, + IApplicationClientSecretStore, + IApplicationScopeStore, + IQueryableApplicationStore + where TApplication : TestApplication + { + private readonly Dictionary _applications = new Dictionary(); + + public IQueryable Applications => _applications.Values.AsQueryable(); + + public Task AddClaimsAsync(TApplication application, IEnumerable claims, CancellationToken cancellationToken) + { + foreach (var claim in claims) + { + application.Claims.Add(new TestApplicationClaim { Type = claim.Type, Value = claim.Value }); + } + return Task.CompletedTask; + } + + public Task AddScopeAsync(TApplication application, string scope, CancellationToken cancellationToken) + { + application.Scopes.Add(new TestApplicationScope { Value = scope }); + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task CreateAsync(TApplication application, CancellationToken cancellationToken) + { + _applications.Add(application.Id, application); + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task DeleteAsync(TApplication application, CancellationToken cancellationToken) + { + _applications.Remove(application.Id); + return Task.FromResult(IdentityServiceResult.Success); + } + + public void Dispose() + { + } + + public Task FindByClientIdAsync(string clientId, CancellationToken cancellationToken) + { + return Task.FromResult(_applications.FirstOrDefault(a => a.Value.ClientId.Equals(clientId)).Value); + } + + public Task FindByIdAsync(string applicationId, CancellationToken cancellationToken) + { + return Task.FromResult(_applications.TryGetValue(applicationId, out var application) ? application : application); + } + + public Task FindByNameAsync(string name, CancellationToken cancellationToken) + { + return Task.FromResult(_applications.FirstOrDefault(a => a.Value.Name.Equals(name)).Value); + } + + public Task> FindByUserIdAsync(string userId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> FindRegisteredLogoutUrisAsync(TApplication app, CancellationToken cancellationToken) + { + return Task.FromResult(app.RedirectUris.Where(ru => ru.IsLogout).Select(ru => ru.Value)); + } + + public Task> FindRegisteredUrisAsync(TApplication app, CancellationToken cancellationToken) + { + return Task.FromResult(app.RedirectUris.Where(ru => !ru.IsLogout).Select(ru => ru.Value)); + } + + public Task> FindScopesAsync(TApplication application, CancellationToken cancellationToken) + { + return Task.FromResult(application.Scopes.Select(s => s.Value)); + } + + public Task GetApplicationClientIdAsync(TApplication application, CancellationToken cancellationToken) + { + return Task.FromResult(application.ClientId); + } + + public Task GetApplicationIdAsync(TApplication application, CancellationToken cancellationToken) + { + return Task.FromResult(application.Id); + } + + public Task GetApplicationNameAsync(TApplication application, CancellationToken cancellationToken) + { + return Task.FromResult(application.Name); + } + + public Task GetApplicationUserIdAsync(TApplication application, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetClaimsAsync(TApplication application, CancellationToken cancellationToken) + { + return Task.FromResult>(application.Claims.Select(claim => new Claim(claim.Type, claim.Value)).ToList()); + } + + public Task GetClientSecretHashAsync(TApplication application, CancellationToken cancellationToken) + { + return Task.FromResult(application.ClientSecretHash); + } + + public Task HasClientSecretAsync(TApplication application, CancellationToken cancellationToken) + { + return Task.FromResult(application.ClientSecretHash != null); + } + + public Task RegisterLogoutRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) + { + app.RedirectUris.Add(new TestApplicationRedirectUri { IsLogout = true, Value = redirectUri }); + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task RegisterRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) + { + app.RedirectUris.Add(new TestApplicationRedirectUri { IsLogout = false, Value = redirectUri }); + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task RemoveClaimsAsync(TApplication application, IEnumerable claims, CancellationToken cancellationToken) + { + foreach (var claim in claims) + { + var foundClaim = application.Claims.FirstOrDefault(c => c.Type == claim.Type && c.Value == claim.Value); + if (foundClaim != null) + { + application.Claims.Remove(foundClaim); + } + } + + return Task.CompletedTask; + } + + public Task RemoveScopeAsync(TApplication application, string scope, CancellationToken cancellationToken) + { + var foundScope = application.Scopes.FirstOrDefault(s => s.Value == scope); + if (foundScope != null) + { + application.Scopes.Remove(foundScope); + } + + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task ReplaceClaimAsync(TApplication application, Claim claim, Claim newClaim, CancellationToken cancellationToken) + { + var matchedClaims = application.Claims.Where(uc => uc.Value == claim.Value && uc.Type == claim.Type).ToList(); + foreach (var matchedClaim in matchedClaims) + { + matchedClaim.Type = newClaim.Type; + matchedClaim.Value = newClaim.Value; + } + + return Task.CompletedTask; + } + + public Task SetClientSecretHashAsync(TApplication application, string clientSecretHash, CancellationToken cancellationToken) + { + application.ClientSecretHash = clientSecretHash; + return Task.CompletedTask; + } + + public Task UnregisterLogoutRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) + { + var logoutUri = app.RedirectUris.FirstOrDefault(ru => ru.IsLogout && ru.Value == redirectUri); + if (logoutUri != null) + { + app.RedirectUris.Remove(logoutUri); + } + + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task UnregisterRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) + { + var logoutUri = app.RedirectUris.FirstOrDefault(ru => !ru.IsLogout && ru.Value == redirectUri); + if (logoutUri != null) + { + app.RedirectUris.Remove(logoutUri); + } + + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task UpdateAsync(TApplication application, CancellationToken cancellationToken) + { + _applications[application.Id] = application; + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task UpdateLogoutRedirectUriAsync(TApplication app, string oldRedirectUri, string newRedirectUri, CancellationToken cancellationToken) + { + var old = app.RedirectUris.Single(ru => ru.IsLogout && ru.Value == oldRedirectUri); + old.Value = newRedirectUri; + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task UpdateRedirectUriAsync(TApplication app, string oldRedirectUri, string newRedirectUri, CancellationToken cancellationToken) + { + var old = app.RedirectUris.Single(ru => !ru.IsLogout && ru.Value == oldRedirectUri); + old.Value = newRedirectUri; + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task UpdateScopeAsync(TApplication application, string oldScope, string newScope, CancellationToken cancellationToken) + { + var old = application.Scopes.Single(s => s.Value == oldScope); + old.Value = newScope; + return Task.FromResult(IdentityServiceResult.Success); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStoreTest.cs b/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStoreTest.cs new file mode 100644 index 0000000000..f98057de76 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStoreTest.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Identity.Service.Specification.Tests; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Identity.Service.InMemory.Test +{ + public class InMemoryStoreTest : IdentityServiceSpecificationTestBase + { + protected override void AddApplicationStore(IServiceCollection services, object context = null) + { + services.AddSingleton>((InMemoryStore)context); + } + + protected override TestApplication CreateTestApplication() + { + return new TestApplication + { + Id = Guid.NewGuid().ToString(), + ClientId = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString() + }; + } + + protected override object CreateTestContext() + { + return new InMemoryStore(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/Microsoft.AspNetCore.Identity.Service.InMemory.Test.csproj b/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/Microsoft.AspNetCore.Identity.Service.InMemory.Test.csproj new file mode 100644 index 0000000000..7a4f83b0fc --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/Microsoft.AspNetCore.Identity.Service.InMemory.Test.csproj @@ -0,0 +1,29 @@ + + + + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/TestApplication.cs b/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/TestApplication.cs new file mode 100644 index 0000000000..61458cbdb3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/TestApplication.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Identity.Service.InMemory.Test +{ + public class TestApplication + { + public string Id { get; set; } + public string ClientId { get; set; } + public string Name { get; set; } + public string ClientSecretHash { get; set; } + public IList Claims { get; set; } = new List(); + public IList Scopes { get; set; } = new List(); + public IList RedirectUris { get; set; } = new List(); + } + + public class TestUser + { + } + + public class TestApplicationClaim + { + public string Id { get; set; } + public string Type { get; set; } + public string Value { get; set; } + } + + public class TestApplicationScope + { + public string Id { get; set; } + public string Value { get; set; } + } + + public class TestApplicationRedirectUri + { + public string Id { get; set; } + public string Value { get; set; } + public bool IsLogout { get; set; } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Test/ClientApplicationValidatorTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Test/ClientApplicationValidatorTest.cs new file mode 100644 index 0000000000..bcb623409e --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Test/ClientApplicationValidatorTest.cs @@ -0,0 +1,115 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ClientApplicationValidatorTest + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ValidateClientIdAsync_ChecksThatTheClientIdExist(bool exists) + { + // Arrange + var options = new IdentityServiceOptions(); + var store = new Mock>(); + store.Setup(s => s.FindByClientIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(exists ? new IdentityServiceApplication() : null); + + var manager = new ApplicationManager( + store.Object, + Mock.Of>(), + Array.Empty>(), + Mock.Of>>()); + + var clientValidator = new ClientApplicationValidator( + Options.Create(options), + GetSessionManager(), + manager, + new ProtocolErrorProvider()); + + // Act + var validation = await clientValidator.ValidateClientIdAsync("clientId"); + + // Assert + Assert.Equal(exists, validation); + } + + [Fact] + public async Task ValidateClientCredentialsAsync_DelegatesToApplicationManager() + { + // Arrange + var options = new IdentityServiceOptions(); + var store = new Mock>(); + store.Setup(s => s.FindByClientIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new IdentityServiceApplication()); + store.As>() + .Setup(s => s.HasClientSecretAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + var manager = new ApplicationManager( + store.Object, + Mock.Of>(), + Array.Empty>(), + Mock.Of>>()); + + var clientValidator = new ClientApplicationValidator( + Options.Create(options), + GetSessionManager(), + manager, + new ProtocolErrorProvider()); + + // Act + var validation = await clientValidator.ValidateClientCredentialsAsync("clientId", null); + + // Assert + Assert.True(validation); + } + + private SessionManager GetSessionManager() + { + return new TestSessionManager( + Mock.Of>(), + Mock.Of>(), + Mock.Of>(), + new TimeStampManager(), + Mock.Of(), + new ProtocolErrorProvider()); + } + + private class TestSessionManager : SessionManager + { + public TestSessionManager( + IOptions options, + IOptions identityOptions, + IOptionsSnapshot cookieOptions, + ITimeStampManager timeStampManager, + IHttpContextAccessor contextAccessor, + ProtocolErrorProvider errorProvider) : + base(options, identityOptions, cookieOptions, timeStampManager, contextAccessor, errorProvider) + { + } + + public override Task CreateSessionAsync(string userId, string clientId) + { + throw new NotImplementedException(); + } + + public override Task IsAuthorizedAsync(AuthorizationRequest request) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Test/IdentityOptionsServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Test/IdentityOptionsServiceCollectionExtensionsTest.cs new file mode 100644 index 0000000000..ca6c3bb9be --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Test/IdentityOptionsServiceCollectionExtensionsTest.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class IdentityOptionsServiceCollectionExtensionsTest + { + [Fact] + public void CanResolveIdentityServiceOptions() + { + var services = new ServiceCollection(); + services.AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity(); + services.AddIdentityService(o => { }); + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + Assert.NotNull(options.Value); + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Test/Microsoft.AspNetCore.Identity.Service.Test.csproj b/test/Microsoft.AspNetCore.Identity.Service.Test/Microsoft.AspNetCore.Identity.Service.Test.csproj new file mode 100644 index 0000000000..1c76a3048b --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Test/Microsoft.AspNetCore.Identity.Service.Test.csproj @@ -0,0 +1,25 @@ + + + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + +