[Blazor] Adds an authentication library for Blazor webassembly (#18851)
* Adds a Microsoft.AspNetCore.Components.WebAssembly.Authentication
library for performing authentication in Blazor webassembly.
* Includes a default implementation that supports OIDC capable IdPs
using oidc-client.js
* Includes multiple primitives to deal with authentication flows and
supports acquiring access tokens to call APIs.
* RemoteAuthenticatorView is responsible for handling authentication
operations at the user interface level.
* RemoteAuthenticatorService is responsible for handling the lower
level authentication details by using JavaScript interop to interact
with the underlying javascript library implementing the auth protocol.
* SignOutSessionStateManager handles CSRF protection for the logout
path.
* IAccessTokenProvider handles provisioning access tokens to call APIs.
This commit is contained in:
parent
4628dfb005
commit
0dbb01bd8c
|
|
@ -154,6 +154,9 @@ and are generated based on the last package release.
|
||||||
<LatestPackageReference Include="Microsoft.AspNetCore.Components.Forms" Version="$(MicrosoftAspNetCoreComponentsFormsPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.AspNetCore.Components.Forms" Version="$(MicrosoftAspNetCoreComponentsFormsPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.AspNetCore.Components.Web" Version="$(MicrosoftAspNetCoreComponentsWebPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.AspNetCore.Components.Web" Version="$(MicrosoftAspNetCoreComponentsWebPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
|
||||||
|
<LatestPackageReference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Version="$(MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion)" />
|
||||||
|
<LatestPackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="$(MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion)" />
|
||||||
|
<LatestPackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="$(MicrosoftAspNetCoreIdentityUIPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.AspNetCore.Server.IntegrationTesting" Version="$(MicrosoftAspNetCoreServerIntegrationTestingPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.AspNetCore.Server.IntegrationTesting" Version="$(MicrosoftAspNetCoreServerIntegrationTestingPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<ProjectReferenceProvider Include="Mono.WebAssembly.Interop" ProjectPath="$(RepoRoot)src\Components\Blazor\Mono.WebAssembly.Interop\src\Mono.WebAssembly.Interop.csproj" />
|
<ProjectReferenceProvider Include="Mono.WebAssembly.Interop" ProjectPath="$(RepoRoot)src\Components\Blazor\Mono.WebAssembly.Interop\src\Mono.WebAssembly.Interop.csproj" />
|
||||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor.Server" ProjectPath="$(RepoRoot)src\Components\Blazor\Server\src\Microsoft.AspNetCore.Blazor.Server.csproj" />
|
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor.Server" ProjectPath="$(RepoRoot)src\Components\Blazor\Server\src\Microsoft.AspNetCore.Blazor.Server.csproj" />
|
||||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation" ProjectPath="$(RepoRoot)src\Components\Blazor\Validation\src\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj" />
|
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation" ProjectPath="$(RepoRoot)src\Components\Blazor\Validation\src\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj" />
|
||||||
|
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" ProjectPath="$(RepoRoot)src\Components\Blazor\WebAssembly.Authentication\src\Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj" />
|
||||||
<ProjectReferenceProvider Include="BlazorServerApp" ProjectPath="$(RepoRoot)src\Components\Samples\BlazorServerApp\BlazorServerApp.csproj" />
|
<ProjectReferenceProvider Include="BlazorServerApp" ProjectPath="$(RepoRoot)src\Components\Samples\BlazorServerApp\BlazorServerApp.csproj" />
|
||||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor" ProjectPath="$(RepoRoot)src\Components\Blazor\Blazor\src\Microsoft.AspNetCore.Blazor.csproj" RefProjectPath="$(RepoRoot)src\Components\Blazor\Blazor\ref\Microsoft.AspNetCore.Blazor.csproj" />
|
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor" ProjectPath="$(RepoRoot)src\Components\Blazor\Blazor\src\Microsoft.AspNetCore.Blazor.csproj" RefProjectPath="$(RepoRoot)src\Components\Blazor\Blazor\ref\Microsoft.AspNetCore.Blazor.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,11 @@
|
||||||
<SystemThreadingTasksExtensionsPackageVersion>4.5.2</SystemThreadingTasksExtensionsPackageVersion>
|
<SystemThreadingTasksExtensionsPackageVersion>4.5.2</SystemThreadingTasksExtensionsPackageVersion>
|
||||||
<!-- Packages developed by @aspnet, but manually updated as necessary. -->
|
<!-- Packages developed by @aspnet, but manually updated as necessary. -->
|
||||||
<LibuvPackageVersion>1.10.0</LibuvPackageVersion>
|
<LibuvPackageVersion>1.10.0</LibuvPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreIdentityUIPackageVersion>3.1.0</MicrosoftAspNetCoreIdentityUIPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>3.1.0</MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>
|
||||||
|
<MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>3.1.0</MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>
|
||||||
|
<MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion>3.1.0</MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreComponentsAuthorizationPackageVersion>3.1.0</MicrosoftAspNetCoreComponentsAuthorizationPackageVersion>
|
||||||
<MicrosoftAspNetWebApiClientPackageVersion>5.2.6</MicrosoftAspNetWebApiClientPackageVersion>
|
<MicrosoftAspNetWebApiClientPackageVersion>5.2.6</MicrosoftAspNetWebApiClientPackageVersion>
|
||||||
<!-- Partner teams -->
|
<!-- Partner teams -->
|
||||||
<MicrosoftAzureKeyVaultPackageVersion>2.3.2</MicrosoftAzureKeyVaultPackageVersion>
|
<MicrosoftAzureKeyVaultPackageVersion>2.3.2</MicrosoftAzureKeyVaultPackageVersion>
|
||||||
|
|
@ -273,6 +278,9 @@
|
||||||
<MicrosoftAspNetCoreComponentsFormsPackageVersion>$(MicrosoftAspNetCoreComponentsPackageVersion)</MicrosoftAspNetCoreComponentsFormsPackageVersion>
|
<MicrosoftAspNetCoreComponentsFormsPackageVersion>$(MicrosoftAspNetCoreComponentsPackageVersion)</MicrosoftAspNetCoreComponentsFormsPackageVersion>
|
||||||
<MicrosoftAspNetCoreComponentsWebPackageVersion>$(MicrosoftAspNetCoreComponentsPackageVersion)</MicrosoftAspNetCoreComponentsWebPackageVersion>
|
<MicrosoftAspNetCoreComponentsWebPackageVersion>$(MicrosoftAspNetCoreComponentsPackageVersion)</MicrosoftAspNetCoreComponentsWebPackageVersion>
|
||||||
<MicrosoftAspNetCoreMvcPackageVersion>$(MicrosoftAspNetCoreComponentsPackageVersion)</MicrosoftAspNetCoreMvcPackageVersion>
|
<MicrosoftAspNetCoreMvcPackageVersion>$(MicrosoftAspNetCoreComponentsPackageVersion)</MicrosoftAspNetCoreMvcPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion>$(MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion)</MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>$(MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion)</MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>
|
||||||
|
<MicrosoftAspNetCoreIdentityUIPackageVersion>$(MicrosoftAspNetCoreIdentityUIPackageVersion)</MicrosoftAspNetCoreIdentityUIPackageVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<!-- Restore feeds -->
|
<!-- Restore feeds -->
|
||||||
<PropertyGroup Label="Restore feeds">
|
<PropertyGroup Label="Restore feeds">
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestContentPackage", "test\
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.TestServer", "test\testassets\TestServer\Components.TestServer.csproj", "{EACC194A-8C1B-424D-B8FE-330E14CAF525}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.TestServer", "test\testassets\TestServer\Components.TestServer.csproj", "{EACC194A-8C1B-424D-B8FE-330E14CAF525}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebAssembly.Authentication", "WebAssembly.Authentication", "{C4D74173-702B-428A-B689-1A9AF51CE356}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebAssembly.Authentication", "Blazor\WebAssembly.Authentication\src\Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj", "{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests", "Blazor\WebAssembly.Authentication\test\Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests.csproj", "{E450CCAC-6E03-4306-9919-47AB0EE98657}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasm.Authentication.Server", "Blazor\testassets\Wasm.Authentication.Server\Wasm.Authentication.Server.csproj", "{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasm.Authentication.Client", "Blazor\testassets\Wasm.Authentication.Client\Wasm.Authentication.Client.csproj", "{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasm.Authentication.Shared", "Blazor\testassets\Wasm.Authentication.Shared\Wasm.Authentication.Shared.csproj", "{A82A1C13-C452-423A-9287-A7E52F6A43E8}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -433,6 +445,66 @@ Global
|
||||||
{EACC194A-8C1B-424D-B8FE-330E14CAF525}.Release|x64.Build.0 = Release|Any CPU
|
{EACC194A-8C1B-424D-B8FE-330E14CAF525}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{EACC194A-8C1B-424D-B8FE-330E14CAF525}.Release|x86.ActiveCfg = Release|Any CPU
|
{EACC194A-8C1B-424D-B8FE-330E14CAF525}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{EACC194A-8C1B-424D-B8FE-330E14CAF525}.Release|x86.Build.0 = Release|Any CPU
|
{EACC194A-8C1B-424D-B8FE-330E14CAF525}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -477,6 +549,12 @@ Global
|
||||||
{DA17FB91-EF6F-4C34-A5D4-1A0F7E42D699} = {10C06583-8506-42DE-863E-EAD48A1F7579}
|
{DA17FB91-EF6F-4C34-A5D4-1A0F7E42D699} = {10C06583-8506-42DE-863E-EAD48A1F7579}
|
||||||
{BC3DDF14-4961-49AB-8F19-2A23F535B0DE} = {10C06583-8506-42DE-863E-EAD48A1F7579}
|
{BC3DDF14-4961-49AB-8F19-2A23F535B0DE} = {10C06583-8506-42DE-863E-EAD48A1F7579}
|
||||||
{EACC194A-8C1B-424D-B8FE-330E14CAF525} = {10C06583-8506-42DE-863E-EAD48A1F7579}
|
{EACC194A-8C1B-424D-B8FE-330E14CAF525} = {10C06583-8506-42DE-863E-EAD48A1F7579}
|
||||||
|
{C4D74173-702B-428A-B689-1A9AF51CE356} = {B29FB58D-FAE5-405E-9695-BCF93582BE9A}
|
||||||
|
{E5C5D4E9-2442-4C4C-94E7-1EDB8ADAD1FE} = {C4D74173-702B-428A-B689-1A9AF51CE356}
|
||||||
|
{E450CCAC-6E03-4306-9919-47AB0EE98657} = {C4D74173-702B-428A-B689-1A9AF51CE356}
|
||||||
|
{B88282F3-1DEF-4B06-8AD6-5A9EF6BAFEEB} = {CBD2BB24-3EC3-4950-ABE4-8C521D258DCD}
|
||||||
|
{7EF0A33A-3E96-4DF5-973C-257CFE6F79D8} = {CBD2BB24-3EC3-4950-ABE4-8C521D258DCD}
|
||||||
|
{A82A1C13-C452-423A-9287-A7E52F6A43E8} = {CBD2BB24-3EC3-4950-ABE4-8C521D258DCD}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {27A36094-AA50-4FFD-ADE6-C055E391F741}
|
SolutionGuid = {27A36094-AA50-4FFD-ADE6-C055E391F741}
|
||||||
|
|
|
||||||
|
|
@ -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 Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
internal class DefaultRemoteApplicationPathsProvider<TProviderOptions> : IRemoteAuthenticationPathsProvider where TProviderOptions : class, new()
|
||||||
|
{
|
||||||
|
private readonly IOptions<RemoteAuthenticationOptions<TProviderOptions>> _options;
|
||||||
|
|
||||||
|
public DefaultRemoteApplicationPathsProvider(IOptions<RemoteAuthenticationOptions<TProviderOptions>> options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteAuthenticationApplicationPathsOptions ApplicationPaths => _options.Value.AuthenticationPaths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Components.WebAssembly.Authentication.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is an internal API that supports the Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
/// infrastructure and not subject to the same compatibility standards as public APIs.
|
||||||
|
/// It may be changed or removed without notice in any release.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRemoteAuthenticationPathsProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is an internal API that supports the Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
/// infrastructure and not subject to the same compatibility standards as public APIs.
|
||||||
|
/// It may be changed or removed without notice in any release.
|
||||||
|
/// </summary>
|
||||||
|
RemoteAuthenticationApplicationPathsOptions ApplicationPaths { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,312 @@
|
||||||
|
import { UserManager, UserManagerSettings, User } from 'oidc-client'
|
||||||
|
|
||||||
|
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||||
|
|
||||||
|
type ExtendedUserManagerSettings = Writeable<UserManagerSettings & AuthorizeServiceSettings>
|
||||||
|
|
||||||
|
type OidcAuthorizeServiceSettings = ExtendedUserManagerSettings | ApiAuthorizationSettings;
|
||||||
|
|
||||||
|
function isApiAuthorizationSettings(settings: OidcAuthorizeServiceSettings): settings is ApiAuthorizationSettings {
|
||||||
|
return settings.hasOwnProperty('configurationEndpoint');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthorizeServiceSettings {
|
||||||
|
defaultScopes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiAuthorizationSettings {
|
||||||
|
configurationEndpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessTokenRequestOptions {
|
||||||
|
scopes: string[];
|
||||||
|
returnUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessTokenResult {
|
||||||
|
status: AccessTokenResultStatus;
|
||||||
|
token?: AccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessToken {
|
||||||
|
value: string;
|
||||||
|
expires: Date;
|
||||||
|
grantedScopes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AccessTokenResultStatus {
|
||||||
|
Success = 'success',
|
||||||
|
RequiresRedirect = 'requiresRedirect'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AuthenticationResultStatus {
|
||||||
|
Redirect = 'redirect',
|
||||||
|
Success = 'success',
|
||||||
|
Failure = 'failure',
|
||||||
|
OperationCompleted = 'operation-completed'
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AuthenticationResult {
|
||||||
|
status: AuthenticationResultStatus;
|
||||||
|
state?: any;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizeService {
|
||||||
|
getUser(): Promise<any>;
|
||||||
|
getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
|
||||||
|
signIn(state: any): Promise<AuthenticationResult>;
|
||||||
|
completeSignIn(state: any): Promise<AuthenticationResult>;
|
||||||
|
signOut(state: any): Promise<AuthenticationResult>;
|
||||||
|
completeSignOut(url: string): Promise<AuthenticationResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OidcAuthorizeService implements AuthorizeService {
|
||||||
|
private _userManager: UserManager;
|
||||||
|
|
||||||
|
constructor(userManager: UserManager) {
|
||||||
|
this._userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser() {
|
||||||
|
const user = await this._userManager.getUser();
|
||||||
|
return user && user.profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult> {
|
||||||
|
const user = await this._userManager.getUser();
|
||||||
|
if (hasValidAccessToken(user) && hasAllScopes(request, user.scopes)) {
|
||||||
|
return {
|
||||||
|
status: AccessTokenResultStatus.Success,
|
||||||
|
token: {
|
||||||
|
grantedScopes: user.scopes,
|
||||||
|
expires: getExpiration(user.expires_in),
|
||||||
|
value: user.access_token
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const parameters = request && request.scopes ?
|
||||||
|
{ scope: request.scopes.join(' ') } : undefined;
|
||||||
|
|
||||||
|
const newUser = await this._userManager.signinSilent(parameters);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: AccessTokenResultStatus.Success,
|
||||||
|
token: {
|
||||||
|
grantedScopes: newUser.scopes,
|
||||||
|
expires: getExpiration(newUser.expires_in),
|
||||||
|
value: newUser.access_token
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: AccessTokenResultStatus.RequiresRedirect
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasValidAccessToken(user: User | null): user is User {
|
||||||
|
return !!(user && user.access_token && !user.expired && user.scopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExpiration(expiresIn: number) {
|
||||||
|
const now = new Date();
|
||||||
|
now.setTime(now.getTime() + expiresIn * 1000);
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAllScopes(request: AccessTokenRequestOptions | undefined, currentScopes: string[]) {
|
||||||
|
const set = new Set(currentScopes);
|
||||||
|
if (request && request.scopes) {
|
||||||
|
for (let current of request.scopes) {
|
||||||
|
if (!set.has(current)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async signIn(state: any) {
|
||||||
|
try {
|
||||||
|
await this._userManager.clearStaleState();
|
||||||
|
await this._userManager.signinSilent(this.createArguments());
|
||||||
|
return this.success(state);
|
||||||
|
} catch (silentError) {
|
||||||
|
try {
|
||||||
|
await this._userManager.clearStaleState();
|
||||||
|
await this._userManager.signinRedirect(this.createArguments(state));
|
||||||
|
return this.redirect();
|
||||||
|
} catch (redirectError) {
|
||||||
|
console.log("Redirect authentication error: ", redirectError);
|
||||||
|
return this.error(redirectError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async completeSignIn(url: string) {
|
||||||
|
const requiresLogin = await this.loginRequired(url);
|
||||||
|
const stateExists = await this.stateExists(url);
|
||||||
|
try {
|
||||||
|
const user = await this._userManager.signinCallback(url);
|
||||||
|
if (window.self !== window.top) {
|
||||||
|
return this.operationCompleted();
|
||||||
|
} else {
|
||||||
|
return this.success(user && user.state);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (requiresLogin || window.self !== window.top || !stateExists) {
|
||||||
|
return this.operationCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.error('There was an error signing in.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async signOut(state: any) {
|
||||||
|
try {
|
||||||
|
if (!(await this._userManager.metadataService.getEndSessionEndpoint())) {
|
||||||
|
await this._userManager.removeUser();
|
||||||
|
return this.success(state);
|
||||||
|
}
|
||||||
|
await this._userManager.signoutRedirect(this.createArguments(state));
|
||||||
|
return this.redirect();
|
||||||
|
} catch (redirectSignOutError) {
|
||||||
|
return this.error(redirectSignOutError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async completeSignOut(url: string) {
|
||||||
|
try {
|
||||||
|
if (await this.stateExists(url)) {
|
||||||
|
const response = await this._userManager.signoutCallback(url);
|
||||||
|
return this.success(response && response.state);
|
||||||
|
} else {
|
||||||
|
return this.operationCompleted();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return this.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async stateExists(url: string) {
|
||||||
|
const stateParam = new URLSearchParams(new URL(url).search).get('state');
|
||||||
|
if (stateParam) {
|
||||||
|
return await this._userManager.settings.stateStore!.get(stateParam);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loginRequired(url: string) {
|
||||||
|
const errorParameter = new URLSearchParams(new URL(url).search).get('error');
|
||||||
|
if (errorParameter) {
|
||||||
|
const error = await this._userManager.settings.stateStore!.get(errorParameter);
|
||||||
|
return error === 'login_required';
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createArguments(state?: any) {
|
||||||
|
return { useReplaceToNavigate: true, data: state };
|
||||||
|
}
|
||||||
|
|
||||||
|
private error(message: string) {
|
||||||
|
return { status: AuthenticationResultStatus.Failure, errorMessage: message };
|
||||||
|
}
|
||||||
|
|
||||||
|
private success(state: any) {
|
||||||
|
return { status: AuthenticationResultStatus.Success, state };
|
||||||
|
}
|
||||||
|
|
||||||
|
private redirect() {
|
||||||
|
return { status: AuthenticationResultStatus.Redirect };
|
||||||
|
}
|
||||||
|
|
||||||
|
private operationCompleted() {
|
||||||
|
return { status: AuthenticationResultStatus.OperationCompleted };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthenticationService {
|
||||||
|
|
||||||
|
static _infrastructureKey = 'Microsoft.AspNetCore.Components.WebAssembly.Authentication';
|
||||||
|
static _initialized = false;
|
||||||
|
static instance: OidcAuthorizeService;
|
||||||
|
|
||||||
|
public static async init(settings: UserManagerSettings & AuthorizeServiceSettings) {
|
||||||
|
if (!AuthenticationService._initialized) {
|
||||||
|
AuthenticationService._initialized = true;
|
||||||
|
const userManager = await this.createUserManager(settings);
|
||||||
|
AuthenticationService.instance = new OidcAuthorizeService(userManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getUser() {
|
||||||
|
return AuthenticationService.instance.getUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getAccessToken() {
|
||||||
|
return AuthenticationService.instance.getAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static signIn(state: any) {
|
||||||
|
return AuthenticationService.instance.signIn(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static completeSignIn(url: string) {
|
||||||
|
return AuthenticationService.instance.completeSignIn(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static signOut(state: any) {
|
||||||
|
return AuthenticationService.instance.signOut(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static completeSignOut(url: string) {
|
||||||
|
return AuthenticationService.instance.completeSignOut(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async createUserManager(settings: OidcAuthorizeServiceSettings): Promise<UserManager> {
|
||||||
|
let finalSettings: UserManagerSettings;
|
||||||
|
if (isApiAuthorizationSettings(settings)) {
|
||||||
|
let response = await fetch(settings.configurationEndpoint);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Could not load settings from '${settings.configurationEndpoint}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadedSettings = await response.json();
|
||||||
|
|
||||||
|
window.sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`, JSON.stringify(settings));
|
||||||
|
|
||||||
|
downloadedSettings.automaticSilentRenew = true;
|
||||||
|
downloadedSettings.includeIdTokenInSilentRenew = true;
|
||||||
|
|
||||||
|
finalSettings = downloadedSettings;
|
||||||
|
} else {
|
||||||
|
if (!settings.scope) {
|
||||||
|
settings.scope = settings.defaultScopes.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
finalSettings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userManager = new UserManager(finalSettings);
|
||||||
|
|
||||||
|
userManager.events.addUserSignedOut(async () => {
|
||||||
|
await userManager.removeUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
return userManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window { AuthenticationService: AuthenticationService; }
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AuthenticationService = AuthenticationService;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.js
|
||||||
|
*.js.map
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run build:release",
|
||||||
|
"build:release": "webpack --mode production --env.production --env.configuration=Release",
|
||||||
|
"build:debug": "webpack --mode development --env.configuration=Debug",
|
||||||
|
"watch": "webpack --watch --mode development"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"ts-loader": "^6.2.1",
|
||||||
|
"typescript": "^3.7.5",
|
||||||
|
"webpack": "^4.41.5",
|
||||||
|
"webpack-cli": "^3.3.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"oidc-client": "^1.10.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2019",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": [ "DOM", "ES2019" ],
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = env => {
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry: './AuthenticationService.ts',
|
||||||
|
devtool: env && env.production ? 'none' : 'source-map',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.js'],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: 'AuthenticationService.js',
|
||||||
|
path: path.resolve(__dirname, 'dist', env.configuration),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,83 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||||
|
|
||||||
|
<Sdk Name="Yarn.MSBuild" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<Description>Build client-side authentication for single-page applications (SPAs).</Description>
|
||||||
|
<IsShippingPackage>true</IsShippingPackage>
|
||||||
|
<HasReferenceAssembly>false</HasReferenceAssembly>
|
||||||
|
<RazorLangVersion>3.0</RazorLangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Components.Web" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<YarnWorkingDir>$(MSBuildThisFileDirectory)Interop\</YarnWorkingDir>
|
||||||
|
<ResolveCurrentProjectStaticWebAssetsInputsDependsOn>
|
||||||
|
CompileInterop;
|
||||||
|
$(ResolveCurrentProjectStaticWebAssetsInputsDependsOn)
|
||||||
|
</ResolveCurrentProjectStaticWebAssetsInputsDependsOn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<YarnInputs Include="$(YarnWorkingDir)**" Exclude="$(YarnWorkingDir)node_modules\**;$(YarnWorkingDir)*.d.ts;$(YarnWorkingDir)dist\**" />
|
||||||
|
<YarnOutputs Include="$(YarnWorkingDir)dist\$(Configuration)\AuthenticationService.js" />
|
||||||
|
<YarnOutputs Include="$(YarnWorkingDir)dist\$(Configuration)\AuthenticationService.js.map" Condition="'$(Configuration)' == 'Debug'" />
|
||||||
|
|
||||||
|
<Content Remove="$(YarnWorkingDir)**" />
|
||||||
|
<None Include="$(YarnWorkingDir)*" Exclude="$(YarnWorkingDir)node_modules\**" />
|
||||||
|
|
||||||
|
<UpToDateCheckInput Include="@(YarnInputs)" Set="StaticWebassets" />
|
||||||
|
<UpToDateCheckOutput Include="@(YarnOutputs)" Set="StaticWebassets" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="_CreateInteropHash" BeforeTargets="CompileInterop" Condition="'$(DesignTimeBuild)' != 'true'">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<InteropCompilationCacheFile>$(IntermediateOutputPath)interop.cache</InteropCompilationCacheFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Hash ItemsToHash="@(YarnInputs)">
|
||||||
|
<Output TaskParameter="HashResult" PropertyName="_YarnInputsHash" />
|
||||||
|
</Hash>
|
||||||
|
|
||||||
|
<WriteLinesToFile Lines="$(_YarnInputsHash)" File="$(InteropCompilationCacheFile)" Overwrite="True" WriteOnlyWhenDifferent="True" />
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FileWrites Include="$(InteropCompilationCacheFile)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="CompileInterop" Condition="'$(DesignTimeBuild)' != 'true'" Inputs="$(InteropCompilationCacheFile)" Outputs="@(YarnOutputs)">
|
||||||
|
<Yarn Command="install --mutex network" WorkingDirectory="$(YarnWorkingDir)" />
|
||||||
|
<Yarn Command="run build:release" WorkingDirectory="$(YarnWorkingDir)" Condition="'$(Configuration)' == 'Release'" />
|
||||||
|
<Yarn Command="run build:debug" WorkingDirectory="$(YarnWorkingDir)" Condition="'$(Configuration)' == 'Debug'" />
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<_InteropBuildOutput Include="$(YarnWorkingDir)dist\$(Configuration)\**" Exclude="$(YarnWorkingDir)dist\.gitignore" />
|
||||||
|
|
||||||
|
<StaticWebAsset Include="@(_InteropBuildOutput->'%(FullPath)')">
|
||||||
|
<SourceType></SourceType>
|
||||||
|
<SourceId>$(PackageId)</SourceId>
|
||||||
|
<ContentRoot>$([MSBuild]::NormalizeDirectory('$(YarnWorkingDir)\dist\$(Configuration)'))</ContentRoot>
|
||||||
|
<BasePath>_content/$(PackageId)</BasePath>
|
||||||
|
<RelativePath>$([System.String]::Copy('%(RecursiveDir)%(FileName)%(Extension)').Replace('\','/'))</RelativePath>
|
||||||
|
</StaticWebAsset>
|
||||||
|
|
||||||
|
<FileWrites Include="$(_InteropBuildOutput)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Message Importance="high" Text="@(_InteropBuildOutput->'Emitted %(FullPath)')" />
|
||||||
|
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an access token for a given user and scopes.
|
||||||
|
/// </summary>
|
||||||
|
public class AccessToken
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of granted scopes for the token.
|
||||||
|
/// </summary>
|
||||||
|
public string[] GrantedScopes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the expiration time of the token.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset Expires { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the serialized representation of the token.
|
||||||
|
/// </summary>
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
// 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.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the list of authentication actions that can be performed by the <see cref="RemoteAuthenticatorViewCore{TAuthenticationState}"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteAuthenticationActions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The log in action.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogIn = "login";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The log in callback action.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogInCallback = "login-callback";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The log in failed action.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogInFailed = "login-failed";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The navigate to user profile action.
|
||||||
|
/// </summary>
|
||||||
|
public const string Profile = "profile";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The navigate to register action.
|
||||||
|
/// </summary>
|
||||||
|
public const string Register = "register";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The log out action.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogOut = "logout";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The log out callback action.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogOutCallback = "logout-callback";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The log out failed action.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogOutFailed = "logout-failed";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The log out succeeded action.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogOutSucceeded = "logged-out";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not a given <paramref name="candidate"/> represents a given <see cref="RemoteAuthenticationActions"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">The <see cref="RemoteAuthenticationActions"/>.</param>
|
||||||
|
/// <param name="candidate">The candidate.</param>
|
||||||
|
/// <returns>Whether or not is the given <see cref="RemoteAuthenticationActions"/> action.</returns>
|
||||||
|
public static bool IsAction(string action, string candidate) => action != null && string.Equals(action, candidate, System.StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the context during authentication operations.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRemoteAuthenticationState"></typeparam>
|
||||||
|
public class RemoteAuthenticationContext<TRemoteAuthenticationState> where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the url for the current authentication operation.
|
||||||
|
/// </summary>
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="TRemoteAuthenticationState"/> instance for the current authentication operation.
|
||||||
|
/// </summary>
|
||||||
|
public TRemoteAuthenticationState State { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of an authentication operation.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TState">The type of the preserved state during the authentication operation.</typeparam>
|
||||||
|
public class RemoteAuthenticationResult<TState> where TState : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the status of the authentication operation. The status can be one of <see cref="RemoteAuthenticationStatus"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the error message of a failed authentication operation.
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the preserved state of a successful authentication operation.
|
||||||
|
/// </summary>
|
||||||
|
public TState State { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the minimal amount of authentication state to be preserved during authentication operations.
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteAuthenticationState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the URL to which the application should redirect after a successful authentication operation.
|
||||||
|
/// It must be a url within the page.
|
||||||
|
/// </summary>
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the status of an authentication operation.
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteAuthenticationStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The application is going to be redirected.
|
||||||
|
/// </summary>
|
||||||
|
public const string Redirect = "redirect";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The authentication operation completed successfully.
|
||||||
|
/// </summary>
|
||||||
|
public const string Success = "success";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// There was an error performing the authentication operation.
|
||||||
|
/// </summary>
|
||||||
|
public const string Failure = "failure";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The operation in the current navigation context has completed. This signals that the application running on the
|
||||||
|
/// current browser context is about to be shut down and no other work is required.
|
||||||
|
/// </summary>
|
||||||
|
public const string OperationCompleted = "operation-completed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents options for applications relying on a server for configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class ApiAuthorizationProviderOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the endpoint to call to retrieve the authentication settings for the application.
|
||||||
|
/// </summary>
|
||||||
|
public string ConfigurationEndpoint { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
internal class DefaultApiAuthorizationOptionsConfiguration : IPostConfigureOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>
|
||||||
|
{
|
||||||
|
private readonly string _applicationName;
|
||||||
|
|
||||||
|
public DefaultApiAuthorizationOptionsConfiguration(string applicationName) => _applicationName = applicationName;
|
||||||
|
|
||||||
|
public void Configure(RemoteAuthenticationOptions<ApiAuthorizationProviderOptions> options)
|
||||||
|
{
|
||||||
|
options.ProviderOptions.ConfigurationEndpoint ??= $"_configuration/{_applicationName}";
|
||||||
|
options.AuthenticationPaths.RemoteRegisterPath ??= "Identity/Account/Register";
|
||||||
|
options.AuthenticationPaths.RemoteProfilePath ??= "Identity/Account/Manage";
|
||||||
|
options.UserOptions.ScopeClaim ??= "scope";
|
||||||
|
options.UserOptions.AuthenticationType ??= _applicationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PostConfigure(string name, RemoteAuthenticationOptions<ApiAuthorizationProviderOptions> options)
|
||||||
|
{
|
||||||
|
if (string.Equals(name, Options.DefaultName))
|
||||||
|
{
|
||||||
|
Configure(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
internal class DefaultOidcOptionsConfiguration : IPostConfigureOptions<RemoteAuthenticationOptions<OidcProviderOptions>>
|
||||||
|
{
|
||||||
|
private readonly NavigationManager _navigationManager;
|
||||||
|
|
||||||
|
public DefaultOidcOptionsConfiguration(NavigationManager navigationManager) => _navigationManager = navigationManager;
|
||||||
|
|
||||||
|
public void Configure(RemoteAuthenticationOptions<OidcProviderOptions> options)
|
||||||
|
{
|
||||||
|
options.UserOptions.AuthenticationType ??= options.ProviderOptions.ClientId;
|
||||||
|
|
||||||
|
var redirectUri = options.ProviderOptions.RedirectUri;
|
||||||
|
if (redirectUri == null || !Uri.TryCreate(redirectUri, UriKind.Absolute, out _))
|
||||||
|
{
|
||||||
|
redirectUri ??= "authentication/login-callback";
|
||||||
|
options.ProviderOptions.RedirectUri = _navigationManager
|
||||||
|
.ToAbsoluteUri(redirectUri).AbsoluteUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
var logoutUri = options.ProviderOptions.PostLogoutRedirectUri;
|
||||||
|
if (logoutUri == null || !Uri.TryCreate(logoutUri, UriKind.Absolute, out _))
|
||||||
|
{
|
||||||
|
logoutUri ??= "authentication/logout-callback";
|
||||||
|
options.ProviderOptions.PostLogoutRedirectUri = _navigationManager
|
||||||
|
.ToAbsoluteUri(logoutUri).AbsoluteUri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PostConfigure(string name, RemoteAuthenticationOptions<OidcProviderOptions> options)
|
||||||
|
{
|
||||||
|
if (string.Equals(name, Options.DefaultName))
|
||||||
|
{
|
||||||
|
Configure(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents options to pass down to configure the oidc-client.js library used when using a standard OIDC flow.
|
||||||
|
/// </summary>
|
||||||
|
public class OidcProviderOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the authority of the OIDC identity provider.
|
||||||
|
/// </summary>
|
||||||
|
public string Authority { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the client of the application.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("client_id")]
|
||||||
|
public string ClientId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of scopes to request when signing in.
|
||||||
|
/// </summary>
|
||||||
|
public IList<string> DefaultScopes { get; set; } = new List<string> { "openid", "profile" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the redirect uri for the application. The application will be redirected here after the user has completed the sign in
|
||||||
|
/// process from the identity provider.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("redirect_uri")]
|
||||||
|
public string RedirectUri { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the post logout redirect uri for the application. The application will be redirected here after the user has completed the sign out
|
||||||
|
/// process from the identity provider.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("post_logout_redirect_uri")]
|
||||||
|
public string PostLogoutRedirectUri { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
// 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.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the options for the paths used by the application for authentication operations. These paths are relative to the base.
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteAuthenticationApplicationPathsOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the endpoint for registering new users.
|
||||||
|
/// </summary>
|
||||||
|
public string RegisterPath { get; set; } = RemoteAuthenticationDefaults.RegisterPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the remote path to the remote endpoint for registering new users.
|
||||||
|
/// It might be absolute and point outside of the application.
|
||||||
|
/// </summary>
|
||||||
|
public string RemoteRegisterPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the endpoint for modifying the settings for the user profile.
|
||||||
|
/// </summary>
|
||||||
|
public string ProfilePath { get; set; } = RemoteAuthenticationDefaults.ProfilePath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the remote endpoint for modifying the settings for the user profile.
|
||||||
|
/// It might be absolute and point outside of the application.
|
||||||
|
/// </summary>
|
||||||
|
public string RemoteProfilePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the login page.
|
||||||
|
/// </summary>
|
||||||
|
public string LogInPath { get; set; } = RemoteAuthenticationDefaults.LoginPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the login callback page.
|
||||||
|
/// </summary>
|
||||||
|
public string LogInCallbackPath { get; set; } = RemoteAuthenticationDefaults.LoginCallbackPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the login failed page.
|
||||||
|
/// </summary>
|
||||||
|
public string LogInFailedPath { get; set; } = RemoteAuthenticationDefaults.LoginFailedPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the logout page.
|
||||||
|
/// </summary>
|
||||||
|
public string LogOutPath { get; set; } = RemoteAuthenticationDefaults.LogoutPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the logout callback page.
|
||||||
|
/// </summary>
|
||||||
|
public string LogOutCallbackPath { get; set; } = RemoteAuthenticationDefaults.LogoutCallbackPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the logout failed page.
|
||||||
|
/// </summary>
|
||||||
|
public string LogOutFailedPath { get; set; } = RemoteAuthenticationDefaults.LogoutFailedPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the logout succeeded page.
|
||||||
|
/// </summary>
|
||||||
|
public string LogOutSucceededPath { get; set; } = RemoteAuthenticationDefaults.LogoutSucceededPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the options for the <see cref="RemoteAuthenticationService{TRemoteAuthenticationState, TProviderOptions}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRemoteAuthenticationProviderOptions">The type of the underlying provider options.</typeparam>
|
||||||
|
public class RemoteAuthenticationOptions<TRemoteAuthenticationProviderOptions> where TRemoteAuthenticationProviderOptions : new()
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="TRemoteAuthenticationProviderOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
public TRemoteAuthenticationProviderOptions ProviderOptions { get; set; } = new TRemoteAuthenticationProviderOptions();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="RemoteAuthenticationApplicationPathsOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
public RemoteAuthenticationApplicationPathsOptions AuthenticationPaths { get; set; } = new RemoteAuthenticationApplicationPathsOptions();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="RemoteAuthenticationUserOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
public RemoteAuthenticationUserOptions UserOptions { get; set; } = new RemoteAuthenticationUserOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents options to use when configuring the <see cref="System.Security.Claims.ClaimsPrincipal"/> for a user.
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteAuthenticationUserOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the claim type to use for the user name.
|
||||||
|
/// </summary>
|
||||||
|
public string NameClaim { get; set; } = "name";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the claim type to use for the user roles.
|
||||||
|
/// </summary>
|
||||||
|
public string RoleClaim { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the claim type to use for the user scopes.
|
||||||
|
/// </summary>
|
||||||
|
public string ScopeClaim { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value to use for the <see cref="System.Security.Claims.ClaimsIdentity.AuthenticationType"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string AuthenticationType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
internal class QueryStringHelper
|
||||||
|
{
|
||||||
|
public static string GetParameter(string queryString, string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(queryString) || queryString == "?")
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scanIndex = 0;
|
||||||
|
if (queryString[0] == '?')
|
||||||
|
{
|
||||||
|
scanIndex = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textLength = queryString.Length;
|
||||||
|
var equalIndex = queryString.IndexOf('=');
|
||||||
|
if (equalIndex == -1)
|
||||||
|
{
|
||||||
|
equalIndex = textLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (scanIndex < textLength)
|
||||||
|
{
|
||||||
|
var ampersandIndex = queryString.IndexOf('&', scanIndex);
|
||||||
|
if (ampersandIndex == -1)
|
||||||
|
{
|
||||||
|
ampersandIndex = textLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equalIndex < ampersandIndex)
|
||||||
|
{
|
||||||
|
while (scanIndex != equalIndex && char.IsWhiteSpace(queryString[scanIndex]))
|
||||||
|
{
|
||||||
|
++scanIndex;
|
||||||
|
}
|
||||||
|
var name = queryString[scanIndex..equalIndex];
|
||||||
|
var value = queryString.Substring(equalIndex + 1, ampersandIndex - equalIndex - 1);
|
||||||
|
var processedName = Uri.UnescapeDataString(name.Replace('+', ' '));
|
||||||
|
if (string.Equals(processedName, key, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Uri.UnescapeDataString(value.Replace('+', ' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
equalIndex = queryString.IndexOf('=', ampersandIndex);
|
||||||
|
if (equalIndex == -1)
|
||||||
|
{
|
||||||
|
equalIndex = textLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ampersandIndex > scanIndex)
|
||||||
|
{
|
||||||
|
var value = queryString[scanIndex..ampersandIndex];
|
||||||
|
if (string.Equals(value, key, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scanIndex = ampersandIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents default values for different configurable values used across the library.
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteAuthenticationDefaults
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The default login path.
|
||||||
|
/// </summary>
|
||||||
|
public const string LoginPath = "authentication/login";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default login callback path.
|
||||||
|
/// </summary>
|
||||||
|
public const string LoginCallbackPath = "authentication/login-callback";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default login failed path.
|
||||||
|
/// </summary>
|
||||||
|
public const string LoginFailedPath = "authentication/login-failed";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default logout path.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogoutPath = "authentication/logout";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default logout callback path.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogoutCallbackPath = "authentication/logout-callback";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default logout failed path.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogoutFailedPath = "authentication/logout-failed";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default logout succeeded path.
|
||||||
|
/// </summary>
|
||||||
|
public const string LogoutSucceededPath = "authentication/logged-out";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default profile path.
|
||||||
|
/// </summary>
|
||||||
|
public const string ProfilePath = "authentication/profile";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default register path.
|
||||||
|
/// </summary>
|
||||||
|
public const string RegisterPath = "authentication/register";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="RemoteAuthenticatorViewCore{TAuthenticationState}"/> that uses <see cref="RemoteAuthenticationState"/> as the
|
||||||
|
/// state to be persisted across authentication operations.
|
||||||
|
/// </summary>
|
||||||
|
public class RemoteAuthenticatorView : RemoteAuthenticatorViewCore<RemoteAuthenticationState>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="RemoteAuthenticatorView"/>.
|
||||||
|
/// </summary>
|
||||||
|
public RemoteAuthenticatorView() => AuthenticationState = new RemoteAuthenticationState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,425 @@
|
||||||
|
// 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.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A component that handles remote authentication operations in an application.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TAuthenticationState">The user state type persisted while the operation is in progress. It must be serializable.</typeparam>
|
||||||
|
public class RemoteAuthenticatorViewCore<TAuthenticationState> : ComponentBase where TAuthenticationState : RemoteAuthenticationState
|
||||||
|
{
|
||||||
|
private string _message;
|
||||||
|
private RemoteAuthenticationApplicationPathsOptions _applicationPaths;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="RemoteAuthenticationActions"/> action the component needs to handle.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public string Action { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <typeparamref name="TAuthenticationState"/> instance to be preserved during the authentication operation.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public TAuthenticationState AuthenticationState { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogIn"/> is being handled.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public RenderFragment LoggingIn { get; set; } = DefaultLogInFragment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.Register"/> is being handled.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public RenderFragment Registering { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.Profile"/> is being handled.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public RenderFragment UserProfile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogInCallback"/> is being handled.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public RenderFragment CompletingLoggingIn { get; set; } = DefaultLogInCallbackFragment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogInFailed"/> is being handled.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public RenderFragment<string> LogInFailed { get; set; } = DefaultLogInFailedFragment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogOut"/> is being handled.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public RenderFragment LogOut { get; set; } = DefaultLogOutFragment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogOutCallback"/> is being handled.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public RenderFragment CompletingLogOut { get; set; } = DefaultLogOutCallbackFragment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogOutFailed"/> is being handled.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public RenderFragment<string> LogOutFailed { get; set; } = DefaultLogOutFailedFragment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogOutSucceeded"/> is being handled.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public RenderFragment LogOutSucceeded { get; set; } = DefaultLoggedOutFragment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="IJSRuntime"/> to use for performin JavaScript interop.
|
||||||
|
/// </summary>
|
||||||
|
[Inject] public IJSRuntime JS { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="NavigationManager"/> to use for redirecting the browser.
|
||||||
|
/// </summary>
|
||||||
|
[Inject] public NavigationManager Navigation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="IRemoteAuthenticationService{TRemoteAuthenticationState}"/> to use for handling the underlying authentication protocol.
|
||||||
|
/// </summary>
|
||||||
|
[Inject] public IRemoteAuthenticationService<TAuthenticationState> AuthenticationService { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a default <see cref="IRemoteAuthenticationPathsProvider"/> to use as fallback if an <see cref="ApplicationPaths"/> has not been explicitly specified.
|
||||||
|
/// </summary>
|
||||||
|
#pragma warning disable PUB0001 // Pubternal type in public API
|
||||||
|
[Inject] public IRemoteAuthenticationPathsProvider RemoteApplicationPathsProvider { get; set; }
|
||||||
|
#pragma warning restore PUB0001 // Pubternal type in public API
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a default <see cref="AuthenticationStateProvider"/> with the current user.
|
||||||
|
/// </summary>
|
||||||
|
[Inject] public AuthenticationStateProvider AuthenticationProvider { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a default <see cref="AuthenticationStateProvider"/> with the current user.
|
||||||
|
/// </summary>
|
||||||
|
[Inject] public SignOutSessionStateManager SignOutManager { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="RemoteAuthenticationApplicationPathsOptions"/> with the paths to different authentication pages.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public RemoteAuthenticationApplicationPathsOptions ApplicationPaths
|
||||||
|
{
|
||||||
|
get => _applicationPaths ?? RemoteApplicationPathsProvider.ApplicationPaths;
|
||||||
|
set => _applicationPaths = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
base.BuildRenderTree(builder);
|
||||||
|
switch (Action)
|
||||||
|
{
|
||||||
|
case RemoteAuthenticationActions.Profile:
|
||||||
|
builder.AddContent(0, UserProfile);
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.Register:
|
||||||
|
builder.AddContent(0, Registering);
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogIn:
|
||||||
|
builder.AddContent(0, LoggingIn);
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogInCallback:
|
||||||
|
builder.AddContent(0, CompletingLoggingIn);
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogInFailed:
|
||||||
|
builder.AddContent(0, LogInFailed(_message));
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogOut:
|
||||||
|
builder.AddContent(0, LogOut);
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogOutCallback:
|
||||||
|
builder.AddContent(0, CompletingLogOut);
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogOutFailed:
|
||||||
|
builder.AddContent(0, LogOutFailed(_message));
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogOutSucceeded:
|
||||||
|
builder.AddContent(0, LogOutSucceeded);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Invalid action '{Action}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
switch (Action)
|
||||||
|
{
|
||||||
|
case RemoteAuthenticationActions.LogIn:
|
||||||
|
await ProcessLogIn(GetReturnUrl(state: null));
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogInCallback:
|
||||||
|
await ProcessLogInCallback();
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogInFailed:
|
||||||
|
_message = GetErrorMessage();
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.Profile:
|
||||||
|
if (ApplicationPaths.RemoteProfilePath == null)
|
||||||
|
{
|
||||||
|
UserProfile ??= ProfileNotSupportedFragment;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UserProfile ??= LoggingIn;
|
||||||
|
await RedirectToProfile();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.Register:
|
||||||
|
if (ApplicationPaths.RemoteRegisterPath == null)
|
||||||
|
{
|
||||||
|
Registering ??= RegisterNotSupportedFragment;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Registering ??= LoggingIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
await RedirectToRegister();
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogOut:
|
||||||
|
await ProcessLogOut(GetReturnUrl(state: null, Navigation.ToAbsoluteUri(ApplicationPaths.LogOutSucceededPath).AbsoluteUri));
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogOutCallback:
|
||||||
|
await ProcessLogOutCallback();
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogOutFailed:
|
||||||
|
_message = GetErrorMessage();
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationActions.LogOutSucceeded:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Invalid action '{Action}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessLogIn(string returnUrl)
|
||||||
|
{
|
||||||
|
AuthenticationState.ReturnUrl = returnUrl;
|
||||||
|
var result = await AuthenticationService.SignInAsync(new RemoteAuthenticationContext<TAuthenticationState>
|
||||||
|
{
|
||||||
|
State = AuthenticationState
|
||||||
|
});
|
||||||
|
switch (result.Status)
|
||||||
|
{
|
||||||
|
case RemoteAuthenticationStatus.Redirect:
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationStatus.Success:
|
||||||
|
await NavigateToReturnUrl(returnUrl);
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationStatus.Failure:
|
||||||
|
var uri = Navigation.ToAbsoluteUri($"{ApplicationPaths.LogInFailedPath}?message={Uri.EscapeDataString(result.ErrorMessage)}").ToString();
|
||||||
|
await NavigateToReturnUrl(uri);
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationStatus.OperationCompleted:
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Invalid authentication result status '{result.Status}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessLogInCallback()
|
||||||
|
{
|
||||||
|
var url = Navigation.Uri;
|
||||||
|
var result = await AuthenticationService.CompleteSignInAsync(new RemoteAuthenticationContext<TAuthenticationState> { Url = url });
|
||||||
|
switch (result.Status)
|
||||||
|
{
|
||||||
|
case RemoteAuthenticationStatus.Redirect:
|
||||||
|
// There should not be any redirects as the only time CompleteSignInAsync finishes
|
||||||
|
// is when we are doing a redirect sign in flow.
|
||||||
|
throw new InvalidOperationException("Should not redirect.");
|
||||||
|
case RemoteAuthenticationStatus.Success:
|
||||||
|
await NavigateToReturnUrl(GetReturnUrl(result.State));
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationStatus.OperationCompleted:
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationStatus.Failure:
|
||||||
|
var uri = Navigation.ToAbsoluteUri($"{ApplicationPaths.LogInFailedPath}?message={Uri.EscapeDataString(result.ErrorMessage)}").ToString();
|
||||||
|
await NavigateToReturnUrl(uri);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Invalid authentication result status '{result.Status}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessLogOut(string returnUrl)
|
||||||
|
{
|
||||||
|
if (!await SignOutManager.ValidateSignOutState())
|
||||||
|
{
|
||||||
|
var uri = $"{Navigation.ToAbsoluteUri(ApplicationPaths.LogOutFailedPath)}?message={Uri.EscapeDataString("The logout was not initiated from within the page.")}";
|
||||||
|
Navigation.NavigateTo(uri);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationState.ReturnUrl = returnUrl;
|
||||||
|
|
||||||
|
var state = await AuthenticationProvider.GetAuthenticationStateAsync();
|
||||||
|
var isauthenticated = state.User.Identity.IsAuthenticated;
|
||||||
|
if (isauthenticated)
|
||||||
|
{
|
||||||
|
var result = await AuthenticationService.SignOutAsync(new RemoteAuthenticationContext<TAuthenticationState> { State = AuthenticationState });
|
||||||
|
switch (result.Status)
|
||||||
|
{
|
||||||
|
case RemoteAuthenticationStatus.Redirect:
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationStatus.Success:
|
||||||
|
await NavigateToReturnUrl(returnUrl);
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationStatus.OperationCompleted:
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationStatus.Failure:
|
||||||
|
var uri = Navigation.ToAbsoluteUri($"{ApplicationPaths.LogOutFailedPath}?message={Uri.EscapeDataString(result.ErrorMessage)}").ToString();
|
||||||
|
await NavigateToReturnUrl(uri);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Invalid authentication result status '{result.Status ?? "(null)"}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await NavigateToReturnUrl(returnUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessLogOutCallback()
|
||||||
|
{
|
||||||
|
var result = await AuthenticationService.CompleteSignOutAsync(new RemoteAuthenticationContext<TAuthenticationState> { Url = Navigation.Uri });
|
||||||
|
switch (result.Status)
|
||||||
|
{
|
||||||
|
case RemoteAuthenticationStatus.Redirect:
|
||||||
|
// There should not be any redirects as the only time completeAuthentication finishes
|
||||||
|
// is when we are doing a redirect sign in flow.
|
||||||
|
throw new InvalidOperationException("Should not redirect.");
|
||||||
|
case RemoteAuthenticationStatus.Success:
|
||||||
|
await NavigateToReturnUrl(GetReturnUrl(result.State, Navigation.ToAbsoluteUri(ApplicationPaths.LogOutSucceededPath).ToString()));
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationStatus.OperationCompleted:
|
||||||
|
break;
|
||||||
|
case RemoteAuthenticationStatus.Failure:
|
||||||
|
var uri = Navigation.ToAbsoluteUri($"{ApplicationPaths.LogOutFailedPath}?message={Uri.EscapeDataString(result.ErrorMessage)}").ToString();
|
||||||
|
await NavigateToReturnUrl(uri);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Invalid authentication result status '{result.Status ?? "(null)"}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetReturnUrl(TAuthenticationState state, string defaultReturnUrl = null)
|
||||||
|
{
|
||||||
|
if (state?.ReturnUrl != null)
|
||||||
|
{
|
||||||
|
return state.ReturnUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromQuery = QueryStringHelper.GetParameter(new Uri(Navigation.Uri).Query, "returnUrl");
|
||||||
|
if (!string.IsNullOrWhiteSpace(fromQuery) && !fromQuery.StartsWith(Navigation.BaseUri))
|
||||||
|
{
|
||||||
|
// This is an extra check to prevent open redirects.
|
||||||
|
throw new InvalidOperationException("Invalid return url. The return url needs to have the same origin as the current page.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromQuery ?? defaultReturnUrl ?? Navigation.BaseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NavigateToReturnUrl(string returnUrl) => await JS.InvokeVoidAsync("Blazor.navigateTo", returnUrl, false, true);
|
||||||
|
|
||||||
|
private ValueTask RedirectToRegister()
|
||||||
|
{
|
||||||
|
var loginUrl = Navigation.ToAbsoluteUri(ApplicationPaths.LogInPath).PathAndQuery;
|
||||||
|
var registerUrl = Navigation.ToAbsoluteUri($"{ApplicationPaths.RemoteRegisterPath}?returnUrl={Uri.EscapeDataString(loginUrl)}").PathAndQuery;
|
||||||
|
|
||||||
|
return JS.InvokeVoidAsync("location.replace", registerUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask RedirectToProfile() => JS.InvokeVoidAsync("location.replace", Navigation.ToAbsoluteUri(ApplicationPaths.RemoteProfilePath).PathAndQuery);
|
||||||
|
|
||||||
|
private string GetErrorMessage() => QueryStringHelper.GetParameter(new Uri(Navigation.Uri).Query, "message");
|
||||||
|
|
||||||
|
private static void DefaultLogInFragment(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "p");
|
||||||
|
builder.AddContent(1, "Checking login state...");
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterNotSupportedFragment(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "p");
|
||||||
|
builder.AddContent(1, "Registration is not supported.");
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProfileNotSupportedFragment(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "p");
|
||||||
|
builder.AddContent(1, "Editing the profile is not supported.");
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DefaultLogInCallbackFragment(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "p");
|
||||||
|
builder.AddContent(1, "Completing login...");
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RenderFragment DefaultLogInFailedFragment(string message)
|
||||||
|
{
|
||||||
|
return builder =>
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "p");
|
||||||
|
builder.AddContent(1, "There was an error trying to log you in: '");
|
||||||
|
builder.AddContent(2, message);
|
||||||
|
builder.AddContent(3, "'");
|
||||||
|
builder.CloseElement();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DefaultLogOutFragment(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "p");
|
||||||
|
builder.AddContent(1, "Processing logout...");
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DefaultLogOutCallbackFragment(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "p");
|
||||||
|
builder.AddContent(1, "Processing logout callback...");
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RenderFragment DefaultLogOutFailedFragment(string message)
|
||||||
|
{
|
||||||
|
return builder =>
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "p");
|
||||||
|
builder.AddContent(1, "There was an error trying to log you out: '");
|
||||||
|
builder.AddContent(2, message);
|
||||||
|
builder.AddContent(3, "'");
|
||||||
|
builder.CloseElement();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DefaultLoggedOutFragment(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "p");
|
||||||
|
builder.AddContent(1, "You are logged out.");
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the options for provisioning an access token on behalf of a user.
|
||||||
|
/// </summary>
|
||||||
|
public class AccessTokenRequestOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of scopes to request for the token.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> Scopes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a specific return url to use for returning the user back to the application if it needs to be
|
||||||
|
/// redirected elsewhere in order to provision the token.
|
||||||
|
/// </summary>
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of trying to provision an access token.
|
||||||
|
/// </summary>
|
||||||
|
public class AccessTokenResult
|
||||||
|
{
|
||||||
|
private AccessToken _token;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="AccessTokenResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="status">The status of the result.</param>
|
||||||
|
/// <param name="token">The <see cref="AccessToken"/> in case it was successful.</param>
|
||||||
|
/// <param name="redirectUrl">The redirect uri to go to for provisioning the token.</param>
|
||||||
|
public AccessTokenResult(string status, AccessToken token, string redirectUrl)
|
||||||
|
{
|
||||||
|
Status = status;
|
||||||
|
_token = token;
|
||||||
|
RedirectUrl = redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the status of the current operation. See <see cref="AccessTokenResultStatus"/> for a list of statuses.
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the URL to redirect to if <see cref="Status"/> is <see cref="AccessTokenResultStatus.RequiresRedirect"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string RedirectUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the token request was successful and makes the <see cref="AccessToken"/> available for use when it is.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accessToken">The <see cref="AccessToken"/> if the request was successful.</param>
|
||||||
|
/// <returns><c>true</c> when the token request is successful; <c>false</c> otherwise.</returns>
|
||||||
|
public bool TryGetToken(out AccessToken accessToken)
|
||||||
|
{
|
||||||
|
if (string.Equals(Status, AccessTokenResultStatus.Success, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
accessToken = _token;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
accessToken = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the possible results from trying to acquire an access token.
|
||||||
|
/// </summary>
|
||||||
|
public class AccessTokenResultStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The token was successfully acquired.
|
||||||
|
/// </summary>
|
||||||
|
public const string Success = "success";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A redirect is needed in order to provision the token.
|
||||||
|
/// </summary>
|
||||||
|
public const string RequiresRedirect = "requiesRedirect";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a contract for services capable of provisioning access tokens for an application.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAccessTokenProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to get an access token for the current user with the default set of permissions.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="ValueTask{AccessTokenResult}"/> that will contain the <see cref="AccessTokenResult"/> when completed.</returns>
|
||||||
|
ValueTask<AccessTokenResult> RequestAccessToken();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to get an access token with the options specified in <see cref="AccessTokenRequestOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">The <see cref="AccessTokenRequestOptions"/> for provisioning the access token.</param>
|
||||||
|
/// <returns>A <see cref="ValueTask{AccessTokenResult}"/> that will contain the <see cref="AccessTokenResult"/> when completed.</returns>
|
||||||
|
ValueTask<AccessTokenResult> RequestAccessToken(AccessTokenRequestOptions options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a contract for services that perform authentication operations for a Blazor WebAssembly application.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam>
|
||||||
|
public interface IRemoteAuthenticationService<TRemoteAuthenticationState>
|
||||||
|
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Signs in a user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="RemoteAuthenticationContext{TRemoteAuthenticationState}"/> for authenticating the user.</param>
|
||||||
|
/// <returns>The result of the authentication operation.</returns>
|
||||||
|
Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> SignInAsync(RemoteAuthenticationContext<TRemoteAuthenticationState> context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Completes the sign in operation for a user when it is performed outside of the application origin via a redirect operation followed
|
||||||
|
/// by a redirect callback to a page in the application.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="RemoteAuthenticationContext{TRemoteAuthenticationState}"/> for authenticating the user.</param>
|
||||||
|
/// <returns>The result of the authentication operation.</returns>
|
||||||
|
Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> CompleteSignInAsync(
|
||||||
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signs out a user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="RemoteAuthenticationContext{TRemoteAuthenticationState}"/> for authenticating the user.</param>
|
||||||
|
/// <returns>The result of the authentication operation.</returns>
|
||||||
|
Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> SignOutAsync(
|
||||||
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Completes the sign out operation for a user when it is performed outside of the application origin via a redirect operation followed
|
||||||
|
/// by a redirect callback to a page in the application.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="RemoteAuthenticationContext{TRemoteAuthenticationState}"/> for authenticating the user.</param>
|
||||||
|
/// <returns>The result of the authentication operation.</returns>
|
||||||
|
Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> CompleteSignOutAsync(
|
||||||
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
// 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 System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The default implementation for <see cref="IRemoteAuthenticationService{TRemoteAuthenticationState}"/> that uses JS interop to authenticate the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRemoteAuthenticationState">The state to preserve across authentication operations.</typeparam>
|
||||||
|
/// <typeparam name="TProviderOptions">The options to be passed down to the underlying JavaScript library handling the authentication operations.</typeparam>
|
||||||
|
public class RemoteAuthenticationService<TRemoteAuthenticationState, TProviderOptions> :
|
||||||
|
AuthenticationStateProvider,
|
||||||
|
IRemoteAuthenticationService<TRemoteAuthenticationState>,
|
||||||
|
IAccessTokenProvider
|
||||||
|
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||||
|
where TProviderOptions : new()
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan _userCacheRefreshInterval = TimeSpan.FromSeconds(60);
|
||||||
|
|
||||||
|
private bool _initialized = false;
|
||||||
|
|
||||||
|
// This defaults to 1/1/1970
|
||||||
|
private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0);
|
||||||
|
private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly IJSRuntime _jsRuntime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="NavigationManager"/> used to compute absolute urls.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly NavigationManager _navigation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The options for the underlying JavaScript library handling the authentication operations.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly RemoteAuthenticationOptions<TProviderOptions> _options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="RemoteAuthenticationService{TRemoteAuthenticationState, TProviderOptions}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.</param>
|
||||||
|
/// <param name="options">The options to be passed down to the underlying JavaScript library handling the authentication operations.</param>
|
||||||
|
public RemoteAuthenticationService(
|
||||||
|
IJSRuntime jsRuntime,
|
||||||
|
IOptions<RemoteAuthenticationOptions<TProviderOptions>> options,
|
||||||
|
NavigationManager navigation)
|
||||||
|
{
|
||||||
|
_jsRuntime = jsRuntime;
|
||||||
|
_navigation = navigation;
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync() => new AuthenticationState(await GetUser(useCache: true));
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> SignInAsync(
|
||||||
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||||
|
{
|
||||||
|
await EnsureAuthService();
|
||||||
|
var result = await _jsRuntime.InvokeAsync<RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signIn", context.State);
|
||||||
|
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||||
|
{
|
||||||
|
UpdateUser(GetUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> CompleteSignInAsync(
|
||||||
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||||
|
{
|
||||||
|
await EnsureAuthService();
|
||||||
|
var result = await _jsRuntime.InvokeAsync<RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignIn", context.Url);
|
||||||
|
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||||
|
{
|
||||||
|
UpdateUser(GetUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> SignOutAsync(
|
||||||
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||||
|
{
|
||||||
|
await EnsureAuthService();
|
||||||
|
var result = await _jsRuntime.InvokeAsync<RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signOut", context.State);
|
||||||
|
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||||
|
{
|
||||||
|
UpdateUser(GetUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> CompleteSignOutAsync(
|
||||||
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||||
|
{
|
||||||
|
await EnsureAuthService();
|
||||||
|
var result = await _jsRuntime.InvokeAsync<RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignOut", context.Url);
|
||||||
|
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||||
|
{
|
||||||
|
UpdateUser(GetUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async ValueTask<AccessTokenResult> RequestAccessToken()
|
||||||
|
{
|
||||||
|
await EnsureAuthService();
|
||||||
|
var result = await _jsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken");
|
||||||
|
|
||||||
|
if (string.Equals(result.Status, AccessTokenResultStatus.RequiresRedirect, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var redirectUrl = GetRedirectUrl(null);
|
||||||
|
result.RedirectUrl = redirectUrl.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AccessTokenResult(result.Status, result.Token, result.RedirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async ValueTask<AccessTokenResult> RequestAccessToken(AccessTokenRequestOptions options)
|
||||||
|
{
|
||||||
|
if (options is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnsureAuthService();
|
||||||
|
var result = await _jsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken", options);
|
||||||
|
|
||||||
|
if (string.Equals(result.Status, AccessTokenResultStatus.RequiresRedirect, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var redirectUrl = GetRedirectUrl(options.ReturnUrl);
|
||||||
|
result.RedirectUrl = redirectUrl.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AccessTokenResult(result.Status, result.Token, result.RedirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri GetRedirectUrl(string customReturnUrl)
|
||||||
|
{
|
||||||
|
var returnUrl = customReturnUrl != null ? _navigation.ToAbsoluteUri(customReturnUrl).ToString() : null;
|
||||||
|
var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? _navigation.Uri);
|
||||||
|
var redirectUrl = _navigation.ToAbsoluteUri($"{_options.AuthenticationPaths.LogInPath}?returnUrl={encodedReturnUrl}");
|
||||||
|
return redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false)
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
|
if (useCache && now < _userLastCheck + _userCacheRefreshInterval)
|
||||||
|
{
|
||||||
|
return _cachedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cachedUser = await GetAuthenticatedUser();
|
||||||
|
_userLastCheck = now;
|
||||||
|
|
||||||
|
return _cachedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current authenticated used using JavaScript interop.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="Task{ClaimsPrincipal}"/>that will return the current authenticated user when completes.</returns>
|
||||||
|
protected internal virtual async Task<ClaimsPrincipal> GetAuthenticatedUser()
|
||||||
|
{
|
||||||
|
await EnsureAuthService();
|
||||||
|
var user = await _jsRuntime.InvokeAsync<IDictionary<string, object>>("AuthenticationService.getUser");
|
||||||
|
|
||||||
|
var identity = user != null ? new ClaimsIdentity(
|
||||||
|
_options.UserOptions.AuthenticationType,
|
||||||
|
_options.UserOptions.NameClaim,
|
||||||
|
_options.UserOptions.RoleClaim) : new ClaimsIdentity();
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
foreach (var kvp in user)
|
||||||
|
{
|
||||||
|
var name = kvp.Key;
|
||||||
|
var value = kvp.Value;
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
if (value is JsonElement element && element.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var item in element.EnumerateArray())
|
||||||
|
{
|
||||||
|
identity.AddClaim(new Claim(name, JsonSerializer.Deserialize<object>(item.GetRawText()).ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
identity.AddClaim(new Claim(name, value.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClaimsPrincipal(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask EnsureAuthService()
|
||||||
|
{
|
||||||
|
if (!_initialized)
|
||||||
|
{
|
||||||
|
await _jsRuntime.InvokeVoidAsync("AuthenticationService.init", _options.ProviderOptions);
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateUser(ValueTask<ClaimsPrincipal> task)
|
||||||
|
{
|
||||||
|
NotifyAuthenticationStateChanged(UpdateAuthenticationState(task));
|
||||||
|
|
||||||
|
static async Task<AuthenticationState> UpdateAuthenticationState(ValueTask<ClaimsPrincipal> futureUser) => new AuthenticationState(await futureUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal for testing purposes
|
||||||
|
internal struct InternalAccessTokenResult
|
||||||
|
{
|
||||||
|
public string Status { get; set; }
|
||||||
|
public AccessToken Token { get; set; }
|
||||||
|
public string RedirectUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
// 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.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles CSRF protection for the logout endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public class SignOutSessionStateManager
|
||||||
|
{
|
||||||
|
private readonly IJSRuntime _jsRuntime;
|
||||||
|
private static readonly JsonSerializerOptions _serializationOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
public SignOutSessionStateManager(IJSRuntime jsRuntime) => _jsRuntime = jsRuntime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets up some state in session storage to allow for logouts from within the <see cref="RemoteAuthenticationDefaults.LogoutPath"/> page.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="ValueTask"/> that completes when the state has been saved to session storage.</returns>
|
||||||
|
public virtual ValueTask SetSignOutState()
|
||||||
|
{
|
||||||
|
return _jsRuntime.InvokeVoidAsync(
|
||||||
|
"sessionStorage.setItem",
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly.Authentication.SignOutState",
|
||||||
|
JsonSerializer.Serialize(SignOutState.Instance, _serializationOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the existence of some state previously setup by <see cref="SetSignOutState"/> in session storage to allow
|
||||||
|
/// logouts from within the <see cref="RemoteAuthenticationDefaults.LogoutPath"/> page.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="ValueTask{bool}"/> that completes when the state has been validated and indicates the validity of the state.</returns>
|
||||||
|
public virtual async Task<bool> ValidateSignOutState()
|
||||||
|
{
|
||||||
|
var state = await GetSignOutState();
|
||||||
|
if (state.Local)
|
||||||
|
{
|
||||||
|
await ClearSignOutState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<SignOutState> GetSignOutState()
|
||||||
|
{
|
||||||
|
var result = await _jsRuntime.InvokeAsync<string>(
|
||||||
|
"sessionStorage.getItem",
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly.Authentication.SignOutState");
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<SignOutState>(result, _serializationOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask ClearSignOutState()
|
||||||
|
{
|
||||||
|
return _jsRuntime.InvokeVoidAsync(
|
||||||
|
"sessionStorage.removeItem",
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly.Authentication.SignOutState");
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct SignOutState
|
||||||
|
{
|
||||||
|
public static readonly SignOutState Instance = new SignOutState { Local = true };
|
||||||
|
|
||||||
|
public bool Local { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
// 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.Components.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains extension methods to add authentication to Blazor WebAssembly applications.
|
||||||
|
/// </summary>
|
||||||
|
public static class WebAssemblyAuthenticationServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds support for authentication for SPA applications using the given <typeparamref name="TProviderOptions"/> and
|
||||||
|
/// <typeparamref name="TRemoteAuthenticationState"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam>
|
||||||
|
/// <typeparam name="TProviderOptions">The configuration options of the underlying provider being used for handling the authentication operations.</typeparam>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
|
public static IServiceCollection AddRemoteAuthentication<TRemoteAuthenticationState, TProviderOptions>(this IServiceCollection services)
|
||||||
|
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||||
|
where TProviderOptions : class, new()
|
||||||
|
{
|
||||||
|
services.AddOptions();
|
||||||
|
services.AddAuthorizationCore();
|
||||||
|
services.TryAddSingleton<AuthenticationStateProvider, RemoteAuthenticationService<TRemoteAuthenticationState, TProviderOptions>>();
|
||||||
|
services.TryAddSingleton(sp =>
|
||||||
|
{
|
||||||
|
return (IRemoteAuthenticationService<TRemoteAuthenticationState>)sp.GetRequiredService<AuthenticationStateProvider>();
|
||||||
|
});
|
||||||
|
|
||||||
|
services.TryAddSingleton(sp =>
|
||||||
|
{
|
||||||
|
return (IAccessTokenProvider)sp.GetRequiredService<AuthenticationStateProvider>();
|
||||||
|
});
|
||||||
|
|
||||||
|
services.TryAddSingleton<IRemoteAuthenticationPathsProvider, DefaultRemoteApplicationPathsProvider<TProviderOptions>>();
|
||||||
|
|
||||||
|
services.TryAddSingleton<SignOutSessionStateManager>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds support for authentication for SPA applications using the given <typeparamref name="TProviderOptions"/> and
|
||||||
|
/// <typeparamref name="TRemoteAuthenticationState"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam>
|
||||||
|
/// <typeparam name="TProviderOptions">The configuration options of the underlying provider being used for handling the authentication operations.</typeparam>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
|
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
||||||
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
|
public static IServiceCollection AddRemoteAuthentication<TRemoteAuthenticationState, TProviderOptions>(this IServiceCollection services, Action<RemoteAuthenticationOptions<TProviderOptions>> configure)
|
||||||
|
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||||
|
where TProviderOptions : class, new()
|
||||||
|
{
|
||||||
|
services.AddRemoteAuthentication<RemoteAuthenticationState, TProviderOptions>();
|
||||||
|
if (configure != null)
|
||||||
|
{
|
||||||
|
services.Configure(configure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds support for authentication for SPA applications using <see cref="OidcProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
|
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
||||||
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
|
public static IServiceCollection AddOidcAuthentication(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure)
|
||||||
|
{
|
||||||
|
AddRemoteAuthentication<RemoteAuthenticationState, OidcProviderOptions>(services, configure);
|
||||||
|
|
||||||
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<OidcProviderOptions>>, DefaultOidcOptionsConfiguration>());
|
||||||
|
|
||||||
|
if (configure != null)
|
||||||
|
{
|
||||||
|
services.Configure(configure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
|
public static IServiceCollection AddApiAuthorization(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
var inferredClientId = Assembly.GetCallingAssembly().GetName().Name;
|
||||||
|
|
||||||
|
services.AddRemoteAuthentication<RemoteAuthenticationState, ApiAuthorizationProviderOptions>();
|
||||||
|
|
||||||
|
services.TryAddEnumerable(
|
||||||
|
ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>, DefaultApiAuthorizationOptionsConfiguration>(_ =>
|
||||||
|
new DefaultApiAuthorizationOptionsConfiguration(inferredClientId)));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
|
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{ApiAuthorizationProviderOptions}"/>.</param>
|
||||||
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
|
public static IServiceCollection AddApiAuthorization(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure)
|
||||||
|
{
|
||||||
|
services.AddApiAuthorization();
|
||||||
|
|
||||||
|
if (configure != null)
|
||||||
|
{
|
||||||
|
services.Configure(configure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<NoWarn>$(NoWarn);BL0005;BL0006</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Blazor" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />
|
||||||
|
<Reference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,560 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
public class RemoteAuthenticationServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoteAuthenticationService_SignIn_UpdatesUserOnSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.SignInResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
{
|
||||||
|
State = state,
|
||||||
|
Status = RemoteAuthenticationStatus.Success
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await runtime.SignInAsync(new RemoteAuthenticationContext<RemoteAuthenticationState> { State = state });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.signIn", "AuthenticationService.getUser" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.Redirect)]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.Failure)]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.OperationCompleted)]
|
||||||
|
public async Task RemoteAuthenticationService_SignIn_DoesNotUpdateUserOnOtherResult(string value)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.SignInResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
{
|
||||||
|
Status = value
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await runtime.SignInAsync(new RemoteAuthenticationContext<RemoteAuthenticationState> { State = state });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.signIn" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoteAuthenticationService_CompleteSignInAsync_UpdatesUserOnSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.CompleteSignInResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
{
|
||||||
|
State = state,
|
||||||
|
Status = RemoteAuthenticationStatus.Success
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await runtime.CompleteSignInAsync(new RemoteAuthenticationContext<RemoteAuthenticationState> { Url = "https://www.example.com/base/login-callback" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.completeSignIn", "AuthenticationService.getUser" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.Redirect)]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.Failure)]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.OperationCompleted)]
|
||||||
|
public async Task RemoteAuthenticationService_CompleteSignInAsync_DoesNotUpdateUserOnOtherResult(string value)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.CompleteSignInResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
{
|
||||||
|
Status = value
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await runtime.CompleteSignInAsync(new RemoteAuthenticationContext<RemoteAuthenticationState> { Url = "https://www.example.com/base/login-callback" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.completeSignIn" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoteAuthenticationService_SignOut_UpdatesUserOnSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.SignOutResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
{
|
||||||
|
State = state,
|
||||||
|
Status = RemoteAuthenticationStatus.Success
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await runtime.SignOutAsync(new RemoteAuthenticationContext<RemoteAuthenticationState> { State = state });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.signOut", "AuthenticationService.getUser" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.Redirect)]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.Failure)]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.OperationCompleted)]
|
||||||
|
public async Task RemoteAuthenticationService_SignOut_DoesNotUpdateUserOnOtherResult(string value)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.SignOutResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
{
|
||||||
|
Status = value
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await runtime.SignOutAsync(new RemoteAuthenticationContext<RemoteAuthenticationState> { State = state });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.signOut" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoteAuthenticationService_CompleteSignOutAsync_UpdatesUserOnSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.CompleteSignOutResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
{
|
||||||
|
State = state,
|
||||||
|
Status = RemoteAuthenticationStatus.Success
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await runtime.CompleteSignOutAsync(new RemoteAuthenticationContext<RemoteAuthenticationState> { Url = "https://www.example.com/base/login-callback" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.completeSignOut", "AuthenticationService.getUser" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.Redirect)]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.Failure)]
|
||||||
|
[InlineData(RemoteAuthenticationStatus.OperationCompleted)]
|
||||||
|
public async Task RemoteAuthenticationService_CompleteSignOutAsync_DoesNotUpdateUserOnOtherResult(string value)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.CompleteSignOutResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
{
|
||||||
|
Status = value
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await runtime.CompleteSignOutAsync(new RemoteAuthenticationContext<RemoteAuthenticationState> { Url = "https://www.example.com/base/login-callback" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.completeSignOut" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoteAuthenticationService_GetAccessToken_ReturnsAccessTokenResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||||
|
{
|
||||||
|
Status = AccessTokenResultStatus.Success,
|
||||||
|
Token = new AccessToken
|
||||||
|
{
|
||||||
|
Value = "1234",
|
||||||
|
GrantedScopes = new[] { "All" },
|
||||||
|
Expires = new DateTimeOffset(2050, 5, 13, 0, 0, 0, TimeSpan.Zero)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await runtime.RequestAccessToken();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.getAccessToken" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
|
||||||
|
Assert.True(result.TryGetToken(out var token));
|
||||||
|
Assert.Equal(result.Status, testJsRuntime.GetAccessTokenResult.Status);
|
||||||
|
Assert.Equal(result.RedirectUrl, testJsRuntime.GetAccessTokenResult.RedirectUrl);
|
||||||
|
Assert.Equal(token, testJsRuntime.GetAccessTokenResult.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoteAuthenticationService_GetAccessToken_PassesDownOptions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||||
|
{
|
||||||
|
Status = AccessTokenResultStatus.RequiresRedirect,
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenOptions = new AccessTokenRequestOptions
|
||||||
|
{
|
||||||
|
Scopes = new[] { "something" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedRedirectUrl = "https://www.example.com/base/login?returnUrl=https%3A%2F%2Fwww.example.com%2Fbase%2Fadd-product";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await runtime.RequestAccessToken(tokenOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.getAccessToken" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
|
||||||
|
Assert.False(result.TryGetToken(out var token));
|
||||||
|
Assert.Null(token);
|
||||||
|
Assert.Equal(result.Status, testJsRuntime.GetAccessTokenResult.Status);
|
||||||
|
Assert.Equal(expectedRedirectUrl, result.RedirectUrl);
|
||||||
|
Assert.Equal(tokenOptions, (AccessTokenRequestOptions)testJsRuntime.PastInvocations[^1].args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoteAuthenticationService_GetAccessToken_ComputesDefaultReturnUrlOnRequiresRedirect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var state = new RemoteAuthenticationState();
|
||||||
|
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||||
|
{
|
||||||
|
Status = AccessTokenResultStatus.RequiresRedirect,
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenOptions = new AccessTokenRequestOptions
|
||||||
|
{
|
||||||
|
Scopes = new[] { "something" },
|
||||||
|
ReturnUrl = "https://www.example.com/base/add-saved-product/123413241234"
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedRedirectUrl = "https://www.example.com/base/login?returnUrl=https%3A%2F%2Fwww.example.com%2Fbase%2Fadd-saved-product%2F123413241234";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await runtime.RequestAccessToken(tokenOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.getAccessToken" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
|
||||||
|
Assert.False(result.TryGetToken(out var token));
|
||||||
|
Assert.Null(token);
|
||||||
|
Assert.Equal(result.Status, testJsRuntime.GetAccessTokenResult.Status);
|
||||||
|
Assert.Equal(expectedRedirectUrl, result.RedirectUrl);
|
||||||
|
Assert.Equal(tokenOptions, (AccessTokenRequestOptions)testJsRuntime.PastInvocations[^1].args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoteAuthenticationService_GetUser_ReturnsAnonymousClaimsPrincipal_ForUnauthenticatedUsers()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
testJsRuntime.GetUserResult = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await runtime.GetAuthenticatedUser();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(result.Claims);
|
||||||
|
Assert.Single(result.Identities);
|
||||||
|
Assert.False(result.Identity.IsAuthenticated);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "AuthenticationService.init", "AuthenticationService.getUser" },
|
||||||
|
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoteAuthenticationService_GetUser_ReturnsUser_ForAuthenticatedUsers()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions();
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var serializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true };
|
||||||
|
var serializedUser = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
CoolName = "Alfred",
|
||||||
|
CoolRole = new[] { "admin", "cool", "fantastic" }
|
||||||
|
}, serializationOptions);
|
||||||
|
|
||||||
|
testJsRuntime.GetUserResult = JsonSerializer.Deserialize<IDictionary<string, object>>(serializedUser);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await runtime.GetAuthenticatedUser();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result.Identities);
|
||||||
|
Assert.True(result.Identity.IsAuthenticated);
|
||||||
|
Assert.Equal("Alfred", result.Identity.Name);
|
||||||
|
Assert.Equal("a", result.Identity.AuthenticationType);
|
||||||
|
Assert.True(result.IsInRole("admin"));
|
||||||
|
Assert.True(result.IsInRole("cool"));
|
||||||
|
Assert.True(result.IsInRole("fantastic"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoteAuthenticationService_GetUser_DoesNotMapScopesToRoles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testJsRuntime = new TestJsRuntime();
|
||||||
|
var options = CreateOptions("scope");
|
||||||
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||||
|
testJsRuntime,
|
||||||
|
options,
|
||||||
|
new TestNavigationManager());
|
||||||
|
|
||||||
|
var serializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true };
|
||||||
|
var serializedUser = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
CoolName = "Alfred",
|
||||||
|
CoolRole = new[] { "admin", "cool", "fantastic" }
|
||||||
|
}, serializationOptions);
|
||||||
|
|
||||||
|
testJsRuntime.GetUserResult = JsonSerializer.Deserialize<IDictionary<string, object>>(serializedUser);
|
||||||
|
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||||
|
{
|
||||||
|
Status = AccessTokenResultStatus.Success,
|
||||||
|
Token = new AccessToken
|
||||||
|
{
|
||||||
|
Value = "1234",
|
||||||
|
GrantedScopes = new[] { "All" },
|
||||||
|
Expires = new DateTimeOffset(2050, 5, 13, 0, 0, 0, TimeSpan.Zero)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await runtime.GetAuthenticatedUser();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result.Identities);
|
||||||
|
Assert.True(result.Identity.IsAuthenticated);
|
||||||
|
Assert.Equal("Alfred", result.Identity.Name);
|
||||||
|
Assert.Equal("a", result.Identity.AuthenticationType);
|
||||||
|
Assert.True(result.IsInRole("admin"));
|
||||||
|
Assert.True(result.IsInRole("cool"));
|
||||||
|
Assert.True(result.IsInRole("fantastic"));
|
||||||
|
Assert.Empty(result.FindAll("scope"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> CreateOptions(string scopeClaim = null)
|
||||||
|
{
|
||||||
|
return Options.Create(
|
||||||
|
new RemoteAuthenticationOptions<OidcProviderOptions>()
|
||||||
|
{
|
||||||
|
AuthenticationPaths = new RemoteAuthenticationApplicationPathsOptions
|
||||||
|
{
|
||||||
|
LogInPath = "login",
|
||||||
|
LogInCallbackPath = "a",
|
||||||
|
LogInFailedPath = "a",
|
||||||
|
RegisterPath = "a",
|
||||||
|
ProfilePath = "a",
|
||||||
|
RemoteRegisterPath = "a",
|
||||||
|
RemoteProfilePath = "a",
|
||||||
|
LogOutPath = "a",
|
||||||
|
LogOutCallbackPath = "a",
|
||||||
|
LogOutFailedPath = "a",
|
||||||
|
LogOutSucceededPath = "a",
|
||||||
|
},
|
||||||
|
UserOptions = new RemoteAuthenticationUserOptions
|
||||||
|
{
|
||||||
|
AuthenticationType = "a",
|
||||||
|
ScopeClaim = scopeClaim,
|
||||||
|
RoleClaim = "coolRole",
|
||||||
|
NameClaim = "coolName",
|
||||||
|
},
|
||||||
|
ProviderOptions = new OidcProviderOptions
|
||||||
|
{
|
||||||
|
Authority = "a",
|
||||||
|
ClientId = "a",
|
||||||
|
DefaultScopes = new[] { "openid" },
|
||||||
|
RedirectUri = "https://www.example.com/base/custom-login",
|
||||||
|
PostLogoutRedirectUri = "https://www.example.com/base/custom-logout",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestJsRuntime : IJSRuntime
|
||||||
|
{
|
||||||
|
public IList<(string identifier, object[] args)> PastInvocations { get; set; } = new List<(string, object[])>();
|
||||||
|
|
||||||
|
public RemoteAuthenticationResult<RemoteAuthenticationState> SignInResult { get; set; }
|
||||||
|
|
||||||
|
public RemoteAuthenticationResult<RemoteAuthenticationState> CompleteSignInResult { get; set; }
|
||||||
|
|
||||||
|
public RemoteAuthenticationResult<RemoteAuthenticationState> SignOutResult { get; set; }
|
||||||
|
|
||||||
|
public RemoteAuthenticationResult<RemoteAuthenticationState> CompleteSignOutResult { get; set; }
|
||||||
|
|
||||||
|
public RemoteAuthenticationResult<RemoteAuthenticationState> InitResult { get; set; }
|
||||||
|
|
||||||
|
public InternalAccessTokenResult GetAccessTokenResult { get; set; }
|
||||||
|
|
||||||
|
public IDictionary<string, object> GetUserResult { get; set; }
|
||||||
|
|
||||||
|
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
|
||||||
|
{
|
||||||
|
PastInvocations.Add((identifier, args));
|
||||||
|
return new ValueTask<TValue>((TValue)GetInvocationResult<TValue>(identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args)
|
||||||
|
{
|
||||||
|
PastInvocations.Add((identifier, args));
|
||||||
|
return new ValueTask<TValue>((TValue)GetInvocationResult<TValue>(identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
private object GetInvocationResult<TValue>(string identifier)
|
||||||
|
{
|
||||||
|
switch (identifier)
|
||||||
|
{
|
||||||
|
case "AuthenticationService.init":
|
||||||
|
return default;
|
||||||
|
case "AuthenticationService.signIn":
|
||||||
|
return SignInResult;
|
||||||
|
case "AuthenticationService.completeSignIn":
|
||||||
|
return CompleteSignInResult;
|
||||||
|
case "AuthenticationService.signOut":
|
||||||
|
return SignOutResult;
|
||||||
|
case "AuthenticationService.completeSignOut":
|
||||||
|
return CompleteSignOutResult;
|
||||||
|
case "AuthenticationService.getAccessToken":
|
||||||
|
return GetAccessTokenResult;
|
||||||
|
case "AuthenticationService.getUser":
|
||||||
|
return GetUserResult;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TestNavigationManager : NavigationManager
|
||||||
|
{
|
||||||
|
public TestNavigationManager() =>
|
||||||
|
Initialize("https://www.example.com/base/", "https://www.example.com/base/add-product");
|
||||||
|
|
||||||
|
protected override void NavigateToCore(string uri, bool forceLoad) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,695 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Components.RenderTree;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
public class RemoteAuthenticatorCoreTests
|
||||||
|
{
|
||||||
|
private const string _action = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.Action);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_Throws_ForInvalidAction()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var remoteAuthenticator = new RemoteAuthenticatorViewCore<RemoteAuthenticationState>();
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_Login_NavigatesToReturnUrlOnSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/login?returnUrl=https://www.example.com/base/fetchData");
|
||||||
|
|
||||||
|
authServiceMock.SignInCallback = _ => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Success,
|
||||||
|
State = remoteAuthenticator.AuthenticationState
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogIn
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("https://www.example.com/base/fetchData", jsRuntime.LastInvocation.args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_Login_DoesNothingOnRedirect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalUrl = "https://www.example.com/base/authentication/login?returnUrl=https://www.example.com/base/fetchData";
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(originalUrl);
|
||||||
|
|
||||||
|
authServiceMock.SignInCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Redirect,
|
||||||
|
State = remoteAuthenticator.AuthenticationState
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogIn
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(originalUrl, remoteAuthenticator.Navigation.Uri);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_Login_NavigatesToLoginFailureOnError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/login?returnUrl=https://www.example.com/base/fetchData");
|
||||||
|
|
||||||
|
authServiceMock.SignInCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Failure,
|
||||||
|
ErrorMessage = "There was an error trying to log in"
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogIn
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
"https://www.example.com/base/authentication/login-failed?message=There was an error trying to log in",
|
||||||
|
jsRuntime.LastInvocation.args[0]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_LoginCallback_ThrowsOnRedirectResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/login?returnUrl=https://www.example.com/base/fetchData");
|
||||||
|
|
||||||
|
authServiceMock.CompleteSignInCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Redirect
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogInCallback
|
||||||
|
});
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
async () => await renderer.Dispatcher.InvokeAsync<object>(async () =>
|
||||||
|
{
|
||||||
|
await remoteAuthenticator.SetParametersAsync(parameters);
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_LoginCallback_DoesNothingOnOperationCompleted()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalUrl = "https://www.example.com/base/authentication/login-callback?code=1234";
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
originalUrl);
|
||||||
|
|
||||||
|
authServiceMock.CompleteSignInCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.OperationCompleted
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogInCallback
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(originalUrl, remoteAuthenticator.Navigation.Uri);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_LoginCallback_NavigatesToReturnUrlFromStateOnSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/login-callback?code=1234");
|
||||||
|
|
||||||
|
var fetchDataUrl = "https://www.example.com/base/fetchData";
|
||||||
|
remoteAuthenticator.AuthenticationState.ReturnUrl = fetchDataUrl;
|
||||||
|
|
||||||
|
authServiceMock.CompleteSignInCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Success,
|
||||||
|
State = remoteAuthenticator.AuthenticationState
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogInCallback
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(fetchDataUrl, jsRuntime.LastInvocation.args[0]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_LoginCallback_NavigatesToLoginFailureOnError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/login-callback?code=1234");
|
||||||
|
|
||||||
|
var fetchDataUrl = "https://www.example.com/base/fetchData";
|
||||||
|
remoteAuthenticator.AuthenticationState.ReturnUrl = fetchDataUrl;
|
||||||
|
|
||||||
|
authServiceMock.CompleteSignInCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Failure,
|
||||||
|
ErrorMessage = "There was an error trying to log in"
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogInCallback
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
"https://www.example.com/base/authentication/login-failed?message=There was an error trying to log in",
|
||||||
|
jsRuntime.LastInvocation.args[0]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_Logout_NavigatesToReturnUrlOnSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/");
|
||||||
|
|
||||||
|
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||||
|
|
||||||
|
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Success,
|
||||||
|
State = remoteAuthenticator.AuthenticationState
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogOut
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("https://www.example.com/base/", jsRuntime.LastInvocation.args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_Logout_NavigatesToDefaultReturnUrlWhenNoReturnUrlIsPresent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/logout");
|
||||||
|
|
||||||
|
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||||
|
|
||||||
|
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Success,
|
||||||
|
State = remoteAuthenticator.AuthenticationState
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogOut
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("https://www.example.com/base/authentication/logged-out", jsRuntime.LastInvocation.args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_Logout_DoesNothingOnRedirect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalUrl = "https://www.example.com/base/authentication/login?returnUrl=https://www.example.com/base/fetchData";
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(originalUrl);
|
||||||
|
|
||||||
|
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||||
|
|
||||||
|
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Redirect,
|
||||||
|
State = remoteAuthenticator.AuthenticationState
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogOut
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(originalUrl, remoteAuthenticator.Navigation.Uri);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_Logout_RedirectsToFailureOnInvalidSignOutState()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/fetchData");
|
||||||
|
|
||||||
|
if(remoteAuthenticator.SignOutManager is TestSignOutSessionStateManager testManager)
|
||||||
|
{
|
||||||
|
testManager.SignOutState = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogOut
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
"https://www.example.com/base/authentication/logout-failed?message=The%20logout%20was%20not%20initiated%20from%20within%20the%20page.",
|
||||||
|
remoteAuthenticator.Navigation.Uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_Logout_NavigatesToLogoutFailureOnError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/fetchData");
|
||||||
|
|
||||||
|
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||||
|
|
||||||
|
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Failure,
|
||||||
|
ErrorMessage = "There was an error trying to log out"
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogOut
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
"https://www.example.com/base/authentication/logout-failed?message=There was an error trying to log out",
|
||||||
|
jsRuntime.LastInvocation.args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_LogoutCallback_ThrowsOnRedirectResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/logout-callback?returnUrl=https://www.example.com/base/fetchData");
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogOutCallback
|
||||||
|
});
|
||||||
|
|
||||||
|
authServiceMock.CompleteSignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Redirect,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
async () => await renderer.Dispatcher.InvokeAsync<object>(async () =>
|
||||||
|
{
|
||||||
|
await remoteAuthenticator.SetParametersAsync(parameters);
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_LogoutCallback_DoesNothingOnOperationCompleted()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalUrl = "https://www.example.com/base/authentication/logout-callback?code=1234";
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
originalUrl);
|
||||||
|
|
||||||
|
authServiceMock.CompleteSignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.OperationCompleted
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogOutCallback
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(originalUrl, remoteAuthenticator.Navigation.Uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_LogoutCallback_NavigatesToReturnUrlFromStateOnSuccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/logout-callback-callback?code=1234");
|
||||||
|
|
||||||
|
var fetchDataUrl = "https://www.example.com/base/fetchData";
|
||||||
|
remoteAuthenticator.AuthenticationState.ReturnUrl = fetchDataUrl;
|
||||||
|
|
||||||
|
authServiceMock.CompleteSignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Success,
|
||||||
|
State = remoteAuthenticator.AuthenticationState
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogOutCallback
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(fetchDataUrl, jsRuntime.LastInvocation.args[0]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticationManager_LogoutCallback_NavigatesToLoginFailureOnError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
|
"https://www.example.com/base/authentication/logout-callback?code=1234");
|
||||||
|
|
||||||
|
var fetchDataUrl = "https://www.example.com/base/fetchData";
|
||||||
|
remoteAuthenticator.AuthenticationState.ReturnUrl = fetchDataUrl;
|
||||||
|
|
||||||
|
authServiceMock.CompleteSignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
|
{
|
||||||
|
Status = RemoteAuthenticationStatus.Failure,
|
||||||
|
ErrorMessage = "There was an error trying to log out"
|
||||||
|
});
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = RemoteAuthenticationActions.LogOutCallback
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => remoteAuthenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
"https://www.example.com/base/authentication/logout-failed?message=There was an error trying to log out",
|
||||||
|
jsRuntime.LastInvocation.args[0]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<UIValidator> DisplaysRightUIData { get; } = new TheoryData<UIValidator>
|
||||||
|
{
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "login", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.LoggingIn = validator.Render; } }
|
||||||
|
},
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "login-callback", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.CompletingLoggingIn = validator.Render; } }
|
||||||
|
},
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "login-failed", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.LogInFailed = m => builder => validator.Render(builder); } }
|
||||||
|
},
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "profile", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.LoggingIn = validator.Render; } }
|
||||||
|
},
|
||||||
|
// Profile fragment overrides
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "profile", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.UserProfile = validator.Render; } }
|
||||||
|
},
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "register", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.LoggingIn = validator.Render; } }
|
||||||
|
},
|
||||||
|
// Register fragment overrides
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "register", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.Registering = validator.Render; } }
|
||||||
|
},
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "logout", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.LogOut = validator.Render; } }
|
||||||
|
},
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "logout-callback", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.CompletingLogOut = validator.Render; } }
|
||||||
|
},
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "logout-failed", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.LogOutFailed = m => builder => validator.Render(builder); } }
|
||||||
|
},
|
||||||
|
{ new UIValidator {
|
||||||
|
Action = "logged-out", SetupAction = (validator, remoteAuthenticator) => { remoteAuthenticator.LogOutSucceeded = validator.Render; } }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(DisplaysRightUIData))]
|
||||||
|
public async Task AuthenticationManager_DisplaysRightUI_ForEachStateAsync(UIValidator validator)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var renderer = new TestRenderer(new ServiceCollection().BuildServiceProvider());
|
||||||
|
var authenticator = new TestRemoteAuthenticatorView();
|
||||||
|
renderer.Attach(authenticator);
|
||||||
|
validator.Setup(authenticator);
|
||||||
|
|
||||||
|
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[_action] = validator.Action
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.Dispatcher.InvokeAsync<object>(() => authenticator.SetParametersAsync(parameters));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(validator.WasCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UIValidator
|
||||||
|
{
|
||||||
|
public string Action { get; set; }
|
||||||
|
public Action<UIValidator, RemoteAuthenticatorViewCore<RemoteAuthenticationState>> SetupAction { get; set; }
|
||||||
|
public bool WasCalled { get; set; }
|
||||||
|
public RenderFragment Render { get; set; }
|
||||||
|
|
||||||
|
public UIValidator() => Render = builder => WasCalled = true;
|
||||||
|
|
||||||
|
internal void Setup(TestRemoteAuthenticatorView manager) => SetupAction(this, manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static
|
||||||
|
(RemoteAuthenticatorViewCore<RemoteAuthenticationState> manager,
|
||||||
|
TestRenderer renderer,
|
||||||
|
TestRemoteAuthenticationService authenticationServiceMock,
|
||||||
|
TestJsRuntime js)
|
||||||
|
|
||||||
|
CreateAuthenticationManager(
|
||||||
|
string currentUri,
|
||||||
|
string baseUri = "https://www.example.com/base/")
|
||||||
|
{
|
||||||
|
var renderer = new TestRenderer(new ServiceCollection().BuildServiceProvider());
|
||||||
|
var remoteAuthenticator = new RemoteAuthenticatorViewCore<RemoteAuthenticationState>();
|
||||||
|
renderer.Attach(remoteAuthenticator);
|
||||||
|
|
||||||
|
var navigationManager = new TestNavigationManager(
|
||||||
|
baseUri,
|
||||||
|
currentUri);
|
||||||
|
remoteAuthenticator.Navigation = navigationManager;
|
||||||
|
|
||||||
|
remoteAuthenticator.AuthenticationState = new RemoteAuthenticationState();
|
||||||
|
remoteAuthenticator.ApplicationPaths = new RemoteAuthenticationApplicationPathsOptions();
|
||||||
|
|
||||||
|
var jsRuntime = new TestJsRuntime();
|
||||||
|
var authenticationServiceMock = new TestRemoteAuthenticationService(
|
||||||
|
jsRuntime,
|
||||||
|
Mock.Of<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>(),
|
||||||
|
navigationManager);
|
||||||
|
|
||||||
|
remoteAuthenticator.SignOutManager = new TestSignOutSessionStateManager();
|
||||||
|
|
||||||
|
remoteAuthenticator.AuthenticationService = authenticationServiceMock;
|
||||||
|
remoteAuthenticator.AuthenticationProvider = authenticationServiceMock;
|
||||||
|
remoteAuthenticator.JS = jsRuntime;
|
||||||
|
return (remoteAuthenticator, renderer, authenticationServiceMock, jsRuntime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestNavigationManager : NavigationManager
|
||||||
|
{
|
||||||
|
public TestNavigationManager(string baseUrl, string currentUrl) => Initialize(baseUrl, currentUrl);
|
||||||
|
|
||||||
|
protected override void NavigateToCore(string uri, bool forceLoad) => Uri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestSignOutSessionStateManager : SignOutSessionStateManager
|
||||||
|
{
|
||||||
|
public TestSignOutSessionStateManager() : base(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SignOutState { get; set; } = true;
|
||||||
|
|
||||||
|
public override ValueTask SetSignOutState()
|
||||||
|
{
|
||||||
|
SignOutState = true;
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<bool> ValidateSignOutState() => Task.FromResult(SignOutState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestJsRuntime : IJSRuntime
|
||||||
|
{
|
||||||
|
public (string identifier, object[] args) LastInvocation { get; set; }
|
||||||
|
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
|
||||||
|
{
|
||||||
|
LastInvocation = (identifier, args);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args)
|
||||||
|
{
|
||||||
|
LastInvocation = (identifier, args);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestRemoteAuthenticatorView : RemoteAuthenticatorViewCore<RemoteAuthenticationState>
|
||||||
|
{
|
||||||
|
public TestRemoteAuthenticatorView()
|
||||||
|
{
|
||||||
|
ApplicationPaths = new RemoteAuthenticationApplicationPathsOptions()
|
||||||
|
{
|
||||||
|
RemoteProfilePath = "Identity/Account/Manage",
|
||||||
|
RemoteRegisterPath = "Identity/Account/Register",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
if (Action == "register" || Action == "profile")
|
||||||
|
{
|
||||||
|
return base.OnParametersSetAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestRemoteAuthenticationService : RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>
|
||||||
|
{
|
||||||
|
public TestRemoteAuthenticationService(
|
||||||
|
IJSRuntime jsRuntime,
|
||||||
|
IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options,
|
||||||
|
TestNavigationManager navigationManager) :
|
||||||
|
base(jsRuntime, options, navigationManager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> SignInCallback { get; set; }
|
||||||
|
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> CompleteSignInCallback { get; set; }
|
||||||
|
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> SignOutCallback { get; set; }
|
||||||
|
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> CompleteSignOutCallback { get; set; }
|
||||||
|
public Func<Task<ClaimsPrincipal>> GetAuthenticatedUserCallback { get; set; }
|
||||||
|
|
||||||
|
public async override Task<AuthenticationState> GetAuthenticationStateAsync() => new AuthenticationState(await GetAuthenticatedUserCallback());
|
||||||
|
|
||||||
|
public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> CompleteSignInAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => CompleteSignInCallback(context);
|
||||||
|
|
||||||
|
protected internal override Task<ClaimsPrincipal> GetAuthenticatedUser() => GetAuthenticatedUserCallback();
|
||||||
|
|
||||||
|
public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> CompleteSignOutAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => CompleteSignOutCallback(context);
|
||||||
|
|
||||||
|
public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> SignInAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => SignInCallback(context);
|
||||||
|
|
||||||
|
public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> SignOutAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => SignOutCallback(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestRenderer : Renderer
|
||||||
|
{
|
||||||
|
public TestRenderer(IServiceProvider services)
|
||||||
|
: base(services, NullLoggerFactory.Instance)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Attach(IComponent component) => AssignRootComponentId(component);
|
||||||
|
|
||||||
|
private static readonly Dispatcher _dispatcher = Dispatcher.CreateDefault();
|
||||||
|
|
||||||
|
public override Dispatcher Dispatcher => _dispatcher;
|
||||||
|
|
||||||
|
protected override void HandleException(Exception exception)
|
||||||
|
=> ExceptionDispatchInfo.Capture(exception).Throw();
|
||||||
|
|
||||||
|
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) =>
|
||||||
|
Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Hosting;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CanResolve_AccessTokenProvider()
|
||||||
|
{
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault();
|
||||||
|
builder.Services.AddApiAuthorization();
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
host.Services.GetRequiredService<IAccessTokenProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanResolve_IRemoteAuthenticationService()
|
||||||
|
{
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault();
|
||||||
|
builder.Services.AddApiAuthorization();
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
host.Services.GetRequiredService<IRemoteAuthenticationService<RemoteAuthenticationState>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApiAuthorizationOptions_ConfigurationDefaultsGetApplied()
|
||||||
|
{
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault();
|
||||||
|
builder.Services.AddApiAuthorization();
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
|
||||||
|
|
||||||
|
var paths = options.Value.AuthenticationPaths;
|
||||||
|
|
||||||
|
Assert.Equal("authentication/login", paths.LogInPath);
|
||||||
|
Assert.Equal("authentication/login-callback", paths.LogInCallbackPath);
|
||||||
|
Assert.Equal("authentication/login-failed", paths.LogInFailedPath);
|
||||||
|
Assert.Equal("authentication/register", paths.RegisterPath);
|
||||||
|
Assert.Equal("authentication/profile", paths.ProfilePath);
|
||||||
|
Assert.Equal("Identity/Account/Register", paths.RemoteRegisterPath);
|
||||||
|
Assert.Equal("Identity/Account/Manage", paths.RemoteProfilePath);
|
||||||
|
Assert.Equal("authentication/logout", paths.LogOutPath);
|
||||||
|
Assert.Equal("authentication/logout-callback", paths.LogOutCallbackPath);
|
||||||
|
Assert.Equal("authentication/logout-failed", paths.LogOutFailedPath);
|
||||||
|
Assert.Equal("authentication/logged-out", paths.LogOutSucceededPath);
|
||||||
|
|
||||||
|
var user = options.Value.UserOptions;
|
||||||
|
Assert.Equal("Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests", user.AuthenticationType);
|
||||||
|
Assert.Equal("scope", user.ScopeClaim);
|
||||||
|
Assert.Null(user.RoleClaim);
|
||||||
|
Assert.Equal("name", user.NameClaim);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
|
||||||
|
options.Value.ProviderOptions.ConfigurationEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApiAuthorizationOptions_DefaultsCanBeOverriden()
|
||||||
|
{
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault();
|
||||||
|
builder.Services.AddApiAuthorization(options =>
|
||||||
|
{
|
||||||
|
options.AuthenticationPaths = new RemoteAuthenticationApplicationPathsOptions
|
||||||
|
{
|
||||||
|
LogInPath = "a",
|
||||||
|
LogInCallbackPath = "b",
|
||||||
|
LogInFailedPath = "c",
|
||||||
|
RegisterPath = "d",
|
||||||
|
ProfilePath = "e",
|
||||||
|
RemoteRegisterPath = "f",
|
||||||
|
RemoteProfilePath = "g",
|
||||||
|
LogOutPath = "h",
|
||||||
|
LogOutCallbackPath = "i",
|
||||||
|
LogOutFailedPath = "j",
|
||||||
|
LogOutSucceededPath = "k",
|
||||||
|
};
|
||||||
|
options.UserOptions = new RemoteAuthenticationUserOptions
|
||||||
|
{
|
||||||
|
AuthenticationType = "l",
|
||||||
|
ScopeClaim = "m",
|
||||||
|
RoleClaim = "n",
|
||||||
|
NameClaim = "o",
|
||||||
|
};
|
||||||
|
options.ProviderOptions = new ApiAuthorizationProviderOptions
|
||||||
|
{
|
||||||
|
ConfigurationEndpoint = "p"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
|
||||||
|
|
||||||
|
var paths = options.Value.AuthenticationPaths;
|
||||||
|
|
||||||
|
Assert.Equal("a", paths.LogInPath);
|
||||||
|
Assert.Equal("b", paths.LogInCallbackPath);
|
||||||
|
Assert.Equal("c", paths.LogInFailedPath);
|
||||||
|
Assert.Equal("d", paths.RegisterPath);
|
||||||
|
Assert.Equal("e", paths.ProfilePath);
|
||||||
|
Assert.Equal("f", paths.RemoteRegisterPath);
|
||||||
|
Assert.Equal("g", paths.RemoteProfilePath);
|
||||||
|
Assert.Equal("h", paths.LogOutPath);
|
||||||
|
Assert.Equal("i", paths.LogOutCallbackPath);
|
||||||
|
Assert.Equal("j", paths.LogOutFailedPath);
|
||||||
|
Assert.Equal("k", paths.LogOutSucceededPath);
|
||||||
|
|
||||||
|
var user = options.Value.UserOptions;
|
||||||
|
Assert.Equal("l", user.AuthenticationType);
|
||||||
|
Assert.Equal("m", user.ScopeClaim);
|
||||||
|
Assert.Equal("n", user.RoleClaim);
|
||||||
|
Assert.Equal("o", user.NameClaim);
|
||||||
|
|
||||||
|
Assert.Equal("p", options.Value.ProviderOptions.ConfigurationEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OidcOptions_ConfigurationDefaultsGetApplied()
|
||||||
|
{
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault();
|
||||||
|
builder.Services.Replace(ServiceDescriptor.Singleton<NavigationManager, TestNavigationManager>());
|
||||||
|
builder.Services.AddOidcAuthentication(options => { });
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>();
|
||||||
|
|
||||||
|
var paths = options.Value.AuthenticationPaths;
|
||||||
|
|
||||||
|
Assert.Equal("authentication/login", paths.LogInPath);
|
||||||
|
Assert.Equal("authentication/login-callback", paths.LogInCallbackPath);
|
||||||
|
Assert.Equal("authentication/login-failed", paths.LogInFailedPath);
|
||||||
|
Assert.Equal("authentication/register", paths.RegisterPath);
|
||||||
|
Assert.Equal("authentication/profile", paths.ProfilePath);
|
||||||
|
Assert.Null(paths.RemoteRegisterPath);
|
||||||
|
Assert.Null(paths.RemoteProfilePath);
|
||||||
|
Assert.Equal("authentication/logout", paths.LogOutPath);
|
||||||
|
Assert.Equal("authentication/logout-callback", paths.LogOutCallbackPath);
|
||||||
|
Assert.Equal("authentication/logout-failed", paths.LogOutFailedPath);
|
||||||
|
Assert.Equal("authentication/logged-out", paths.LogOutSucceededPath);
|
||||||
|
|
||||||
|
var user = options.Value.UserOptions;
|
||||||
|
Assert.Null(user.AuthenticationType);
|
||||||
|
Assert.Null(user.ScopeClaim);
|
||||||
|
Assert.Null(user.RoleClaim);
|
||||||
|
Assert.Equal("name", user.NameClaim);
|
||||||
|
|
||||||
|
var provider = options.Value.ProviderOptions;
|
||||||
|
Assert.Null(provider.Authority);
|
||||||
|
Assert.Null(provider.ClientId);
|
||||||
|
Assert.Equal(new[] { "openid", "profile" }, provider.DefaultScopes);
|
||||||
|
Assert.Equal("https://www.example.com/base/authentication/login-callback", provider.RedirectUri);
|
||||||
|
Assert.Equal("https://www.example.com/base/authentication/logout-callback", provider.PostLogoutRedirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OidcOptions_DefaultsCanBeOverriden()
|
||||||
|
{
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault();
|
||||||
|
builder.Services.AddOidcAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.AuthenticationPaths = new RemoteAuthenticationApplicationPathsOptions
|
||||||
|
{
|
||||||
|
LogInPath = "a",
|
||||||
|
LogInCallbackPath = "b",
|
||||||
|
LogInFailedPath = "c",
|
||||||
|
RegisterPath = "d",
|
||||||
|
ProfilePath = "e",
|
||||||
|
RemoteRegisterPath = "f",
|
||||||
|
RemoteProfilePath = "g",
|
||||||
|
LogOutPath = "h",
|
||||||
|
LogOutCallbackPath = "i",
|
||||||
|
LogOutFailedPath = "j",
|
||||||
|
LogOutSucceededPath = "k",
|
||||||
|
};
|
||||||
|
options.UserOptions = new RemoteAuthenticationUserOptions
|
||||||
|
{
|
||||||
|
AuthenticationType = "l",
|
||||||
|
ScopeClaim = "m",
|
||||||
|
RoleClaim = "n",
|
||||||
|
NameClaim = "o",
|
||||||
|
};
|
||||||
|
options.ProviderOptions = new OidcProviderOptions
|
||||||
|
{
|
||||||
|
Authority = "p",
|
||||||
|
ClientId = "q",
|
||||||
|
DefaultScopes = Array.Empty<string>(),
|
||||||
|
RedirectUri = "https://www.example.com/base/custom-login",
|
||||||
|
PostLogoutRedirectUri = "https://www.example.com/base/custom-logout",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>();
|
||||||
|
|
||||||
|
var paths = options.Value.AuthenticationPaths;
|
||||||
|
|
||||||
|
Assert.Equal("a", paths.LogInPath);
|
||||||
|
Assert.Equal("b", paths.LogInCallbackPath);
|
||||||
|
Assert.Equal("c", paths.LogInFailedPath);
|
||||||
|
Assert.Equal("d", paths.RegisterPath);
|
||||||
|
Assert.Equal("e", paths.ProfilePath);
|
||||||
|
Assert.Equal("f", paths.RemoteRegisterPath);
|
||||||
|
Assert.Equal("g", paths.RemoteProfilePath);
|
||||||
|
Assert.Equal("h", paths.LogOutPath);
|
||||||
|
Assert.Equal("i", paths.LogOutCallbackPath);
|
||||||
|
Assert.Equal("j", paths.LogOutFailedPath);
|
||||||
|
Assert.Equal("k", paths.LogOutSucceededPath);
|
||||||
|
|
||||||
|
var user = options.Value.UserOptions;
|
||||||
|
Assert.Equal("l", user.AuthenticationType);
|
||||||
|
Assert.Equal("m", user.ScopeClaim);
|
||||||
|
Assert.Equal("n", user.RoleClaim);
|
||||||
|
Assert.Equal("o", user.NameClaim);
|
||||||
|
|
||||||
|
var provider = options.Value.ProviderOptions;
|
||||||
|
Assert.Equal("p", provider.Authority);
|
||||||
|
Assert.Equal("q", provider.ClientId);
|
||||||
|
Assert.Equal(Array.Empty<string>(), provider.DefaultScopes);
|
||||||
|
Assert.Equal("https://www.example.com/base/custom-login", provider.RedirectUri);
|
||||||
|
Assert.Equal("https://www.example.com/base/custom-logout", provider.PostLogoutRedirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestNavigationManager : NavigationManager
|
||||||
|
{
|
||||||
|
public TestNavigationManager()
|
||||||
|
{
|
||||||
|
Initialize("https://www.example.com/base/", "https://www.example.com/base/counter");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void NavigateToCore(string uri, bool forceLoad) => throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
||||||
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
||||||
|
<DisableImplicitComponentsAnalyzers>true</DisableImplicitComponentsAnalyzers>
|
||||||
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<CascadingAuthenticationState>
|
||||||
|
<Router AppAssembly="@typeof(Program).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||||
|
<NotAuthorized>
|
||||||
|
<RedirectToLogin />
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeRouteView>
|
||||||
|
</Found>
|
||||||
|
<NotFound>
|
||||||
|
<LayoutView Layout="@typeof(MainLayout)">
|
||||||
|
<p>Sorry, there's nothing at this address.</p>
|
||||||
|
</LayoutView>
|
||||||
|
</NotFound>
|
||||||
|
</Router>
|
||||||
|
</CascadingAuthenticationState>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
@page "/authentication/{action}"
|
||||||
|
|
||||||
|
<RemoteAuthenticatorView Action="@Action" />
|
||||||
|
|
||||||
|
@code{
|
||||||
|
[Parameter] public string Action { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
@page "/fetchdata"
|
||||||
|
@using Wasm.Authentication.Shared
|
||||||
|
@attribute [Authorize]
|
||||||
|
@inject IAccessTokenProvider AuthenticationService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
<h1>Weather forecast</h1>
|
||||||
|
|
||||||
|
<p>This component demonstrates fetching data from the server.</p>
|
||||||
|
|
||||||
|
@if (forecasts == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Temp. (C)</th>
|
||||||
|
<th>Temp. (F)</th>
|
||||||
|
<th>Summary</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var forecast in forecasts)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@forecast.Date.ToShortDateString()</td>
|
||||||
|
<td>@forecast.TemperatureC</td>
|
||||||
|
<td>@forecast.TemperatureF</td>
|
||||||
|
<td>@forecast.Summary</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private WeatherForecast[] forecasts;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var httpClient = new HttpClient();
|
||||||
|
httpClient.BaseAddress = new Uri(Navigation.BaseUri);
|
||||||
|
|
||||||
|
var tokenResult = await AuthenticationService.RequestAccessToken();
|
||||||
|
|
||||||
|
if (tokenResult.TryGetToken(out var token))
|
||||||
|
{
|
||||||
|
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
|
||||||
|
forecasts = await httpClient.GetJsonAsync<WeatherForecast[]>("WeatherForecast");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo(tokenResult.RedirectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
@page "/"
|
||||||
|
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
|
||||||
|
Welcome to your new app.
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
@page "/User"
|
||||||
|
@attribute [Authorize]
|
||||||
|
@using System.Text.Json
|
||||||
|
@using System.Security.Claims
|
||||||
|
@inject IAccessTokenProvider AuthorizationService
|
||||||
|
|
||||||
|
<h1>Welcome @AuthenticatedUser?.Identity?.Name</h1>
|
||||||
|
|
||||||
|
<h2>Claims for the user</h2>
|
||||||
|
@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
|
||||||
|
{
|
||||||
|
<p class="claim">@(claim.Type): @claim.Value</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Access token for the user</h2>
|
||||||
|
<p id="access-token">@AccessToken?.Value</p>
|
||||||
|
|
||||||
|
<h2>Access token claims</h2>
|
||||||
|
@foreach (var claim in GetAccessTokenClaims())
|
||||||
|
{
|
||||||
|
<p>@(claim.Key): @claim.Value.ToString()</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (AccessToken != null)
|
||||||
|
{
|
||||||
|
<h2>Access token expires</h2>
|
||||||
|
<p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
|
||||||
|
<p id="access-token-expires">@AccessToken.Expires</p>
|
||||||
|
|
||||||
|
<h2>Access token granted scopes (as reported by the API)</h2>
|
||||||
|
@foreach (var scope in AccessToken.GrantedScopes)
|
||||||
|
{
|
||||||
|
<p>Scope: @scope</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }
|
||||||
|
|
||||||
|
public ClaimsPrincipal AuthenticatedUser { get; set; }
|
||||||
|
public AccessToken AccessToken { get; set; }
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
var state = await AuthenticationState;
|
||||||
|
var accessTokenResult = await AuthorizationService.RequestAccessToken();
|
||||||
|
if (!accessTokenResult.TryGetToken(out var token))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to provision the access token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken = token;
|
||||||
|
|
||||||
|
AuthenticatedUser = state.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IDictionary<string, object> GetAccessTokenClaims()
|
||||||
|
{
|
||||||
|
if (AccessToken == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// header.payload.signature
|
||||||
|
var payload = AccessToken.Value.Split(".")[1];
|
||||||
|
var base64Payload = payload.Replace('-', '+').Replace('_', '/').PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');
|
||||||
|
return JsonSerializer.Deserialize<IDictionary<string, object>>(Convert.FromBase64String(base64Payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Hosting;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Client
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
|
builder.Services.AddApiAuthorization();
|
||||||
|
|
||||||
|
builder.RootComponents.Add<App>("app");
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject SignOutSessionStateManager SignOutManager
|
||||||
|
|
||||||
|
<AuthorizeView Context="authenticationState">
|
||||||
|
<Authorized>
|
||||||
|
<a href="authentication/profile">Hello, @authenticationState.User.Identity.Name!</a>
|
||||||
|
<button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<a href="authentication/register">Register</a>
|
||||||
|
<a href="authentication/login">Log in</a>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
|
||||||
|
@code{
|
||||||
|
public async Task BeginSignOut()
|
||||||
|
{
|
||||||
|
await SignOutManager.SetSignOutState();
|
||||||
|
Navigation.NavigateTo("authentication/logout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
<NavMenu />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<div class="top-row px-4 auth">
|
||||||
|
<LoginDisplay />
|
||||||
|
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content px-4">
|
||||||
|
@Body
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<div class="top-row pl-4 navbar navbar-dark">
|
||||||
|
<a class="navbar-brand" href="">Wasm.Authentication.Client</a>
|
||||||
|
<button class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||||
|
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="counter">
|
||||||
|
<span class="oi oi-plus" aria-hidden="true"></span> Counter
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="fetchdata">
|
||||||
|
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="user">
|
||||||
|
<span class="oi oi-list-rich" aria-hidden="true"></span> User
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool collapseNavMenu = true;
|
||||||
|
|
||||||
|
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||||
|
|
||||||
|
private void ToggleNavMenu()
|
||||||
|
{
|
||||||
|
collapseNavMenu = !collapseNavMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@using Microsoft.Extensions.Options
|
||||||
|
@inject IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> Options
|
||||||
|
@code {
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"{Options.Value.AuthenticationPaths.LogInPath}?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<RazorLangVersion>3.0</RazorLangVersion>
|
||||||
|
<ReferenceBlazorBuildLocally>true</ReferenceBlazorBuildLocally>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Blazor" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Blazor.HttpClient" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Wasm.Authentication.Shared\Wasm.Authentication.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
@using System.Net.Http
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using Wasm.Authentication.Client
|
||||||
|
@using Wasm.Authentication.Client.Shared
|
||||||
|
@using Wasm.Authentication.Shared
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,181 @@
|
||||||
|
html, body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, .btn-link {
|
||||||
|
color: #0366d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1b6ec2;
|
||||||
|
border-color: #1861ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
app {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .top-row {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #d6d5d5;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .top-row > a, .main .top-row .btn-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .top-row a:first-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .top-row {
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .navbar-brand {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .oi {
|
||||||
|
width: 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
vertical-align: text-top;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-item {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-item:first-of-type {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-item:last-of-type {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-item a {
|
||||||
|
color: #d7d7d7;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-item a.active {
|
||||||
|
background-color: rgba(255,255,255,0.25);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-item a:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-top: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid.modified:not([type=checkbox]) {
|
||||||
|
outline: 1px solid #26b050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
outline: 1px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui {
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.main .top-row:not(.auth) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .top-row.auth {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .top-row a, .main .top-row .btn-link {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
app {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
height: 100vh;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .top-row {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main > div {
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
padding-right: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .collapse {
|
||||||
|
/* Never collapse the sidebar for wide screens */
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<title>Wasm.Authentication.Client</title>
|
||||||
|
<base href="/" />
|
||||||
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="css/site.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<app>Loading...</app>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href="" class="reload">Reload</a>
|
||||||
|
<a class="dismiss">🗙</a>
|
||||||
|
</div>
|
||||||
|
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
|
||||||
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
app.db
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Server.Controllers
|
||||||
|
{
|
||||||
|
public class OidcConfigurationController : Controller
|
||||||
|
{
|
||||||
|
public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider, ILogger<OidcConfigurationController> logger)
|
||||||
|
{
|
||||||
|
ClientRequestParametersProvider = clientRequestParametersProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IClientRequestParametersProvider ClientRequestParametersProvider { get; }
|
||||||
|
|
||||||
|
[HttpGet("_configuration/{clientId}")]
|
||||||
|
public IActionResult GetClientRequestParameters([FromRoute]string clientId)
|
||||||
|
{
|
||||||
|
var parameters = ClientRequestParametersProvider.GetClientParameters(HttpContext, clientId);
|
||||||
|
return Ok(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Wasm.Authentication.Shared;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Server.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
[Route("[controller]")]
|
||||||
|
public class WeatherForecastController : ControllerBase
|
||||||
|
{
|
||||||
|
private static readonly string[] Summaries = new[]
|
||||||
|
{
|
||||||
|
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly ILogger<WeatherForecastController> logger;
|
||||||
|
|
||||||
|
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IEnumerable<WeatherForecast> Get()
|
||||||
|
{
|
||||||
|
var rng = new Random();
|
||||||
|
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||||
|
{
|
||||||
|
Date = DateTime.Now.AddDays(index),
|
||||||
|
TemperatureC = rng.Next(-20, 55),
|
||||||
|
Summary = Summaries[rng.Next(Summaries.Length)]
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
using Wasm.Authentication.Server.Models;
|
||||||
|
using IdentityServer4.EntityFramework.Options;
|
||||||
|
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Server.Data
|
||||||
|
{
|
||||||
|
public class ApplicationDbContext : ApiAuthorizationDbContext<ApplicationUser>
|
||||||
|
{
|
||||||
|
public ApplicationDbContext(
|
||||||
|
DbContextOptions options,
|
||||||
|
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,352 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Wasm.Authentication.Server.Data;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20200123141439_Initial")]
|
||||||
|
partial class Initial
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "3.1.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserCode")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(50000);
|
||||||
|
|
||||||
|
b.Property<string>("DeviceCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiration")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.HasKey("UserCode");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Expiration");
|
||||||
|
|
||||||
|
b.ToTable("DeviceCodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(50000);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiration")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("Expiration");
|
||||||
|
|
||||||
|
b.HasIndex("SubjectId", "ClientId", "Type");
|
||||||
|
|
||||||
|
b.ToTable("PersistedGrants");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(128);
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(128);
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(128);
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(128);
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Wasm.Authentication.Server.Models.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,278 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class Initial : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(nullable: false),
|
||||||
|
Name = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
|
NormalizedName = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
|
ConcurrencyStamp = table.Column<string>(nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUsers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(nullable: false),
|
||||||
|
UserName = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
|
NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
|
Email = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
|
NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
|
||||||
|
EmailConfirmed = table.Column<bool>(nullable: false),
|
||||||
|
PasswordHash = table.Column<string>(nullable: true),
|
||||||
|
SecurityStamp = table.Column<string>(nullable: true),
|
||||||
|
ConcurrencyStamp = table.Column<string>(nullable: true),
|
||||||
|
PhoneNumber = table.Column<string>(nullable: true),
|
||||||
|
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
|
||||||
|
TwoFactorEnabled = table.Column<bool>(nullable: false),
|
||||||
|
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
|
||||||
|
LockoutEnabled = table.Column<bool>(nullable: false),
|
||||||
|
AccessFailedCount = table.Column<int>(nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DeviceCodes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserCode = table.Column<string>(maxLength: 200, nullable: false),
|
||||||
|
DeviceCode = table.Column<string>(maxLength: 200, nullable: false),
|
||||||
|
SubjectId = table.Column<string>(maxLength: 200, nullable: true),
|
||||||
|
ClientId = table.Column<string>(maxLength: 200, nullable: false),
|
||||||
|
CreationTime = table.Column<DateTime>(nullable: false),
|
||||||
|
Expiration = table.Column<DateTime>(nullable: false),
|
||||||
|
Data = table.Column<string>(maxLength: 50000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DeviceCodes", x => x.UserCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PersistedGrants",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<string>(maxLength: 200, nullable: false),
|
||||||
|
Type = table.Column<string>(maxLength: 50, nullable: false),
|
||||||
|
SubjectId = table.Column<string>(maxLength: 200, nullable: true),
|
||||||
|
ClientId = table.Column<string>(maxLength: 200, nullable: false),
|
||||||
|
CreationTime = table.Column<DateTime>(nullable: false),
|
||||||
|
Expiration = table.Column<DateTime>(nullable: true),
|
||||||
|
Data = table.Column<string>(maxLength: 50000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PersistedGrants", x => x.Key);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetRoleClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
RoleId = table.Column<string>(nullable: false),
|
||||||
|
ClaimType = table.Column<string>(nullable: true),
|
||||||
|
ClaimValue = table.Column<string>(nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "AspNetRoles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserId = table.Column<string>(nullable: false),
|
||||||
|
ClaimType = table.Column<string>(nullable: true),
|
||||||
|
ClaimValue = table.Column<string>(nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserLogins",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
LoginProvider = table.Column<string>(maxLength: 128, nullable: false),
|
||||||
|
ProviderKey = table.Column<string>(maxLength: 128, nullable: false),
|
||||||
|
ProviderDisplayName = table.Column<string>(nullable: true),
|
||||||
|
UserId = table.Column<string>(nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<string>(nullable: false),
|
||||||
|
RoleId = table.Column<string>(nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "AspNetRoles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserTokens",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<string>(nullable: false),
|
||||||
|
LoginProvider = table.Column<string>(maxLength: 128, nullable: false),
|
||||||
|
Name = table.Column<string>(maxLength: 128, nullable: false),
|
||||||
|
Value = table.Column<string>(nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetRoleClaims_RoleId",
|
||||||
|
table: "AspNetRoleClaims",
|
||||||
|
column: "RoleId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "RoleNameIndex",
|
||||||
|
table: "AspNetRoles",
|
||||||
|
column: "NormalizedName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserClaims_UserId",
|
||||||
|
table: "AspNetUserClaims",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserLogins_UserId",
|
||||||
|
table: "AspNetUserLogins",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserRoles_RoleId",
|
||||||
|
table: "AspNetUserRoles",
|
||||||
|
column: "RoleId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "EmailIndex",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
column: "NormalizedEmail");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UserNameIndex",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
column: "NormalizedUserName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DeviceCodes_DeviceCode",
|
||||||
|
table: "DeviceCodes",
|
||||||
|
column: "DeviceCode",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DeviceCodes_Expiration",
|
||||||
|
table: "DeviceCodes",
|
||||||
|
column: "Expiration");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PersistedGrants_Expiration",
|
||||||
|
table: "PersistedGrants",
|
||||||
|
column: "Expiration");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PersistedGrants_SubjectId_ClientId_Type",
|
||||||
|
table: "PersistedGrants",
|
||||||
|
columns: new[] { "SubjectId", "ClientId", "Type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetRoleClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserLogins");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserRoles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserTokens");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DeviceCodes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PersistedGrants");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetRoles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,350 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Wasm.Authentication.Server.Data;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "3.1.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserCode")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(50000);
|
||||||
|
|
||||||
|
b.Property<string>("DeviceCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiration")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.HasKey("UserCode");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Expiration");
|
||||||
|
|
||||||
|
b.ToTable("DeviceCodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(50000);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiration")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("Expiration");
|
||||||
|
|
||||||
|
b.HasIndex("SubjectId", "ClientId", "Type");
|
||||||
|
|
||||||
|
b.ToTable("PersistedGrants");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(128);
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(128);
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(128);
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(128);
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Wasm.Authentication.Server.Models.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Server.Models
|
||||||
|
{
|
||||||
|
public class ApplicationUser : IdentityUser
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
@page
|
||||||
|
@namespace Wasm.Authentication.Server.Pages
|
||||||
|
@model ErrorModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
@if (Model.ShowRequestId)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Development Mode</h3>
|
||||||
|
<p>
|
||||||
|
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||||
|
It can result in displaying sensitive information from exceptions to end users.
|
||||||
|
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||||
|
and restarting the app.
|
||||||
|
</p>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Server
|
||||||
|
{
|
||||||
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
|
public class ErrorModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly ILogger<ErrorModel> _logger;
|
||||||
|
|
||||||
|
public ErrorModel(ILogger<ErrorModel> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string RequestId { get; set; }
|
||||||
|
|
||||||
|
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
@using Microsoft.AspNetCore.Identity
|
||||||
|
@using Wasm.Authentication.Server.Models
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
|
||||||
|
@inject SignInManager<ApplicationUser> SignInManager
|
||||||
|
@inject UserManager<ApplicationUser> UserManager
|
||||||
|
|
||||||
|
@{
|
||||||
|
string returnUrl = null;
|
||||||
|
var query = ViewContext.HttpContext.Request.Query;
|
||||||
|
if (query.ContainsKey("returnUrl"))
|
||||||
|
{
|
||||||
|
returnUrl = query["returnUrl"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
@if (SignInManager.IsSignedIn(User))
|
||||||
|
{
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/">
|
||||||
|
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register" asp-route-returnUrl="@returnUrl">Register</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login" asp-route-returnUrl="@returnUrl">Login</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
using Microsoft.AspNetCore;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Server
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
CreateHostBuilder(args).Build().Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
|
Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
|
{
|
||||||
|
webBuilder.UseSetting(WebHostDefaults.ApplicationKey, typeof(Program).Assembly.GetName().Name);
|
||||||
|
|
||||||
|
// We require this line because we run in Production environment
|
||||||
|
// and static web assets are only on by default during development.
|
||||||
|
webBuilder.UseStaticWebAssets();
|
||||||
|
webBuilder.UseStartup<Startup>();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Wasm.Authentication.Server.Data;
|
||||||
|
using Wasm.Authentication.Server.Models;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Server
|
||||||
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public Startup(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfiguration Configuration { get; }
|
||||||
|
|
||||||
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
|
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
|
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
|
||||||
|
.AddEntityFrameworkStores<ApplicationDbContext>();
|
||||||
|
|
||||||
|
services.AddIdentityServer()
|
||||||
|
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
|
||||||
|
|
||||||
|
services.AddAuthentication()
|
||||||
|
.AddIdentityServerJwt();
|
||||||
|
|
||||||
|
services.AddMvc();
|
||||||
|
services.AddResponseCompression(opts =>
|
||||||
|
{
|
||||||
|
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
|
||||||
|
new[] { "application/octet-stream" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
app.UseResponseCompression();
|
||||||
|
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
app.UseBlazorDebugging();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseClientSideBlazorFiles<Client.Program>();
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseIdentityServer();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapControllers();
|
||||||
|
endpoints.MapRazorPages();
|
||||||
|
endpoints.MapFallbackToClientSideBlazor<Client.Program>("index.html");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
||||||
|
<DisableImplicitComponentsAnalyzers>true</DisableImplicitComponentsAnalyzers>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Identity.UI" />
|
||||||
|
<Reference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
|
<Reference Include="Microsoft.EntityFrameworkCore.SQLite" />
|
||||||
|
<Reference Include="Microsoft.EntityFrameworkCore.Tools" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Blazor.Server" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Wasm.Authentication.Client\Wasm.Authentication.Client.csproj" />
|
||||||
|
<ProjectReference Include="..\Wasm.Authentication.Shared\Wasm.Authentication.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "DataSource=app.db"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IdentityServer": {
|
||||||
|
"Key": {
|
||||||
|
"Type": "Development"
|
||||||
|
},
|
||||||
|
"Clients": {
|
||||||
|
"Wasm.Authentication.Client": {
|
||||||
|
"Profile": "IdentityServerSPA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<LangVersion>7.3</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Shared
|
||||||
|
{
|
||||||
|
public class WeatherForecast
|
||||||
|
{
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureC { get; set; }
|
||||||
|
|
||||||
|
public string Summary { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,10 @@
|
||||||
<OutputPath />
|
<OutputPath />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<EnableTypeScriptNuGetTarget>true</EnableTypeScriptNuGetTarget>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PackageTags>aspnetcore;components</PackageTags>
|
<PackageTags>aspnetcore;components</PackageTags>
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -72,12 +72,12 @@ export function attachToEventDelegator(eventDelegator: EventDelegator) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function navigateTo(uri: string, forceLoad: boolean) {
|
export function navigateTo(uri: string, forceLoad: boolean, replace: boolean = false) {
|
||||||
const absoluteUri = toAbsoluteUri(uri);
|
const absoluteUri = toAbsoluteUri(uri);
|
||||||
|
|
||||||
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
|
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
|
||||||
// It's an internal URL, so do client-side navigation
|
// It's an internal URL, so do client-side navigation
|
||||||
performInternalNavigation(absoluteUri, false);
|
performInternalNavigation(absoluteUri, false, replace);
|
||||||
} else if (forceLoad && location.href === uri) {
|
} else if (forceLoad && location.href === uri) {
|
||||||
// Force-loading the same URL you're already on requires special handling to avoid
|
// Force-loading the same URL you're already on requires special handling to avoid
|
||||||
// triggering browser-specific behavior issues.
|
// triggering browser-specific behavior issues.
|
||||||
|
|
@ -85,13 +85,15 @@ export function navigateTo(uri: string, forceLoad: boolean) {
|
||||||
const temporaryUri = uri + '?';
|
const temporaryUri = uri + '?';
|
||||||
history.replaceState(null, '', temporaryUri);
|
history.replaceState(null, '', temporaryUri);
|
||||||
location.replace(uri);
|
location.replace(uri);
|
||||||
|
} else if (replace){
|
||||||
|
history.replaceState(null, '', absoluteUri)
|
||||||
} else {
|
} else {
|
||||||
// It's either an external URL, or forceLoad is requested, so do a full page load
|
// It's either an external URL, or forceLoad is requested, so do a full page load
|
||||||
location.href = uri;
|
location.href = uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean) {
|
function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean, replace: boolean = false) {
|
||||||
// Since this was *not* triggered by a back/forward gesture (that goes through a different
|
// Since this was *not* triggered by a back/forward gesture (that goes through a different
|
||||||
// code path starting with a popstate event), we don't want to preserve the current scroll
|
// code path starting with a popstate event), we don't want to preserve the current scroll
|
||||||
// position, so reset it.
|
// position, so reset it.
|
||||||
|
|
@ -99,7 +101,11 @@ function performInternalNavigation(absoluteInternalHref: string, interceptedLink
|
||||||
// we render the new page. As a best approximation, wait until the next batch.
|
// we render the new page. As a best approximation, wait until the next batch.
|
||||||
resetScrollAfterNextBatch();
|
resetScrollAfterNextBatch();
|
||||||
|
|
||||||
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
|
if(!replace){
|
||||||
|
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
|
||||||
|
}else{
|
||||||
|
history.replaceState(null, /* ignored title */ '', absoluteInternalHref);
|
||||||
|
}
|
||||||
notifyLocationChanged(interceptedLink);
|
notifyLocationChanged(interceptedLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
|
||||||
m => m.Value);
|
m => m.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static string FindSampleOrTestSitePath(string projectName)
|
public static string FindSampleOrTestSitePath(string projectName)
|
||||||
{
|
{
|
||||||
var projects = _projects.Value;
|
var projects = _projects.Value;
|
||||||
if (projects.TryGetValue(projectName, out var dir))
|
if (projects.TryGetValue(projectName, out var dir))
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
<ProjectReference Include="..\..\Blazor\DevServer\src\Microsoft.AspNetCore.Blazor.DevServer.csproj" />
|
<ProjectReference Include="..\..\Blazor\DevServer\src\Microsoft.AspNetCore.Blazor.DevServer.csproj" />
|
||||||
<ProjectReference Include="..\testassets\BasicTestApp\BasicTestApp.csproj" />
|
<ProjectReference Include="..\testassets\BasicTestApp\BasicTestApp.csproj" />
|
||||||
<ProjectReference Include="..\testassets\TestServer\Components.TestServer.csproj" />
|
<ProjectReference Include="..\testassets\TestServer\Components.TestServer.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Blazor\testassets\Wasm.Authentication.Server\Wasm.Authentication.Server.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Shared testing infrastructure for running E2E tests using selenium -->
|
<!-- Shared testing infrastructure for running E2E tests using selenium -->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,441 @@
|
||||||
|
// 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.Data.Common;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||||
|
using Microsoft.AspNetCore.E2ETesting;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using OpenQA.Selenium;
|
||||||
|
using OpenQA.Selenium.Support.Extensions;
|
||||||
|
using OpenQA.Selenium.Support.UI;
|
||||||
|
using Wasm.Authentication.Server;
|
||||||
|
using Wasm.Authentication.Server.Data;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
|
{
|
||||||
|
public class WebAssemblyAuthenticationTests : ServerTestBase<AspNetSiteServerFixture>, IDisposable
|
||||||
|
{
|
||||||
|
private static readonly SqliteConnection _connection;
|
||||||
|
|
||||||
|
// We create a conection here and open it as the in memory Db will delete the database
|
||||||
|
// as soon as there are no open connections to it.
|
||||||
|
static WebAssemblyAuthenticationTests()
|
||||||
|
{
|
||||||
|
_connection = new SqliteConnection($"DataSource=:memory:");
|
||||||
|
_connection.Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebAssemblyAuthenticationTests(
|
||||||
|
BrowserFixture browserFixture,
|
||||||
|
AspNetSiteServerFixture serverFixture,
|
||||||
|
ITestOutputHelper output) :
|
||||||
|
base(browserFixture, serverFixture, output)
|
||||||
|
{
|
||||||
|
_serverFixture.ApplicationAssembly = typeof(Program).Assembly;
|
||||||
|
|
||||||
|
_serverFixture.AdditionalArguments.Clear();
|
||||||
|
|
||||||
|
_serverFixture.BuildWebHostMethod = args => Program.CreateHostBuilder(args)
|
||||||
|
.ConfigureServices(services => SetupTestDatabase<ApplicationDbContext>(services, _connection))
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitializeAsyncCore()
|
||||||
|
{
|
||||||
|
Browser.Navigate().GoToUrl("data:");
|
||||||
|
Navigate("/", noReload: true);
|
||||||
|
EnsureDatabaseCreated(_serverFixture.Host.Services);
|
||||||
|
Browser.ExecuteJavaScript("sessionStorage.clear()");
|
||||||
|
Browser.ExecuteJavaScript("localStorage.clear()");
|
||||||
|
Browser.Manage().Cookies.DeleteAllCookies();
|
||||||
|
Browser.Navigate().Refresh();
|
||||||
|
WaitUntilLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WasmAuthentication_Loads()
|
||||||
|
{
|
||||||
|
Assert.Equal("Wasm.Authentication.Client", Browser.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnonymousUser_GetsRedirectedToLogin_AndBackToOriginalProtectedResource()
|
||||||
|
{
|
||||||
|
var link = By.PartialLinkText("Fetch data");
|
||||||
|
var page = "/Identity/Account/Login";
|
||||||
|
|
||||||
|
ClickAndNavigate(link, page);
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
|
||||||
|
FirstTimeRegister(userName, password);
|
||||||
|
|
||||||
|
ValidateFetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClickAndNavigate(By link, string page)
|
||||||
|
{
|
||||||
|
Browser.FindElement(link).Click();
|
||||||
|
Browser.Contains(page, () => Browser.Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnonymousUser_CanRegister_AndGetLoggedIn()
|
||||||
|
{
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Register"), "/Identity/Account/Register");
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
RegisterCore(userName, password);
|
||||||
|
|
||||||
|
// Need to navigate to fetch page
|
||||||
|
Browser.FindElement(By.PartialLinkText("Fetch data")).Click();
|
||||||
|
|
||||||
|
// Can navigate to the 'fetch data' page
|
||||||
|
ValidateFetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthenticatedUser_ProfileIncludesDetails_And_AccessToken()
|
||||||
|
{
|
||||||
|
ClickAndNavigate(By.PartialLinkText("User"), "/Identity/Account/Login");
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
FirstTimeRegister(userName, password);
|
||||||
|
|
||||||
|
Browser.Contains("user", () => Browser.Url);
|
||||||
|
Browser.Equal($"Welcome {userName}", () => Browser.FindElement(By.TagName("h1")).Text);
|
||||||
|
|
||||||
|
var claims = Browser.FindElements(By.CssSelector("p.claim"))
|
||||||
|
.Select(e =>
|
||||||
|
{
|
||||||
|
var pair = e.Text.Split(":");
|
||||||
|
return (pair[0].Trim(), pair[1].Trim());
|
||||||
|
})
|
||||||
|
.Where(c => !new[] { "s_hash", "auth_time", "sid", "sub" }.Contains(c.Item1))
|
||||||
|
.OrderBy(o => o.Item1)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Equal(4, claims.Length);
|
||||||
|
|
||||||
|
Assert.Equal(new[]
|
||||||
|
{
|
||||||
|
("amr", "pwd"),
|
||||||
|
("idp", "local"),
|
||||||
|
("name", userName),
|
||||||
|
("preferred_username", userName)
|
||||||
|
},
|
||||||
|
claims);
|
||||||
|
|
||||||
|
var token = Browser.Exists(By.Id("access-token")).Text;
|
||||||
|
Assert.NotNull(token);
|
||||||
|
var payload = JsonSerializer.Deserialize<JwtPayload>(Base64UrlTextEncoder.Decode(token.Split(".")[1]));
|
||||||
|
|
||||||
|
Assert.StartsWith("http://127.0.0.1", payload.Issuer);
|
||||||
|
Assert.StartsWith("Wasm.Authentication.ServerAPI", payload.Audience);
|
||||||
|
Assert.StartsWith("Wasm.Authentication.Client", payload.ClientId);
|
||||||
|
Assert.Equal(new[]
|
||||||
|
{
|
||||||
|
"openid",
|
||||||
|
"profile",
|
||||||
|
"Wasm.Authentication.ServerAPI"
|
||||||
|
},
|
||||||
|
payload.Scopes);
|
||||||
|
|
||||||
|
var currentTime = DateTimeOffset.Parse(Browser.Exists(By.Id("current-time")).Text);
|
||||||
|
var tokenExpiration = DateTimeOffset.Parse(Browser.Exists(By.Id("access-token-expires")).Text);
|
||||||
|
Assert.True(currentTime.AddMinutes(50) < tokenExpiration);
|
||||||
|
Assert.True(currentTime.AddMinutes(60) >= tokenExpiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthenticatedUser_CanGoToProfile()
|
||||||
|
{
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Register"), "/Identity/Account/Register");
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
RegisterCore(userName, password);
|
||||||
|
|
||||||
|
Browser.Exists(By.PartialLinkText($"Hello, {userName}!")).Click();
|
||||||
|
Browser.Contains("/Identity/Account/Manage", () => Browser.Url);
|
||||||
|
|
||||||
|
Browser.Navigate().Back();
|
||||||
|
Browser.Equal("/", () => new Uri(Browser.Url).PathAndQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegisterAndBack_DoesNotCause_RedirectLoop()
|
||||||
|
{
|
||||||
|
Browser.FindElement(By.PartialLinkText("Register")).Click();
|
||||||
|
|
||||||
|
// We will be redirected to the identity UI
|
||||||
|
Browser.Contains("/Identity/Account/Register", () => Browser.Url);
|
||||||
|
|
||||||
|
Browser.Navigate().Back();
|
||||||
|
|
||||||
|
Browser.Equal("/", () => new Uri(Browser.Url).PathAndQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoginAndBack_DoesNotCause_RedirectLoop()
|
||||||
|
{
|
||||||
|
Browser.FindElement(By.PartialLinkText("Log in")).Click();
|
||||||
|
|
||||||
|
// We will be redirected to the identity UI
|
||||||
|
Browser.Contains("/Identity/Account/Login", () => Browser.Url);
|
||||||
|
|
||||||
|
Browser.Navigate().Back();
|
||||||
|
|
||||||
|
Browser.Equal("/", () => new Uri(Browser.Url).PathAndQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NewlyRegisteredUser_CanLogOut()
|
||||||
|
{
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Register"), "/Identity/Account/Register");
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
RegisterCore(userName, password);
|
||||||
|
|
||||||
|
ValidateLogout();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlreadyRegisteredUser_CanLogOut()
|
||||||
|
{
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Register"), "/Identity/Account/Register");
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
RegisterCore(userName, password);
|
||||||
|
|
||||||
|
ValidateLogout();
|
||||||
|
|
||||||
|
Browser.Navigate().GoToUrl("data:");
|
||||||
|
Navigate("/");
|
||||||
|
WaitUntilLoaded();
|
||||||
|
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Log in"), "/Identity/Account/Login");
|
||||||
|
|
||||||
|
// Now we can login
|
||||||
|
LoginCore(userName, password);
|
||||||
|
|
||||||
|
ValidateLoggedIn(userName);
|
||||||
|
|
||||||
|
ValidateLogout();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoggedInUser_OnTheIdP_CanLogInSilently()
|
||||||
|
{
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Register"), "/Identity/Account/Register");
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
RegisterCore(userName, password);
|
||||||
|
ValidateLoggedIn(userName);
|
||||||
|
|
||||||
|
// Clear the existing storage on the page and refresh
|
||||||
|
Browser.ExecuteJavaScript("sessionStorage.clear()");
|
||||||
|
Browser.Navigate().Refresh();
|
||||||
|
Browser.Exists(By.PartialLinkText("Log in"));
|
||||||
|
|
||||||
|
Browser.FindElement(By.PartialLinkText("Log in")).Click();
|
||||||
|
ValidateLoggedIn(userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanNotRedirect_To_External_ReturnUrl()
|
||||||
|
{
|
||||||
|
Browser.Navigate().GoToUrl(new Uri(new Uri(Browser.Url), "/authentication/login?returnUrl=https%3A%2F%2Fwww.bing.com").AbsoluteUri);
|
||||||
|
WaitUntilLoaded(skipHeader: true);
|
||||||
|
Assert.NotEmpty(Browser.GetBrowserLogs(LogLevel.Severe));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanNotTrigger_Logout_WithNavigation()
|
||||||
|
{
|
||||||
|
Browser.Navigate().GoToUrl(new Uri(new Uri(Browser.Url), "/authentication/logout").AbsoluteUri);
|
||||||
|
WaitUntilLoaded(skipHeader: true);
|
||||||
|
Browser.Contains("/authentication/logout-failed", () => Browser.Url);
|
||||||
|
await Task.Delay(3000);
|
||||||
|
Browser.Contains("/authentication/logout-failed", () => Browser.Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateLoggedIn(string userName)
|
||||||
|
{
|
||||||
|
Browser.Exists(By.CssSelector("button.nav-link.btn.btn-link"));
|
||||||
|
Browser.Exists(By.PartialLinkText($"Hello, {userName}!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoginCore(string userName, string password)
|
||||||
|
{
|
||||||
|
Browser.FindElement(By.PartialLinkText("Login")).Click();
|
||||||
|
Browser.Exists(By.Name("Input.Email"));
|
||||||
|
Browser.FindElement(By.Name("Input.Email")).SendKeys(userName);
|
||||||
|
Browser.FindElement(By.Name("Input.Password")).SendKeys(password);
|
||||||
|
Browser.FindElement(By.Id("login-submit")).Click();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateLogout()
|
||||||
|
{
|
||||||
|
Browser.Exists(By.CssSelector("button.nav-link.btn.btn-link"));
|
||||||
|
|
||||||
|
// Click logout button
|
||||||
|
Browser.FindElement(By.CssSelector("button.nav-link.btn.btn-link")).Click();
|
||||||
|
|
||||||
|
Browser.Contains("/authentication/logged-out", () => Browser.Url);
|
||||||
|
Browser.True(() => Browser.FindElements(By.TagName("p")).Any(e => e.Text == "You are logged out."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateFetchData()
|
||||||
|
{
|
||||||
|
// Can navigate to the 'fetch data' page
|
||||||
|
Browser.Contains("fetchdata", () => Browser.Url);
|
||||||
|
Browser.Equal("Weather forecast", () => Browser.FindElement(By.TagName("h1")).Text);
|
||||||
|
|
||||||
|
// Asynchronously loads and displays the table of weather forecasts
|
||||||
|
Browser.Exists(By.CssSelector("table>tbody>tr"));
|
||||||
|
Browser.Equal(5, () => Browser.FindElements(By.CssSelector("p+table>tbody>tr")).Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FirstTimeRegister(string userName, string password)
|
||||||
|
{
|
||||||
|
Browser.FindElement(By.PartialLinkText("Register as a new user")).Click();
|
||||||
|
RegisterCore(userName, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterCore(string userName, string password)
|
||||||
|
{
|
||||||
|
Browser.Exists(By.Name("Input.Email"));
|
||||||
|
Browser.FindElement(By.Name("Input.Email")).SendKeys(userName);
|
||||||
|
Browser.FindElement(By.Name("Input.Password")).SendKeys(password);
|
||||||
|
Browser.FindElement(By.Name("Input.ConfirmPassword")).SendKeys(password);
|
||||||
|
Browser.FindElement(By.Id("registerSubmit")).Click();
|
||||||
|
|
||||||
|
// We will be redirected to the RegisterConfirmation
|
||||||
|
Browser.Contains("/Identity/Account/RegisterConfirmation", () => Browser.Url);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// For some reason the test sometimes get stuck here. Given that this is not something we are testing, to avoid
|
||||||
|
// this we'll retry once to minify the chances it happens on CI runs.
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Click here to confirm your account"), "/Identity/Account/ConfirmEmail");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Click here to confirm your account"), "/Identity/Account/ConfirmEmail");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we can login
|
||||||
|
Browser.FindElement(By.PartialLinkText("Login")).Click();
|
||||||
|
Browser.Exists(By.Name("Input.Email"));
|
||||||
|
Browser.FindElement(By.Name("Input.Email")).SendKeys(userName);
|
||||||
|
Browser.FindElement(By.Name("Input.Password")).SendKeys(password);
|
||||||
|
Browser.FindElement(By.Id("login-submit")).Click();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WaitUntilLoaded(bool skipHeader = false)
|
||||||
|
{
|
||||||
|
new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(
|
||||||
|
driver => driver.FindElement(By.TagName("app")).Text != "Loading...");
|
||||||
|
|
||||||
|
if (!skipHeader)
|
||||||
|
{
|
||||||
|
// All pages in the text contain an h1 element. This helps us wait until the router has intercepted links as that
|
||||||
|
// happens before rendering the underlying page.
|
||||||
|
Browser.Exists(By.TagName("h1"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection SetupTestDatabase<TContext>(IServiceCollection services, DbConnection connection) where TContext : DbContext
|
||||||
|
{
|
||||||
|
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<TContext>));
|
||||||
|
if (descriptor != null)
|
||||||
|
{
|
||||||
|
services.Remove(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
services.AddScoped(p =>
|
||||||
|
DbContextOptionsFactory<TContext>(
|
||||||
|
p,
|
||||||
|
(sp, options) => options
|
||||||
|
.ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning))
|
||||||
|
.UseSqlite(connection)));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DbContextOptions<TContext> DbContextOptionsFactory<TContext>(
|
||||||
|
IServiceProvider applicationServiceProvider,
|
||||||
|
Action<IServiceProvider, DbContextOptionsBuilder> optionsAction)
|
||||||
|
where TContext : DbContext
|
||||||
|
{
|
||||||
|
var builder = new DbContextOptionsBuilder<TContext>(
|
||||||
|
new DbContextOptions<TContext>(new Dictionary<Type, IDbContextOptionsExtension>()));
|
||||||
|
|
||||||
|
builder.UseApplicationServiceProvider(applicationServiceProvider);
|
||||||
|
|
||||||
|
optionsAction?.Invoke(applicationServiceProvider, builder);
|
||||||
|
|
||||||
|
return builder.Options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureDatabaseCreated(IServiceProvider services)
|
||||||
|
{
|
||||||
|
using var scope = services.CreateScope();
|
||||||
|
|
||||||
|
var applicationDbContext = scope.ServiceProvider.GetService<ApplicationDbContext>();
|
||||||
|
if (applicationDbContext?.Database?.GetPendingMigrations()?.Any() == true)
|
||||||
|
{
|
||||||
|
applicationDbContext?.Database?.Migrate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Make the tests run faster by navigating back to the home page when we are done
|
||||||
|
// If we don't, then the next test will reload the whole page before it starts
|
||||||
|
Browser.FindElement(By.LinkText("Home")).Click();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class JwtPayload
|
||||||
|
{
|
||||||
|
[JsonPropertyName("iss")]
|
||||||
|
public string Issuer { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("aud")]
|
||||||
|
public string Audience { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("client_id")]
|
||||||
|
public string ClientId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("sub")]
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("idp")]
|
||||||
|
public string IdentityProvider { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("scope")]
|
||||||
|
public string[] Scopes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<Project>
|
||||||
|
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\..\, Directory.Build.props))\Directory.Build.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<EnableTypeScriptNuGetTarget>true</EnableTypeScriptNuGetTarget>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -7,6 +7,10 @@
|
||||||
<StaticWebAssetBasePath>_content/TestContentPackage</StaticWebAssetBasePath>
|
<StaticWebAssetBasePath>_content/TestContentPackage</StaticWebAssetBasePath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<EnableTypeScriptNuGetTarget>true</EnableTypeScriptNuGetTarget>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Components" />
|
<Reference Include="Microsoft.AspNetCore.Components" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Components.Web" />
|
<Reference Include="Microsoft.AspNetCore.Components.Web" />
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
||||||
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
||||||
|
<DisableImplicitComponentsAnalyzers>true</DisableImplicitComponentsAnalyzers>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using OpenQA.Selenium;
|
using OpenQA.Selenium;
|
||||||
using OpenQA.Selenium.Support.UI;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
using Xunit.Sdk;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.E2ETesting
|
namespace Microsoft.AspNetCore.E2ETesting
|
||||||
{
|
{
|
||||||
|
|
@ -21,11 +16,6 @@ namespace Microsoft.AspNetCore.E2ETesting
|
||||||
private static readonly AsyncLocal<ILogs> _logs = new AsyncLocal<ILogs>();
|
private static readonly AsyncLocal<ILogs> _logs = new AsyncLocal<ILogs>();
|
||||||
private static readonly AsyncLocal<ITestOutputHelper> _output = new AsyncLocal<ITestOutputHelper>();
|
private static readonly AsyncLocal<ITestOutputHelper> _output = new AsyncLocal<ITestOutputHelper>();
|
||||||
|
|
||||||
// Limit the number of concurrent browser tests.
|
|
||||||
private readonly static int MaxConcurrentBrowsers = Environment.ProcessorCount * 2;
|
|
||||||
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(MaxConcurrentBrowsers);
|
|
||||||
private bool _semaphoreHeld;
|
|
||||||
|
|
||||||
public BrowserTestBase(BrowserFixture browserFixture, ITestOutputHelper output)
|
public BrowserTestBase(BrowserFixture browserFixture, ITestOutputHelper output)
|
||||||
{
|
{
|
||||||
BrowserFixture = browserFixture;
|
BrowserFixture = browserFixture;
|
||||||
|
|
@ -44,11 +34,6 @@ namespace Microsoft.AspNetCore.E2ETesting
|
||||||
|
|
||||||
public Task DisposeAsync()
|
public Task DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_semaphoreHeld)
|
|
||||||
{
|
|
||||||
_semaphore.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,9 +55,6 @@ namespace Microsoft.AspNetCore.E2ETesting
|
||||||
|
|
||||||
protected async Task InitializeBrowser(string isolationContext)
|
protected async Task InitializeBrowser(string isolationContext)
|
||||||
{
|
{
|
||||||
await _semaphore.WaitAsync(TimeSpan.FromMinutes(30));
|
|
||||||
_semaphoreHeld = true;
|
|
||||||
|
|
||||||
var (browser, logs) = await BrowserFixture.GetOrCreateBrowserAsync(Output, isolationContext);
|
var (browser, logs) = await BrowserFixture.GetOrCreateBrowserAsync(Output, isolationContext);
|
||||||
_asyncBrowser.Value = browser;
|
_asyncBrowser.Value = browser;
|
||||||
_logs.Value = logs;
|
_logs.Value = logs;
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.E2ETesting
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
assertion();
|
assertion();
|
||||||
throw new InvalidOperationException("The assertion succeded after the timeout.");
|
throw new InvalidOperationException("The assertion succeeded after the timeout.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue