[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:
Javier Calvarro Nelson 2020-02-17 14:36:57 -08:00 committed by GitHub
parent 4628dfb005
commit 0dbb01bd8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 8828 additions and 36 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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}

View File

@ -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;
}
}

View File

@ -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; }
}
}

View File

@ -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;

View File

@ -0,0 +1,2 @@
*.js
*.js.map

View File

@ -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"
}
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"lib": [ "DOM", "ES2019" ],
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}

View File

@ -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

View File

@ -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>

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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";
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -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;
}
}
}

View File

@ -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";
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}
}

View File

@ -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";
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>

View File

@ -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>

View File

@ -0,0 +1,7 @@
@page "/authentication/{action}"
<RemoteAuthenticatorView Action="@Action" />
@code{
[Parameter] public string Action { get; set; }
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,5 @@
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@ -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));
}
}

View File

@ -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();
}
}
}

View File

@ -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");
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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)}");
}
}

View File

@ -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>

View File

@ -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

View File

@ -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;
}
}

View File

@ -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>

View File

@ -0,0 +1 @@
app.db

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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
}
}
}

View File

@ -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");
}
}
}

View File

@ -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
}
}
}

View File

@ -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
{
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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>();
});
}
}

View File

@ -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");
});
}
}
}

View File

@ -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>

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -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": "*"
}

View File

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
</Project>

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}

View File

@ -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))

View File

@ -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 -->

View File

@ -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; }
}
}
}

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<EnableTypeScriptNuGetTarget>true</EnableTypeScriptNuGetTarget>
</PropertyGroup>
</Project>

View File

@ -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" />

View File

@ -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>

View File

@ -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;

View File

@ -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)
{