[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.Web" Version="$(MicrosoftAspNetCoreComponentsWebPackageVersion)" />
|
||||
<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)" />
|
||||
</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="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.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="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>
|
||||
|
|
|
|||
|
|
@ -206,6 +206,11 @@
|
|||
<SystemThreadingTasksExtensionsPackageVersion>4.5.2</SystemThreadingTasksExtensionsPackageVersion>
|
||||
<!-- Packages developed by @aspnet, but manually updated as necessary. -->
|
||||
<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>
|
||||
<!-- Partner teams -->
|
||||
<MicrosoftAzureKeyVaultPackageVersion>2.3.2</MicrosoftAzureKeyVaultPackageVersion>
|
||||
|
|
@ -273,6 +278,9 @@
|
|||
<MicrosoftAspNetCoreComponentsFormsPackageVersion>$(MicrosoftAspNetCoreComponentsPackageVersion)</MicrosoftAspNetCoreComponentsFormsPackageVersion>
|
||||
<MicrosoftAspNetCoreComponentsWebPackageVersion>$(MicrosoftAspNetCoreComponentsPackageVersion)</MicrosoftAspNetCoreComponentsWebPackageVersion>
|
||||
<MicrosoftAspNetCoreMvcPackageVersion>$(MicrosoftAspNetCoreComponentsPackageVersion)</MicrosoftAspNetCoreMvcPackageVersion>
|
||||
<MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion>$(MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion)</MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion>
|
||||
<MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>$(MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion)</MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>
|
||||
<MicrosoftAspNetCoreIdentityUIPackageVersion>$(MicrosoftAspNetCoreIdentityUIPackageVersion)</MicrosoftAspNetCoreIdentityUIPackageVersion>
|
||||
</PropertyGroup>
|
||||
<!-- Restore feeds -->
|
||||
<PropertyGroup Label="Restore feeds">
|
||||
|
|
|
|||
|
|
@ -87,6 +87,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestContentPackage", "test\
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.TestServer", "test\testassets\TestServer\Components.TestServer.csproj", "{EACC194A-8C1B-424D-B8FE-330E14CAF525}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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|x86.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -477,6 +549,12 @@ Global
|
|||
{DA17FB91-EF6F-4C34-A5D4-1A0F7E42D699} = {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}
|
||||
{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
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
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>
|
||||
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
||||
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
||||
<DisableImplicitComponentsAnalyzers>true</DisableImplicitComponentsAnalyzers>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<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 />
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableTypeScriptNuGetTarget>true</EnableTypeScriptNuGetTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<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);
|
||||
|
||||
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
|
||||
// It's an internal URL, so do client-side navigation
|
||||
performInternalNavigation(absoluteUri, false);
|
||||
performInternalNavigation(absoluteUri, false, replace);
|
||||
} else if (forceLoad && location.href === uri) {
|
||||
// Force-loading the same URL you're already on requires special handling to avoid
|
||||
// triggering browser-specific behavior issues.
|
||||
|
|
@ -85,13 +85,15 @@ export function navigateTo(uri: string, forceLoad: boolean) {
|
|||
const temporaryUri = uri + '?';
|
||||
history.replaceState(null, '', temporaryUri);
|
||||
location.replace(uri);
|
||||
} else if (replace){
|
||||
history.replaceState(null, '', absoluteUri)
|
||||
} else {
|
||||
// It's either an external URL, or forceLoad is requested, so do a full page load
|
||||
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
|
||||
// code path starting with a popstate event), we don't want to preserve the current scroll
|
||||
// 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.
|
||||
resetScrollAfterNextBatch();
|
||||
|
||||
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
|
||||
if(!replace){
|
||||
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
|
||||
}else{
|
||||
history.replaceState(null, /* ignored title */ '', absoluteInternalHref);
|
||||
}
|
||||
notifyLocationChanged(interceptedLink);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
|
|||
m => m.Value);
|
||||
}
|
||||
|
||||
protected static string FindSampleOrTestSitePath(string projectName)
|
||||
public static string FindSampleOrTestSitePath(string projectName)
|
||||
{
|
||||
var projects = _projects.Value;
|
||||
if (projects.TryGetValue(projectName, out var dir))
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
<ProjectReference Include="..\..\Blazor\DevServer\src\Microsoft.AspNetCore.Blazor.DevServer.csproj" />
|
||||
<ProjectReference Include="..\testassets\BasicTestApp\BasicTestApp.csproj" />
|
||||
<ProjectReference Include="..\testassets\TestServer\Components.TestServer.csproj" />
|
||||
<ProjectReference Include="..\..\Blazor\testassets\Wasm.Authentication.Server\Wasm.Authentication.Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 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>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableTypeScriptNuGetTarget>true</EnableTypeScriptNuGetTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Components" />
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Web" />
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
||||
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
||||
<DisableImplicitComponentsAnalyzers>true</DisableImplicitComponentsAnalyzers>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
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<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)
|
||||
{
|
||||
BrowserFixture = browserFixture;
|
||||
|
|
@ -44,11 +34,6 @@ namespace Microsoft.AspNetCore.E2ETesting
|
|||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
if (_semaphoreHeld)
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
|
@ -70,9 +55,6 @@ namespace Microsoft.AspNetCore.E2ETesting
|
|||
|
||||
protected async Task InitializeBrowser(string isolationContext)
|
||||
{
|
||||
await _semaphore.WaitAsync(TimeSpan.FromMinutes(30));
|
||||
_semaphoreHeld = true;
|
||||
|
||||
var (browser, logs) = await BrowserFixture.GetOrCreateBrowserAsync(Output, isolationContext);
|
||||
_asyncBrowser.Value = browser;
|
||||
_logs.Value = logs;
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.E2ETesting
|
|||
try
|
||||
{
|
||||
assertion();
|
||||
throw new InvalidOperationException("The assertion succeded after the timeout.");
|
||||
throw new InvalidOperationException("The assertion succeeded after the timeout.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue