[Blazor] More auth fixes (#20192)

* Introduces customization options for mapping user claims principals.
* Supports login/logout flows extensibility.
* Improves E2E test reliability
* Improves reliability on the AuthenticationService
* Improves the experience by trying to silently log-in users on startup.
* Avoids loading the Blazor application when within a hidden iframe.
This commit is contained in:
Javier Calvarro Nelson 2020-04-04 13:06:25 +02:00 committed by GitHub
parent e67e7a08ca
commit fd9c786165
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1392 additions and 226 deletions

View File

@ -37,7 +37,22 @@ namespace Microsoft.Extensions.DependencyInjection
public static IServiceCollection AddMsalAuthentication<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<MsalProviderOptions>> configure) public static IServiceCollection AddMsalAuthentication<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<MsalProviderOptions>> configure)
where TRemoteAuthenticationState : RemoteAuthenticationState, new() where TRemoteAuthenticationState : RemoteAuthenticationState, new()
{ {
services.AddRemoteAuthentication<RemoteAuthenticationState, MsalProviderOptions>(configure); AddMsalAuthentication<TRemoteAuthenticationState, RemoteUserAccount>(services, configure);
return services;
}
/// <summary>
/// Adds authentication using msal.js to Blazor applications.
/// </summary>
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="configure">The <see cref="Action{RemoteAuthenticationOptions{MsalProviderOptions}}"/> to configure the <see cref="RemoteAuthenticationOptions{MsalProviderOptions}"/>.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddMsalAuthentication<TRemoteAuthenticationState, TAccount>(this IServiceCollection services, Action<RemoteAuthenticationOptions<MsalProviderOptions>> configure)
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
where TAccount : RemoteUserAccount
{
services.AddRemoteAuthentication<RemoteAuthenticationState, TAccount, MsalProviderOptions>(configure);
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<MsalProviderOptions>>, MsalDefaultOptionsConfiguration>()); services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<MsalProviderOptions>>, MsalDefaultOptionsConfiguration>());
return services; return services;

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 IAccessTokenProviderAccessor
{
/// <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>
IAccessTokenProvider TokenProvider { get; }
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// An interface for configuring remote authentication services.
/// </summary>
/// <typeparam name="TRemoteAuthenticationState">The remote authentication state type.</typeparam>
/// <typeparam name="TAccount">The account type.</typeparam>
public interface IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>
where TRemoteAuthenticationState : RemoteAuthenticationState
where TAccount : RemoteUserAccount
{
IServiceCollection Services { get; }
}
}

View File

@ -48,27 +48,49 @@ export enum AuthenticationResultStatus {
export interface AuthenticationResult { export interface AuthenticationResult {
status: AuthenticationResultStatus; status: AuthenticationResultStatus;
state?: any; state?: unknown;
message?: string; message?: string;
} }
export interface AuthorizeService { export interface AuthorizeService {
getUser(): Promise<any>; getUser(): Promise<unknown>;
getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult>; getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
signIn(state: any): Promise<AuthenticationResult>; signIn(state: unknown): Promise<AuthenticationResult>;
completeSignIn(state: any): Promise<AuthenticationResult>; completeSignIn(state: unknown): Promise<AuthenticationResult>;
signOut(state: any): Promise<AuthenticationResult>; signOut(state: unknown): Promise<AuthenticationResult>;
completeSignOut(url: string): Promise<AuthenticationResult>; completeSignOut(url: string): Promise<AuthenticationResult>;
} }
class OidcAuthorizeService implements AuthorizeService { class OidcAuthorizeService implements AuthorizeService {
private _userManager: UserManager; private _userManager: UserManager;
private _intialSilentSignIn: Promise<void> | undefined;
constructor(userManager: UserManager) { constructor(userManager: UserManager) {
this._userManager = userManager; this._userManager = userManager;
} }
async trySilentSignIn() {
if (!this._intialSilentSignIn) {
this._intialSilentSignIn = (async () => {
try {
await this._userManager.signinSilent();
} catch (e) {
// It is ok to swallow the exception here.
// The user might not be logged in and in that case it
// is expected for signinSilent to fail and throw
}
})();
}
return this._intialSilentSignIn;
}
async getUser() { async getUser() {
if (window.parent === window && !window.opener && !window.frameElement && this._userManager.settings.redirect_uri &&
!location.href.startsWith(this._userManager.settings.redirect_uri)) {
// If we are not inside a hidden iframe, try authenticating silently.
await AuthenticationService.instance.trySilentSignIn();
}
const user = await this._userManager.getUser(); const user = await this._userManager.getUser();
return user && user.profile; return user && user.profile;
} }
@ -120,7 +142,7 @@ class OidcAuthorizeService implements AuthorizeService {
function hasAllScopes(request: AccessTokenRequestOptions | undefined, currentScopes: string[]) { function hasAllScopes(request: AccessTokenRequestOptions | undefined, currentScopes: string[]) {
const set = new Set(currentScopes); const set = new Set(currentScopes);
if (request && request.scopes) { if (request && request.scopes) {
for (let current of request.scopes) { for (const current of request.scopes) {
if (!set.has(current)) { if (!set.has(current)) {
return false; return false;
} }
@ -131,7 +153,7 @@ class OidcAuthorizeService implements AuthorizeService {
} }
} }
async signIn(state: any) { async signIn(state: unknown) {
try { try {
await this._userManager.clearStaleState(); await this._userManager.clearStaleState();
await this._userManager.signinSilent(this.createArguments()); await this._userManager.signinSilent(this.createArguments());
@ -166,7 +188,7 @@ class OidcAuthorizeService implements AuthorizeService {
} }
} }
async signOut(state: any) { async signOut(state: unknown) {
try { try {
if (!(await this._userManager.metadataService.getEndSessionEndpoint())) { if (!(await this._userManager.metadataService.getEndSessionEndpoint())) {
await this._userManager.removeUser(); await this._userManager.removeUser();
@ -212,8 +234,8 @@ class OidcAuthorizeService implements AuthorizeService {
private async stateExists(url: string) { private async stateExists(url: string) {
const stateParam = new URLSearchParams(new URL(url).search).get('state'); const stateParam = new URLSearchParams(new URL(url).search).get('state');
if (stateParam) { if (stateParam && this._userManager.settings.stateStore) {
return await this._userManager.settings.stateStore!.get(stateParam); return await this._userManager.settings.stateStore.get(stateParam);
} else { } else {
return undefined; return undefined;
} }
@ -221,15 +243,15 @@ class OidcAuthorizeService implements AuthorizeService {
private async loginRequired(url: string) { private async loginRequired(url: string) {
const errorParameter = new URLSearchParams(new URL(url).search).get('error'); const errorParameter = new URLSearchParams(new URL(url).search).get('error');
if (errorParameter) { if (errorParameter && this._userManager.settings.stateStore) {
const error = await this._userManager.settings.stateStore!.get(errorParameter); const error = await this._userManager.settings.stateStore.get(errorParameter);
return error === 'login_required'; return error === 'login_required';
} else { } else {
return false; return false;
} }
} }
private createArguments(state?: any) { private createArguments(state?: unknown) {
return { useReplaceToNavigate: true, data: state }; return { useReplaceToNavigate: true, data: state };
} }
@ -237,7 +259,7 @@ class OidcAuthorizeService implements AuthorizeService {
return { status: AuthenticationResultStatus.Failure, errorMessage: message }; return { status: AuthenticationResultStatus.Failure, errorMessage: message };
} }
private success(state: any) { private success(state: unknown) {
return { status: AuthenticationResultStatus.Success, state }; return { status: AuthenticationResultStatus.Success, state };
} }
@ -253,21 +275,57 @@ class OidcAuthorizeService implements AuthorizeService {
export class AuthenticationService { export class AuthenticationService {
static _infrastructureKey = 'Microsoft.AspNetCore.Components.WebAssembly.Authentication'; static _infrastructureKey = 'Microsoft.AspNetCore.Components.WebAssembly.Authentication';
static _initialized : Promise<void>; static _initialized: Promise<void>;
static instance: OidcAuthorizeService; static instance: OidcAuthorizeService;
static _pendingOperations: { [key: string]: Promise<AuthenticationResult> | undefined } = {}
public static async init(settings: UserManagerSettings & AuthorizeServiceSettings) { public static init(settings: UserManagerSettings & AuthorizeServiceSettings) {
// Multiple initializations can start concurrently and we want to avoid that. // Multiple initializations can start concurrently and we want to avoid that.
// In order to do so, we create an initialization promise and the first call to init // In order to do so, we create an initialization promise and the first call to init
// tries to initialize the app and sets up a promise other calls can await on. // tries to initialize the app and sets up a promise other calls can await on.
if (!AuthenticationService._initialized) { if (!AuthenticationService._initialized) {
this._initialized = (async () => { AuthenticationService._initialized = AuthenticationService.initializeCore(settings);
const userManager = await this.createUserManager(settings);
AuthenticationService.instance = new OidcAuthorizeService(userManager);
})();
} }
await this._initialized; return AuthenticationService._initialized;
}
public static handleCallback() {
return AuthenticationService.initializeCore();
}
private static async initializeCore(settings?: UserManagerSettings & AuthorizeServiceSettings) {
const finalSettings = settings || AuthenticationService.resolveCachedSettings();
if (!settings && finalSettings) {
const userManager = AuthenticationService.createUserManagerCore(finalSettings);
if (window.parent !== window && !window.opener && (window.frameElement && userManager.settings.redirect_uri &&
location.href.startsWith(userManager.settings.redirect_uri))) {
// If we are inside a hidden iframe, try completing the sign in early.
// This prevents loading the blazor app inside a hidden iframe, which speeds up the authentication operations
// and avoids wasting resources (CPU and memory from bootstrapping the Blazor app)
AuthenticationService.instance = new OidcAuthorizeService(userManager);
// This makes sure that if the blazor app has time to load inside the hidden iframe,
// it is not able to perform another auth operation until this operation has completed.
AuthenticationService._initialized = (async (): Promise<void> => {
await AuthenticationService.instance.completeSignIn(location.href);
return;
})();
}
} else if (settings) {
const userManager = await AuthenticationService.createUserManager(settings);
AuthenticationService.instance = new OidcAuthorizeService(userManager);
} else {
// HandleCallback gets called unconditionally, so we do nothing for normal paths.
// Cached settings are only used on handling the redirect_uri path and if the settings are not there
// the app will fallback to the default logic for handling the redirect.
}
}
private static resolveCachedSettings(): UserManagerSettings | undefined {
const cachedSettings = window.sessionStorage.getItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`);
return cachedSettings ? JSON.parse(cachedSettings) : undefined;
} }
public static getUser() { public static getUser() {
@ -278,37 +336,46 @@ export class AuthenticationService {
return AuthenticationService.instance.getAccessToken(); return AuthenticationService.instance.getAccessToken();
} }
public static signIn(state: any) { public static signIn(state: unknown) {
return AuthenticationService.instance.signIn(state); return AuthenticationService.instance.signIn(state);
} }
public static completeSignIn(url: string) { public static async completeSignIn(url: string) {
return AuthenticationService.instance.completeSignIn(url); let operation = this._pendingOperations[url];
if (!operation) {
operation = AuthenticationService.instance.completeSignIn(url);
await operation;
delete this._pendingOperations[url];
}
return operation;
} }
public static signOut(state: any) { public static signOut(state: unknown) {
return AuthenticationService.instance.signOut(state); return AuthenticationService.instance.signOut(state);
} }
public static completeSignOut(url: string) { public static async completeSignOut(url: string) {
return AuthenticationService.instance.completeSignOut(url); let operation = this._pendingOperations[url];
if (!operation) {
operation = AuthenticationService.instance.completeSignOut(url);
await operation;
delete this._pendingOperations[url];
}
return operation;
} }
private static async createUserManager(settings: OidcAuthorizeServiceSettings): Promise<UserManager> { private static async createUserManager(settings: OidcAuthorizeServiceSettings): Promise<UserManager> {
let finalSettings: UserManagerSettings; let finalSettings: UserManagerSettings;
if (isApiAuthorizationSettings(settings)) { if (isApiAuthorizationSettings(settings)) {
let response = await fetch(settings.configurationEndpoint); const response = await fetch(settings.configurationEndpoint);
if (!response.ok) { if (!response.ok) {
throw new Error(`Could not load settings from '${settings.configurationEndpoint}'`); throw new Error(`Could not load settings from '${settings.configurationEndpoint}'`);
} }
const downloadedSettings = await response.json(); const downloadedSettings = await response.json();
window.sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`, JSON.stringify(settings));
downloadedSettings.automaticSilentRenew = true;
downloadedSettings.includeIdTokenInSilentRenew = true;
finalSettings = downloadedSettings; finalSettings = downloadedSettings;
} else { } else {
if (!settings.scope) { if (!settings.scope) {
@ -323,18 +390,24 @@ export class AuthenticationService {
finalSettings = settings; finalSettings = settings;
} }
window.sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`, JSON.stringify(finalSettings));
return AuthenticationService.createUserManagerCore(finalSettings);
}
private static createUserManagerCore(finalSettings: UserManagerSettings) {
const userManager = new UserManager(finalSettings); const userManager = new UserManager(finalSettings);
userManager.events.addUserSignedOut(async () => { userManager.events.addUserSignedOut(async () => {
await userManager.removeUser(); userManager.removeUser();
}); });
return userManager; return userManager;
} }
} }
declare global { declare global {
interface Window { AuthenticationService: AuthenticationService; } interface Window { AuthenticationService: AuthenticationService }
} }
AuthenticationService.handleCallback();
window.AuthenticationService = AuthenticationService; window.AuthenticationService = AuthenticationService;

View File

@ -4,7 +4,7 @@
"build": "npm run build:release", "build": "npm run build:release",
"build:release": "webpack --mode production --env.production --env.configuration=Release", "build:release": "webpack --mode production --env.production --env.configuration=Release",
"build:debug": "webpack --mode development --env.configuration=Debug", "build:debug": "webpack --mode development --env.configuration=Debug",
"watch": "webpack --watch --mode development" "watch": "webpack --watch --mode development --env.configuration=Debug"
}, },
"devDependencies": { "devDependencies": {
"ts-loader": "^6.2.1", "ts-loader": "^6.2.1",

View File

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
{
/// <summary>
/// A user account.
/// </summary>
/// <remarks>
/// The information in this type will be use to produce a <see cref="System.Security.Claims.ClaimsPrincipal"/> for the application.
/// </remarks>
public class RemoteUserAccount
{
/// <summary>
/// Gets or sets properties not explicitly mapped about the user.
/// </summary>
[JsonExtensionData]
public IDictionary<string, object> AdditionalProperties { get; set; }
}
}

View File

@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
options.AuthenticationPaths.RemoteRegisterPath ??= "Identity/Account/Register"; options.AuthenticationPaths.RemoteRegisterPath ??= "Identity/Account/Register";
options.AuthenticationPaths.RemoteProfilePath ??= "Identity/Account/Manage"; options.AuthenticationPaths.RemoteProfilePath ??= "Identity/Account/Manage";
options.UserOptions.ScopeClaim ??= "scope"; options.UserOptions.ScopeClaim ??= "scope";
options.UserOptions.RoleClaim ??= "role";
options.UserOptions.AuthenticationType ??= _applicationName; options.UserOptions.AuthenticationType ??= _applicationName;
} }

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.
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
{
internal class RemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>
: IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>
where TRemoteAuthenticationState : RemoteAuthenticationState
where TAccount : RemoteUserAccount
{
public RemoteAuthenticationBuilder(IServiceCollection services) => Services = services;
public IServiceCollection Services { get; }
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extensions for remote authentication services.
/// </summary>
public static class RemoteAuthenticationBuilderExtensions
{
/// <summary>
/// Replaces the existing <see cref="UserFactory{TAccount}"/> with the user factory defined by <typeparamref name="TUserFactory"/>.
/// </summary>
/// <typeparam name="TRemoteAuthenticationState">The remote authentication state.</typeparam>
/// <typeparam name="TAccount">The account type.</typeparam>
/// <typeparam name="TUserFactory">The new user factory type.</typeparam>
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, TAccount}"/>.</param>
/// <returns>The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, TAccount}"/>.</returns>
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddUserFactory<TRemoteAuthenticationState, TAccount, TUserFactory>(
this IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> builder)
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
where TAccount : RemoteUserAccount
where TUserFactory : UserFactory<TAccount>
{
builder.Services.Replace(ServiceDescriptor.Scoped<UserFactory<TAccount>, TUserFactory>());
return builder;
}
/// <summary>
/// Replaces the existing <see cref="UserFactory{Account}"/> with the user factory defined by <typeparamref name="TUserFactory"/>.
/// </summary>
/// <typeparam name="TRemoteAuthenticationState">The remote authentication state.</typeparam>
/// <typeparam name="TUserFactory">The new user factory type.</typeparam>
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, Account}"/>.</param>
/// <returns>The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, Account}"/>.</returns>
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddUserFactory<TRemoteAuthenticationState, TUserFactory>(
this IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> builder)
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
where TUserFactory : UserFactory<RemoteUserAccount> => builder.AddUserFactory<TRemoteAuthenticationState, RemoteUserAccount, TUserFactory>();
/// <summary>
/// Replaces the existing <see cref="UserFactory{TAccount}"/> with the user factory defined by <typeparamref name="TUserFactory"/>.
/// </summary>
/// <typeparam name="TUserFactory">The new user factory type.</typeparam>
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{RemoteAuthenticationState, Account}"/>.</param>
/// <returns>The <see cref="IRemoteAuthenticationBuilder{RemoteAuthenticationState, Account}"/>.</returns>
public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddUserFactory<TUserFactory>(
this IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> builder)
where TUserFactory : UserFactory<RemoteUserAccount> => builder.AddUserFactory<RemoteAuthenticationState, RemoteUserAccount, TUserFactory>();
}
}

View File

@ -225,12 +225,14 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
{ {
State = AuthenticationState State = AuthenticationState
}); });
switch (result.Status) switch (result.Status)
{ {
case RemoteAuthenticationStatus.Redirect: case RemoteAuthenticationStatus.Redirect:
break; break;
case RemoteAuthenticationStatus.Success: case RemoteAuthenticationStatus.Success:
await NavigateToReturnUrl(returnUrl); await OnLogInSucceeded.InvokeAsync(result.State);
await NavigateToReturnUrl(GetReturnUrl(result.State, returnUrl));
break; break;
case RemoteAuthenticationStatus.Failure: case RemoteAuthenticationStatus.Failure:
var uri = Navigation.ToAbsoluteUri($"{ApplicationPaths.LogInFailedPath}?message={Uri.EscapeDataString(result.ErrorMessage)}").ToString(); var uri = Navigation.ToAbsoluteUri($"{ApplicationPaths.LogInFailedPath}?message={Uri.EscapeDataString(result.ErrorMessage)}").ToString();
@ -253,10 +255,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// is when we are doing a redirect sign in flow. // is when we are doing a redirect sign in flow.
throw new InvalidOperationException("Should not redirect."); throw new InvalidOperationException("Should not redirect.");
case RemoteAuthenticationStatus.Success: case RemoteAuthenticationStatus.Success:
if (OnLogInSucceeded.HasDelegate) await OnLogInSucceeded.InvokeAsync(result.State);
{
await OnLogInSucceeded.InvokeAsync(result.State);
}
await NavigateToReturnUrl(GetReturnUrl(result.State)); await NavigateToReturnUrl(GetReturnUrl(result.State));
break; break;
case RemoteAuthenticationStatus.OperationCompleted: case RemoteAuthenticationStatus.OperationCompleted:
@ -292,6 +291,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
case RemoteAuthenticationStatus.Redirect: case RemoteAuthenticationStatus.Redirect:
break; break;
case RemoteAuthenticationStatus.Success: case RemoteAuthenticationStatus.Success:
await OnLogOutSucceeded.InvokeAsync(result.State);
await NavigateToReturnUrl(returnUrl); await NavigateToReturnUrl(returnUrl);
break; break;
case RemoteAuthenticationStatus.OperationCompleted: case RemoteAuthenticationStatus.OperationCompleted:
@ -320,10 +320,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// is when we are doing a redirect sign in flow. // is when we are doing a redirect sign in flow.
throw new InvalidOperationException("Should not redirect."); throw new InvalidOperationException("Should not redirect.");
case RemoteAuthenticationStatus.Success: case RemoteAuthenticationStatus.Success:
if (OnLogOutSucceeded.HasDelegate) await OnLogOutSucceeded.InvokeAsync(result.State);
{
await OnLogOutSucceeded.InvokeAsync(result.State);
}
await NavigateToReturnUrl(GetReturnUrl(result.State, Navigation.ToAbsoluteUri(ApplicationPaths.LogOutSucceededPath).ToString())); await NavigateToReturnUrl(GetReturnUrl(result.State, Navigation.ToAbsoluteUri(ApplicationPaths.LogOutSucceededPath).ToString()));
break; break;
case RemoteAuthenticationStatus.OperationCompleted: case RemoteAuthenticationStatus.OperationCompleted:

View File

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal
{
internal class AccessTokenProviderAccessor : IAccessTokenProviderAccessor
{
private readonly IServiceProvider _provider;
private IAccessTokenProvider _tokenProvider;
public AccessTokenProviderAccessor(IServiceProvider provider) => _provider = provider;
public IAccessTokenProvider TokenProvider => _tokenProvider ??= _provider.GetRequiredService<IAccessTokenProvider>();
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -17,15 +16,15 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
/// </summary> /// </summary>
/// <typeparam name="TRemoteAuthenticationState">The state to preserve across authentication operations.</typeparam> /// <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> /// <typeparam name="TProviderOptions">The options to be passed down to the underlying JavaScript library handling the authentication operations.</typeparam>
public class RemoteAuthenticationService<TRemoteAuthenticationState, TProviderOptions> : public class RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions> :
AuthenticationStateProvider, AuthenticationStateProvider,
IRemoteAuthenticationService<TRemoteAuthenticationState>, IRemoteAuthenticationService<TRemoteAuthenticationState>,
IAccessTokenProvider IAccessTokenProvider
where TRemoteAuthenticationState : RemoteAuthenticationState where TRemoteAuthenticationState : RemoteAuthenticationState
where TProviderOptions : new() where TProviderOptions : new()
where TAccount : RemoteUserAccount
{ {
private static readonly TimeSpan _userCacheRefreshInterval = TimeSpan.FromSeconds(60); private static readonly TimeSpan _userCacheRefreshInterval = TimeSpan.FromSeconds(60);
private bool _initialized = false; private bool _initialized = false;
// This defaults to 1/1/1970 // This defaults to 1/1/1970
@ -33,33 +32,42 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity()); private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity());
/// <summary> /// <summary>
/// The <see cref="IJSRuntime"/> to use for performing JavaScript interop operations. /// Gets the <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.
/// </summary> /// </summary>
protected readonly IJSRuntime _jsRuntime; protected IJSRuntime JsRuntime { get; }
/// <summary> /// <summary>
/// The <see cref="NavigationManager"/> used to compute absolute urls. /// Gets the <see cref="NavigationManager"/> used to compute absolute urls.
/// </summary> /// </summary>
protected readonly NavigationManager _navigation; protected NavigationManager Navigation { get; }
/// <summary> /// <summary>
/// The options for the underlying JavaScript library handling the authentication operations. /// Gets the <see cref="UserFactory{TAccount}"/> to map accounts to <see cref="ClaimsPrincipal"/>.
/// </summary> /// </summary>
protected readonly RemoteAuthenticationOptions<TProviderOptions> _options; protected UserFactory<TAccount> UserFactory { get; }
/// <summary>
/// Gets the options for the underlying JavaScript library handling the authentication operations.
/// </summary>
protected RemoteAuthenticationOptions<TProviderOptions> Options { get; }
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="RemoteAuthenticationService{TRemoteAuthenticationState, TProviderOptions}"/>. /// Initializes a new instance of <see cref="RemoteAuthenticationService{TRemoteAuthenticationState, TProviderOptions}"/>.
/// </summary> /// </summary>
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.</param> /// <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> /// <param name="options">The options to be passed down to the underlying JavaScript library handling the authentication operations.</param>
/// <param name="navigation">The <see cref="NavigationManager"/> used to generate URLs.</param>
/// <param name="userFactory">The <see cref="UserFactory{TAccount}"/> used to generate the <see cref="ClaimsPrincipal"/> for the user.</param>
public RemoteAuthenticationService( public RemoteAuthenticationService(
IJSRuntime jsRuntime, IJSRuntime jsRuntime,
IOptions<RemoteAuthenticationOptions<TProviderOptions>> options, IOptions<RemoteAuthenticationOptions<TProviderOptions>> options,
NavigationManager navigation) NavigationManager navigation,
UserFactory<TAccount> userFactory)
{ {
_jsRuntime = jsRuntime; JsRuntime = jsRuntime;
_navigation = navigation; Navigation = navigation;
_options = options.Value; UserFactory = userFactory;
Options = options.Value;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -70,11 +78,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
RemoteAuthenticationContext<TRemoteAuthenticationState> context) RemoteAuthenticationContext<TRemoteAuthenticationState> context)
{ {
await EnsureAuthService(); await EnsureAuthService();
var internalResult = await _jsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signIn", context.State); var internalResult = await JsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signIn", context.State);
var result = internalResult.Convert(); var result = internalResult.Convert();
if (result.Status == RemoteAuthenticationStatus.Success) if (result.Status == RemoteAuthenticationStatus.Success)
{ {
UpdateUser(GetUser()); var getUserTask = GetUser();
await getUserTask;
UpdateUser(getUserTask);
} }
return result; return result;
@ -85,11 +95,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
RemoteAuthenticationContext<TRemoteAuthenticationState> context) RemoteAuthenticationContext<TRemoteAuthenticationState> context)
{ {
await EnsureAuthService(); await EnsureAuthService();
var internalResult = await _jsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignIn", context.Url); var internalResult = await JsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignIn", context.Url);
var result = internalResult.Convert(); var result = internalResult.Convert();
if (result.Status == RemoteAuthenticationStatus.Success) if (result.Status == RemoteAuthenticationStatus.Success)
{ {
UpdateUser(GetUser()); var getUserTask = GetUser();
await getUserTask;
UpdateUser(getUserTask);
} }
return result; return result;
@ -100,11 +112,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
RemoteAuthenticationContext<TRemoteAuthenticationState> context) RemoteAuthenticationContext<TRemoteAuthenticationState> context)
{ {
await EnsureAuthService(); await EnsureAuthService();
var internalResult = await _jsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signOut", context.State); var internalResult = await JsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signOut", context.State);
var result = internalResult.Convert(); var result = internalResult.Convert();
if (result.Status == RemoteAuthenticationStatus.Success) if (result.Status == RemoteAuthenticationStatus.Success)
{ {
UpdateUser(GetUser()); var getUserTask = GetUser();
await getUserTask;
UpdateUser(getUserTask);
} }
return result; return result;
@ -115,11 +129,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
RemoteAuthenticationContext<TRemoteAuthenticationState> context) RemoteAuthenticationContext<TRemoteAuthenticationState> context)
{ {
await EnsureAuthService(); await EnsureAuthService();
var internalResult = await _jsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignOut", context.Url); var internalResult = await JsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignOut", context.Url);
var result = internalResult.Convert(); var result = internalResult.Convert();
if (result.Status == RemoteAuthenticationStatus.Success) if (result.Status == RemoteAuthenticationStatus.Success)
{ {
UpdateUser(GetUser()); var getUserTask = GetUser();
await getUserTask;
UpdateUser(getUserTask);
} }
return result; return result;
@ -129,7 +145,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
public virtual async ValueTask<AccessTokenResult> RequestAccessToken() public virtual async ValueTask<AccessTokenResult> RequestAccessToken()
{ {
await EnsureAuthService(); await EnsureAuthService();
var result = await _jsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken"); var result = await JsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken");
if (!Enum.TryParse<AccessTokenResultStatus>(result.Status, ignoreCase: true, out var parsedStatus)) if (!Enum.TryParse<AccessTokenResultStatus>(result.Status, ignoreCase: true, out var parsedStatus))
{ {
@ -154,7 +170,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
} }
await EnsureAuthService(); await EnsureAuthService();
var result = await _jsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken", options); var result = await JsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken", options);
if (!Enum.TryParse<AccessTokenResultStatus>(result.Status, ignoreCase: true, out var parsedStatus)) if (!Enum.TryParse<AccessTokenResultStatus>(result.Status, ignoreCase: true, out var parsedStatus))
{ {
@ -172,13 +188,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
private Uri GetRedirectUrl(string customReturnUrl) private Uri GetRedirectUrl(string customReturnUrl)
{ {
var returnUrl = customReturnUrl != null ? _navigation.ToAbsoluteUri(customReturnUrl).ToString() : null; var returnUrl = customReturnUrl != null ? Navigation.ToAbsoluteUri(customReturnUrl).ToString() : null;
var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? _navigation.Uri); var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? Navigation.Uri);
var redirectUrl = _navigation.ToAbsoluteUri($"{_options.AuthenticationPaths.LogInPath}?returnUrl={encodedReturnUrl}"); var redirectUrl = Navigation.ToAbsoluteUri($"{Options.AuthenticationPaths.LogInPath}?returnUrl={encodedReturnUrl}");
return redirectUrl; return redirectUrl;
} }
private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false) private async Task<ClaimsPrincipal> GetUser(bool useCache = false)
{ {
var now = DateTimeOffset.Now; var now = DateTimeOffset.Now;
if (useCache && now < _userLastCheck + _userCacheRefreshInterval) if (useCache && now < _userLastCheck + _userCacheRefreshInterval)
@ -196,56 +212,29 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
/// Gets the current authenticated used using JavaScript interop. /// Gets the current authenticated used using JavaScript interop.
/// </summary> /// </summary>
/// <returns>A <see cref="Task{ClaimsPrincipal}"/>that will return the current authenticated user when completes.</returns> /// <returns>A <see cref="Task{ClaimsPrincipal}"/>that will return the current authenticated user when completes.</returns>
protected internal virtual async Task<ClaimsPrincipal> GetAuthenticatedUser() protected internal virtual async ValueTask<ClaimsPrincipal> GetAuthenticatedUser()
{ {
await EnsureAuthService(); await EnsureAuthService();
var user = await _jsRuntime.InvokeAsync<IDictionary<string, object>>("AuthenticationService.getUser"); var account = await JsRuntime.InvokeAsync<TAccount>("AuthenticationService.getUser");
var user = await UserFactory.CreateUserAsync(account, Options.UserOptions);
var identity = user != null ? new ClaimsIdentity( return user;
_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() private async ValueTask EnsureAuthService()
{ {
if (!_initialized) if (!_initialized)
{ {
await _jsRuntime.InvokeVoidAsync("AuthenticationService.init", _options.ProviderOptions); await JsRuntime.InvokeVoidAsync("AuthenticationService.init", Options.ProviderOptions);
_initialized = true; _initialized = true;
} }
} }
private void UpdateUser(ValueTask<ClaimsPrincipal> task) private void UpdateUser(Task<ClaimsPrincipal> task)
{ {
NotifyAuthenticationStateChanged(UpdateAuthenticationState(task)); NotifyAuthenticationStateChanged(UpdateAuthenticationState(task));
static async Task<AuthenticationState> UpdateAuthenticationState(ValueTask<ClaimsPrincipal> futureUser) => new AuthenticationState(await futureUser); static async Task<AuthenticationState> UpdateAuthenticationState(Task<ClaimsPrincipal> futureUser) => new AuthenticationState(await futureUser);
} }
} }

View File

@ -0,0 +1,59 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
{
/// <summary>
/// Converts a <see cref="TAccount"/> into a <see cref="ClaimsPrincipal"/>.
/// </summary>
/// <typeparam name="TAccount">The account type.</typeparam>
public class UserFactory<TAccount> where TAccount : RemoteUserAccount
{
private readonly IAccessTokenProviderAccessor _accessor;
#pragma warning disable PUB0001 // Pubternal type in public API
public UserFactory(IAccessTokenProviderAccessor accessor) => _accessor = accessor;
/// <summary>
/// Gets or sets the <see cref="IAccessTokenProvider"/>.
/// </summary>
public IAccessTokenProvider TokenProvider => _accessor.TokenProvider;
/// <summary>
/// Converts the <paramref name="account"/> into the final <see cref="ClaimsPrincipal"/>.
/// </summary>
/// <param name="account">The <see cref="TAccount"/>.</param>
/// <param name="options">The <see cref="RemoteAuthenticationUserOptions"/> to configure the <see cref="ClaimsPrincipal"/> with.</param>
/// <returns>A <see cref="ValueTask{TResult}"/>that will contain the <see cref="ClaimsPrincipal"/> user when completed.</returns>
public virtual ValueTask<ClaimsPrincipal> CreateUserAsync(
TAccount account,
RemoteAuthenticationUserOptions options)
{
var identity = account != null ? new ClaimsIdentity(
options.AuthenticationType,
options.NameClaim,
options.RoleClaim) : new ClaimsIdentity();
if (account != null)
{
foreach (var kvp in account.AdditionalProperties)
{
var name = kvp.Key;
var value = kvp.Value;
if (value != null ||
(value is JsonElement element && element.ValueKind != JsonValueKind.Undefined && element.ValueKind != JsonValueKind.Null))
{
identity.AddClaim(new Claim(name, value.ToString()));
}
}
}
return new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(identity));
}
}
}

View File

@ -21,31 +21,35 @@ namespace Microsoft.Extensions.DependencyInjection
/// <typeparamref name="TRemoteAuthenticationState"/>. /// <typeparamref name="TRemoteAuthenticationState"/>.
/// </summary> /// </summary>
/// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam> /// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam>
/// <typeparam name="TAccount">The account type.</typeparam>
/// <typeparam name="TProviderOptions">The configuration options of the underlying provider being used for handling the 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="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns> /// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
public static IServiceCollection AddRemoteAuthentication<TRemoteAuthenticationState, TProviderOptions>(this IServiceCollection services) public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, TProviderOptions>(this IServiceCollection services)
where TRemoteAuthenticationState : RemoteAuthenticationState where TRemoteAuthenticationState : RemoteAuthenticationState
where TAccount : RemoteUserAccount
where TProviderOptions : class, new() where TProviderOptions : class, new()
{ {
services.AddOptions(); services.AddOptions();
services.AddAuthorizationCore(); services.AddAuthorizationCore();
services.TryAddSingleton<AuthenticationStateProvider, RemoteAuthenticationService<TRemoteAuthenticationState, TProviderOptions>>(); services.TryAddScoped<AuthenticationStateProvider, RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions>>();
services.TryAddSingleton(sp => services.TryAddScoped(sp =>
{ {
return (IRemoteAuthenticationService<TRemoteAuthenticationState>)sp.GetRequiredService<AuthenticationStateProvider>(); return (IRemoteAuthenticationService<TRemoteAuthenticationState>)sp.GetRequiredService<AuthenticationStateProvider>();
}); });
services.TryAddSingleton(sp => services.TryAddScoped(sp =>
{ {
return (IAccessTokenProvider)sp.GetRequiredService<AuthenticationStateProvider>(); return (IAccessTokenProvider)sp.GetRequiredService<AuthenticationStateProvider>();
}); });
services.TryAddSingleton<IRemoteAuthenticationPathsProvider, DefaultRemoteApplicationPathsProvider<TProviderOptions>>(); services.TryAddScoped<IRemoteAuthenticationPathsProvider, DefaultRemoteApplicationPathsProvider<TProviderOptions>>();
services.TryAddScoped<IAccessTokenProviderAccessor, AccessTokenProviderAccessor>();
services.TryAddScoped<SignOutSessionStateManager>();
services.TryAddSingleton<SignOutSessionStateManager>(); services.TryAddScoped<UserFactory<TAccount>>();
return services; return new RemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>(services);
} }
/// <summary> /// <summary>
@ -53,21 +57,23 @@ namespace Microsoft.Extensions.DependencyInjection
/// <typeparamref name="TRemoteAuthenticationState"/>. /// <typeparamref name="TRemoteAuthenticationState"/>.
/// </summary> /// </summary>
/// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam> /// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam>
/// <typeparam name="TAccount">The account type.</typeparam>
/// <typeparam name="TProviderOptions">The configuration options of the underlying provider being used for handling the 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="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> /// <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> /// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
public static IServiceCollection AddRemoteAuthentication<TRemoteAuthenticationState, TProviderOptions>(this IServiceCollection services, Action<RemoteAuthenticationOptions<TProviderOptions>> configure) public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, TProviderOptions>(this IServiceCollection services, Action<RemoteAuthenticationOptions<TProviderOptions>> configure)
where TRemoteAuthenticationState : RemoteAuthenticationState where TRemoteAuthenticationState : RemoteAuthenticationState
where TAccount : RemoteUserAccount
where TProviderOptions : class, new() where TProviderOptions : class, new()
{ {
services.AddRemoteAuthentication<RemoteAuthenticationState, TProviderOptions>(); services.AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, TProviderOptions>();
if (configure != null) if (configure != null)
{ {
services.Configure(configure); services.Configure(configure);
} }
return services; return new RemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>(services);
} }
/// <summary> /// <summary>
@ -76,7 +82,7 @@ namespace Microsoft.Extensions.DependencyInjection
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <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> /// <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> /// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
public static IServiceCollection AddOidcAuthentication(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure) public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddOidcAuthentication(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure)
{ {
return AddOidcAuthentication<RemoteAuthenticationState>(services, configure); return AddOidcAuthentication<RemoteAuthenticationState>(services, configure);
} }
@ -88,23 +94,37 @@ namespace Microsoft.Extensions.DependencyInjection
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <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> /// <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> /// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
public static IServiceCollection AddOidcAuthentication<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure) public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddOidcAuthentication<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure)
where TRemoteAuthenticationState : RemoteAuthenticationState, new() where TRemoteAuthenticationState : RemoteAuthenticationState, new()
{
return AddOidcAuthentication<TRemoteAuthenticationState, RemoteUserAccount>(services, configure);
}
/// <summary>
/// Adds support for authentication for SPA applications using <see cref="OidcProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
/// </summary>
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
/// <typeparam name="TAccount">The account type.</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 IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddOidcAuthentication<TRemoteAuthenticationState, TAccount>(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure)
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
where TAccount : RemoteUserAccount
{ {
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<OidcProviderOptions>>, DefaultOidcOptionsConfiguration>()); services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<OidcProviderOptions>>, DefaultOidcOptionsConfiguration>());
return AddRemoteAuthentication<TRemoteAuthenticationState, OidcProviderOptions>(services, configure); return AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, OidcProviderOptions>(services, configure);
} }
/// <summary> /// <summary>
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>. /// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
/// </summary> /// </summary>
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns> /// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
public static IServiceCollection AddApiAuthorization(this IServiceCollection services) public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddApiAuthorization(this IServiceCollection services)
{ {
return AddApiauthorizationCore<RemoteAuthenticationState>(services, configure: null, Assembly.GetCallingAssembly().GetName().Name); return AddApiauthorizationCore<RemoteAuthenticationState, RemoteUserAccount>(services, configure: null, Assembly.GetCallingAssembly().GetName().Name);
} }
/// <summary> /// <summary>
@ -113,10 +133,35 @@ namespace Microsoft.Extensions.DependencyInjection
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam> /// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns> /// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
public static IServiceCollection AddApiAuthorization<TRemoteAuthenticationState>(this IServiceCollection services) public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddApiAuthorization<TRemoteAuthenticationState>(this IServiceCollection services)
where TRemoteAuthenticationState : RemoteAuthenticationState, new() where TRemoteAuthenticationState : RemoteAuthenticationState, new()
{ {
return AddApiauthorizationCore<TRemoteAuthenticationState>(services, configure: null, Assembly.GetCallingAssembly().GetName().Name); return AddApiauthorizationCore<TRemoteAuthenticationState, RemoteUserAccount>(services, configure: null, Assembly.GetCallingAssembly().GetName().Name);
}
/// <summary>
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
/// </summary>
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
/// <typeparam name="TAccount">The account type.</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 IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddApiAuthorization<TRemoteAuthenticationState, TAccount>(this IServiceCollection services)
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
where TAccount : RemoteUserAccount
{
return AddApiauthorizationCore<TRemoteAuthenticationState, TAccount>(services, configure: null, Assembly.GetCallingAssembly().GetName().Name);
}
/// <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 IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddApiAuthorization(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure)
{
return AddApiauthorizationCore<RemoteAuthenticationState, RemoteUserAccount>(services, configure, Assembly.GetCallingAssembly().GetName().Name);
} }
/// <summary> /// <summary>
@ -126,37 +171,41 @@ namespace Microsoft.Extensions.DependencyInjection
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <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> /// <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> /// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
public static IServiceCollection AddApiAuthorization(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure) public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddApiAuthorization<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure)
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
{ {
return AddApiauthorizationCore<RemoteAuthenticationState>(services, configure, Assembly.GetCallingAssembly().GetName().Name); return AddApiauthorizationCore<TRemoteAuthenticationState, RemoteUserAccount>(services, configure, Assembly.GetCallingAssembly().GetName().Name);
} }
/// <summary> /// <summary>
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>. /// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
/// </summary> /// </summary>
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam> /// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
/// <typeparam name="TAccount">The account type.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <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> /// <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> /// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
public static IServiceCollection AddApiAuthorization<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure) public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddApiAuthorization<TRemoteAuthenticationState, TAccount>(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure)
where TRemoteAuthenticationState : RemoteAuthenticationState, new() where TRemoteAuthenticationState : RemoteAuthenticationState, new()
where TAccount : RemoteUserAccount
{ {
return AddApiauthorizationCore<TRemoteAuthenticationState>(services, configure, Assembly.GetCallingAssembly().GetName().Name); return AddApiauthorizationCore<TRemoteAuthenticationState, TAccount>(services, configure, Assembly.GetCallingAssembly().GetName().Name);
} }
private static IServiceCollection AddApiauthorizationCore<TRemoteAuthenticationState>( private static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddApiauthorizationCore<TRemoteAuthenticationState, TAccount>(
IServiceCollection services, IServiceCollection services,
Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure,
string inferredClientId) string inferredClientId)
where TRemoteAuthenticationState : RemoteAuthenticationState, new() where TRemoteAuthenticationState : RemoteAuthenticationState
where TAccount : RemoteUserAccount
{ {
services.TryAddEnumerable( services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>, DefaultApiAuthorizationOptionsConfiguration>(_ => ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>, DefaultApiAuthorizationOptionsConfiguration>(_ =>
new DefaultApiAuthorizationOptionsConfiguration(inferredClientId))); new DefaultApiAuthorizationOptionsConfiguration(inferredClientId)));
services.AddRemoteAuthentication<TRemoteAuthenticationState, ApiAuthorizationProviderOptions>(configure); services.AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, ApiAuthorizationProviderOptions>(configure);
return services; return new RemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>(services);
} }
} }
} }

View File

@ -4,11 +4,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
@ -21,10 +24,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.SignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState> testJsRuntime.SignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
@ -51,10 +55,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.SignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState> testJsRuntime.SignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
@ -77,10 +82,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.CompleteSignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState> testJsRuntime.CompleteSignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
@ -107,10 +113,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.CompleteSignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState> testJsRuntime.CompleteSignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
@ -133,10 +140,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.SignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState> testJsRuntime.SignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
@ -163,10 +171,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.SignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState> testJsRuntime.SignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
@ -189,10 +198,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.CompleteSignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState> testJsRuntime.CompleteSignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
@ -219,10 +229,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.CompleteSignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState> testJsRuntime.CompleteSignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
@ -245,10 +256,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
@ -282,10 +294,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
@ -321,10 +334,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
var state = new RemoteAuthenticationState(); var state = new RemoteAuthenticationState();
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
@ -361,12 +375,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
testJsRuntime.GetUserResult = null; testJsRuntime.GetUserResult = default;
// Act // Act
var result = await runtime.GetAuthenticatedUser(); var result = await runtime.GetAuthenticatedUser();
@ -387,19 +402,22 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions(); var options = CreateOptions();
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, CoolRoleAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new TestUserFactory(Mock.Of<IAccessTokenProviderAccessor>()));
var serializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; var account = new CoolRoleAccount
var serializedUser = JsonSerializer.Serialize(new
{ {
CoolName = "Alfred", CoolRole = new[] { "admin", "cool", "fantastic" },
CoolRole = new[] { "admin", "cool", "fantastic" } AdditionalProperties = new Dictionary<string, object>
}, serializationOptions); {
["CoolName"] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize("Alfred"))
}
};
testJsRuntime.GetUserResult = JsonSerializer.Deserialize<IDictionary<string, object>>(serializedUser); testJsRuntime.GetUserResult = account;
// Act // Act
var result = await runtime.GetAuthenticatedUser(); var result = await runtime.GetAuthenticatedUser();
@ -420,19 +438,22 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
// Arrange // Arrange
var testJsRuntime = new TestJsRuntime(); var testJsRuntime = new TestJsRuntime();
var options = CreateOptions("scope"); var options = CreateOptions("scope");
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>( var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, CoolRoleAccount, OidcProviderOptions>(
testJsRuntime, testJsRuntime,
options, options,
new TestNavigationManager()); new TestNavigationManager(),
new TestUserFactory(Mock.Of<IAccessTokenProviderAccessor>()));
var serializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; var account = new CoolRoleAccount
var serializedUser = JsonSerializer.Serialize(new
{ {
CoolName = "Alfred", CoolRole = new[] { "admin", "cool", "fantastic" },
CoolRole = new[] { "admin", "cool", "fantastic" } AdditionalProperties = new Dictionary<string, object>
}, serializationOptions); {
["CoolName"] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize("Alfred")),
}
};
testJsRuntime.GetUserResult = JsonSerializer.Deserialize<IDictionary<string, object>>(serializedUser); testJsRuntime.GetUserResult = account;
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
{ {
Status = "success", Status = "success",
@ -509,22 +530,21 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
public InternalAccessTokenResult GetAccessTokenResult { get; set; } public InternalAccessTokenResult GetAccessTokenResult { get; set; }
public IDictionary<string, object> GetUserResult { get; set; } public RemoteUserAccount GetUserResult { get; set; }
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args) public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
{ {
PastInvocations.Add((identifier, args)); PastInvocations.Add((identifier, args));
return new ValueTask<TValue>((TValue)GetInvocationResult<TValue>(identifier)); return new ValueTask<TValue>((TValue)GetInvocationResult(identifier));
} }
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args) public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args)
{ {
PastInvocations.Add((identifier, args)); PastInvocations.Add((identifier, args));
return new ValueTask<TValue>((TValue)GetInvocationResult<TValue>(identifier)); return new ValueTask<TValue>((TValue)GetInvocationResult(identifier));
} }
private object GetInvocationResult<TValue>(string identifier) private object GetInvocationResult(string identifier)
{ {
switch (identifier) switch (identifier)
{ {
@ -551,6 +571,35 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
} }
} }
internal class TestUserFactory : UserFactory<CoolRoleAccount>
{
public TestUserFactory(IAccessTokenProviderAccessor accessor) : base(accessor)
{
}
public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
CoolRoleAccount account,
RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
if (account.CoolRole != null)
{
foreach (var role in account.CoolRole)
{
((ClaimsIdentity)user.Identity).AddClaim(new Claim("CoolRole", role));
}
}
return user;
}
}
internal class CoolRoleAccount : RemoteUserAccount
{
public string[] CoolRole { get; set; }
}
internal class TestNavigationManager : NavigationManager internal class TestNavigationManager : NavigationManager
{ {
public TestNavigationManager() => public TestNavigationManager() =>

View File

@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -242,7 +243,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager( var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/"); "https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/");
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test"))); authServiceMock.GetAuthenticatedUserCallback = () => new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(new ClaimsIdentity("Test")));
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>() authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
{ {
@ -269,7 +270,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager( var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
"https://www.example.com/base/authentication/logout"); "https://www.example.com/base/authentication/logout");
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test"))); authServiceMock.GetAuthenticatedUserCallback = () => new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(new ClaimsIdentity("Test")));
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>() authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
{ {
@ -296,7 +297,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
var originalUrl = "https://www.example.com/base/authentication/login?returnUrl=https://www.example.com/base/fetchData"; var originalUrl = "https://www.example.com/base/authentication/login?returnUrl=https://www.example.com/base/fetchData";
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(originalUrl); var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(originalUrl);
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test"))); authServiceMock.GetAuthenticatedUserCallback = () => new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(new ClaimsIdentity("Test")));
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>() authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
{ {
@ -350,7 +351,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager( var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/fetchData"); "https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/fetchData");
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test"))); authServiceMock.GetAuthenticatedUserCallback = () => new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(new ClaimsIdentity("Test")));
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>() authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
{ {
@ -660,13 +661,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
} }
} }
private class TestRemoteAuthenticationService : RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions> private class TestRemoteAuthenticationService : RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>
{ {
public TestRemoteAuthenticationService( public TestRemoteAuthenticationService(
IJSRuntime jsRuntime, IJSRuntime jsRuntime,
IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options, IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options,
TestNavigationManager navigationManager) : TestNavigationManager navigationManager) :
base(jsRuntime, options, navigationManager) base(jsRuntime, options, navigationManager, new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()))
{ {
} }
@ -674,13 +675,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> CompleteSignInCallback { 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>>> SignOutCallback { get; set; }
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> CompleteSignOutCallback { get; set; } public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> CompleteSignOutCallback { get; set; }
public Func<Task<ClaimsPrincipal>> GetAuthenticatedUserCallback { get; set; } public Func<ValueTask<ClaimsPrincipal>> GetAuthenticatedUserCallback { get; set; }
public async override Task<AuthenticationState> GetAuthenticationStateAsync() => new AuthenticationState(await GetAuthenticatedUserCallback()); public async override Task<AuthenticationState> GetAuthenticationStateAsync() => new AuthenticationState(await GetAuthenticatedUserCallback());
public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> CompleteSignInAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => CompleteSignInCallback(context); public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> CompleteSignInAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => CompleteSignInCallback(context);
protected internal override Task<ClaimsPrincipal> GetAuthenticatedUser() => GetAuthenticatedUserCallback(); protected internal override ValueTask<ClaimsPrincipal> GetAuthenticatedUser() => GetAuthenticatedUserCallback();
public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> CompleteSignOutAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => CompleteSignOutCallback(context); public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> CompleteSignOutAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => CompleteSignOutCallback(context);

View File

@ -2,13 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.JSInterop.WebAssembly;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
var user = options.Value.UserOptions; var user = options.Value.UserOptions;
Assert.Equal("Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests", user.AuthenticationType); Assert.Equal("Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests", user.AuthenticationType);
Assert.Equal("scope", user.ScopeClaim); Assert.Equal("scope", user.ScopeClaim);
Assert.Null(user.RoleClaim); Assert.Equal("role", user.RoleClaim);
Assert.Equal("name", user.NameClaim); Assert.Equal("name", user.NameClaim);
Assert.Equal( Assert.Equal(
@ -69,6 +69,131 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
options.Value.ProviderOptions.ConfigurationEndpoint); options.Value.ProviderOptions.ConfigurationEndpoint);
} }
[Fact]
public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce()
{
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
var calls = 0;
builder.Services.AddApiAuthorization(options =>
{
calls++;
});
var host = builder.Build();
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
var user = options.Value.UserOptions;
Assert.Equal("Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests", user.AuthenticationType);
// Make sure that the defaults are applied on this overload
Assert.Equal("role", user.RoleClaim);
Assert.Equal(
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
options.Value.ProviderOptions.ConfigurationEndpoint);
Assert.Equal(1, calls);
}
[Fact]
public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
var calls = 0;
builder.Services.AddApiAuthorization<TestAuthenticationState>(options => calls++);
var host = builder.Build();
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
var user = options.Value.UserOptions;
// Make sure that the defaults are applied on this overload
Assert.Equal("role", user.RoleClaim);
Assert.Equal(
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
options.Value.ProviderOptions.ConfigurationEndpoint);
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
Assert.NotNull(authenticationService);
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, RemoteUserAccount, ApiAuthorizationProviderOptions>>(authenticationService);
Assert.Equal(1, calls);
}
[Fact]
public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
builder.Services.AddApiAuthorization<TestAuthenticationState>();
var host = builder.Build();
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
var user = options.Value.UserOptions;
// Make sure that the defaults are applied on this overload
Assert.Equal("role", user.RoleClaim);
Assert.Equal(
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
options.Value.ProviderOptions.ConfigurationEndpoint);
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
Assert.NotNull(authenticationService);
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, RemoteUserAccount, ApiAuthorizationProviderOptions>>(authenticationService);
}
[Fact]
public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
var calls = 0;
builder.Services.AddApiAuthorization<TestAuthenticationState, TestAccount>(options => calls++);
var host = builder.Build();
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
var user = options.Value.UserOptions;
// Make sure that the defaults are applied on this overload
Assert.Equal("role", user.RoleClaim);
Assert.Equal(
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
options.Value.ProviderOptions.ConfigurationEndpoint);
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
Assert.NotNull(authenticationService);
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, TestAccount, ApiAuthorizationProviderOptions>>(authenticationService);
Assert.Equal(1, calls);
}
[Fact]
public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
builder.Services.AddApiAuthorization<TestAuthenticationState, TestAccount>();
var host = builder.Build();
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
var user = options.Value.UserOptions;
// Make sure that the defaults are applied on this overload
Assert.Equal("role", user.RoleClaim);
Assert.Equal(
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
options.Value.ProviderOptions.ConfigurationEndpoint);
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
Assert.NotNull(authenticationService);
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, TestAccount, ApiAuthorizationProviderOptions>>(authenticationService);
}
[Fact] [Fact]
public void ApiAuthorizationOptions_DefaultsCanBeOverriden() public void ApiAuthorizationOptions_DefaultsCanBeOverriden()
{ {
@ -236,6 +361,67 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
Assert.Equal("https://www.example.com/base/custom-logout", provider.PostLogoutRedirectUri); Assert.Equal("https://www.example.com/base/custom-logout", provider.PostLogoutRedirectUri);
} }
[Fact]
public void AddOidc_ConfigurationGetsCalledOnce()
{
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
var calls = 0;
builder.Services.AddOidcAuthentication(options => calls++);
builder.Services.Replace(ServiceDescriptor.Singleton(typeof(NavigationManager), new TestNavigationManager()));
var host = builder.Build();
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>();
Assert.Equal("name", options.Value.UserOptions.NameClaim);
Assert.Equal(1, calls);
}
[Fact]
public void AddOidc_CustomState_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
var calls = 0;
builder.Services.AddOidcAuthentication<TestAuthenticationState>(options => options.ProviderOptions.Authority = (++calls).ToString());
builder.Services.Replace(ServiceDescriptor.Singleton(typeof(NavigationManager), new TestNavigationManager()));
var host = builder.Build();
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>();
// Make sure options are applied
Assert.Equal("name", options.Value.UserOptions.NameClaim);
Assert.Equal("1", options.Value.ProviderOptions.Authority);
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
Assert.NotNull(authenticationService);
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, RemoteUserAccount, OidcProviderOptions>>(authenticationService);
}
[Fact]
public void AddOidc_CustomStateAndAccount_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
var calls = 0;
builder.Services.AddOidcAuthentication<TestAuthenticationState, TestAccount>(options => options.ProviderOptions.Authority = (++calls).ToString());
builder.Services.Replace(ServiceDescriptor.Singleton(typeof(NavigationManager), new TestNavigationManager()));
var host = builder.Build();
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>();
// Make sure options are applied
Assert.Equal("name", options.Value.UserOptions.NameClaim);
Assert.Equal("1", options.Value.ProviderOptions.Authority);
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
Assert.NotNull(authenticationService);
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, TestAccount, OidcProviderOptions>>(authenticationService);
}
private class TestNavigationManager : NavigationManager private class TestNavigationManager : NavigationManager
{ {
public TestNavigationManager() public TestNavigationManager()
@ -245,5 +431,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
protected override void NavigateToCore(string uri, bool forceLoad) => throw new System.NotImplementedException(); protected override void NavigateToCore(string uri, bool forceLoad) => throw new System.NotImplementedException();
} }
private class TestAuthenticationState : RemoteAuthenticationState
{
}
private class TestAccount : RemoteUserAccount
{
}
} }
} }

View File

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
namespace Wasm.Authentication.Client
{
public class OidcAccount : RemoteUserAccount
{
[JsonPropertyName("amr")]
public string[] AuthenticationMethod { get; set; }
}
}

View File

@ -0,0 +1,47 @@
@page "/admin-settings"
@attribute [Authorize(Roles = "admin")]
@inject IAccessTokenProvider TokenProvider
@inject NavigationManager Navigation
@if(_error == null)
{
<button id="admin-action" @onclick="AdminAction">Perform admin action</button>
}
else if(_error == true)
{
<p>Could not get the access token.</p>
}
else if (_error == false)
{
<p id="admin-success">Successfully perfomed admin action.</p>
}
@code {
private bool? _error;
public async Task AdminAction()
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var token))
{
var client = new HttpClient() { BaseAddress = new Uri(Navigation.BaseUri) };
var request = new HttpRequestMessage(HttpMethod.Post, "Roles/AdminOnly");
request.Headers.Add("Authorization", $"Bearer {token.Value}");
var response = await client.SendAsync(request);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
_error = true;
}
else
{
_error = false;
}
}
else
{
_error = true;
}
}
}

View File

@ -1,7 +1,43 @@
@page "/authentication/{action}" @page "/authentication/{action}"
@inject StateService State
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager Navigation
<RemoteAuthenticatorView Action="@Action" /> <RemoteAuthenticatorViewCore TAuthenticationState="RemoteAppState"
AuthenticationState="AppState"
OnLogInSucceeded="CompleteLogin"
Action="@Action" />
@code{ @code{
[Parameter] public string Action { get; set; } [Parameter] public string Action { get; set; }
public RemoteAppState AppState { get; set; } = new RemoteAppState();
protected override void OnInitialized()
{
if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn, Action))
{
AppState.State = State.GetCurrentState();
}
base.OnInitialized();
}
public async Task CompleteLogin(RemoteAppState remoteState)
{
if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogInCallback, Action))
{
State.RestoreCurrentState(remoteState.State);
}
var userState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = userState.User;
if (user.HasClaim("NewUser", "true"))
{
var originalReturnUrl = remoteState.ReturnUrl;
var preferencesUrl = Navigation.ToAbsoluteUri("preferences");
remoteState.ReturnUrl = $"{preferencesUrl}?returnUrl={Uri.EscapeDataString(originalReturnUrl)}";
}
}
} }

View File

@ -1,5 +1,27 @@
@page "/" @page "/"
@inject StateService State
@inject IJSRuntime JS
<h1>Hello, world!</h1> <h1>Hello, world!</h1>
Welcome to your new app. Welcome to your new app.
Current state is:
<p id="app-state">@State.GetCurrentState()</p>
<!-- Elements to help testing functionality -->
<p id="test-helpers">
<button id="test-clear-storage" @onclick="ClearStorage">Clear storage</button>
<button id="test-refresh-page" @onclick="TriggerPageRefresh">Refresh page</button>
</p>
@code{
public async Task ClearStorage()
{
await JS.InvokeVoidAsync("sessionStorage.clear");
}
public async Task TriggerPageRefresh()
{
await JS.InvokeVoidAsync("location.reload", true);
}
}

View File

@ -0,0 +1,37 @@
@page "/new-admin"
@attribute [Authorize]
@inject IAccessTokenProvider TokenProvider
@inject NavigationManager Navigation
@if (_error == true)
{
<p>Could not get the access token.</p>
}
else if (_error == false)
{
<p>Successfully added to the admin group.</p>
}
@code {
private bool? _error;
protected override async Task OnInitializedAsync()
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var token))
{
var client = new HttpClient() { BaseAddress = new Uri(Navigation.BaseUri) };
var request = new HttpRequestMessage(HttpMethod.Post, "Roles/MakeAdmin");
request.Headers.Add("Authorization", $"Bearer {token.Value}");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
}
else
{
_error = true;
}
}
}

View File

@ -0,0 +1,47 @@
@page "/preferences"
@attribute [Authorize]
@using System.Text.Json;
@using System.Text;
@using System.Net.Http.Headers;
@inject NavigationManager Navigation
@inject IAccessTokenProvider AccessTokenProvider
<p>User preferences</p>
<input id="color-preference" type="text" @bind="Color" />
<button id="submit-preference" @onclick="SendPreferences">Send</button>
@code {
public string Color { get; set; }
public async Task SendPreferences()
{
var content = new StringContent(JsonSerializer.Serialize(new UserPreferences { Color = Color }), Encoding.UTF8, "application/json");
var tokenResponse = await AccessTokenProvider.RequestAccessToken();
if (tokenResponse.TryGetToken(out var token))
{
var client = new HttpClient { BaseAddress = new Uri(Navigation.BaseUri) };
var request = new HttpRequestMessage(HttpMethod.Post, "Preferences/AddPreferences");
request.Content = content;
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Value);
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var query = new Uri(Navigation.Uri).Query;
var hasReturnUrl = System.Text.RegularExpressions.Regex.Match(query, ".*?returnUrl=([^&]+).*");
if (hasReturnUrl.Success)
{
var returnUrl = hasReturnUrl.Groups[1];
Navigation.NavigateTo(Uri.UnescapeDataString(returnUrl.Value));
}
}
else
{
Navigation.NavigateTo("/");
}
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
namespace Wasm.Authentication.Client
{
public class PreferencesUserFactory : UserFactory<OidcAccount>
{
private readonly HttpClient _httpClient;
public PreferencesUserFactory(NavigationManager navigationManager, IAccessTokenProviderAccessor accessor)
: base(accessor)
{
_httpClient = new HttpClient { BaseAddress = new Uri(navigationManager.BaseUri) };
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
OidcAccount account,
RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);
if (initialUser.Identity.IsAuthenticated)
{
foreach (var value in account.AuthenticationMethod)
{
((ClaimsIdentity)initialUser.Identity).AddClaim(new Claim("amr", value));
}
var tokenResponse = await TokenProvider.RequestAccessToken();
if (tokenResponse.TryGetToken(out var token))
{
var request = new HttpRequestMessage(HttpMethod.Get, "Preferences/HasCompletedAdditionalInformation");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Value);
var response = await _httpClient.SendAsync(request);
if (response.StatusCode != HttpStatusCode.OK)
{
throw new InvalidOperationException("Error accessing additional user info.");
}
var hasInfo = JsonSerializer.Deserialize<bool>(await response.Content.ReadAsStringAsync());
if (!hasInfo)
{
// The actual pattern would be to cache this info to avoid constant queries to the server per auth update.
// (By default once every minute)
((ClaimsIdentity)initialUser.Identity).AddClaim(new Claim("NewUser", "true"));
}
}
}
return initialUser;
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -13,7 +14,10 @@ namespace Wasm.Authentication.Client
{ {
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddApiAuthorization(); builder.Services.AddApiAuthorization<RemoteAppState, OidcAccount>()
.AddUserFactory<RemoteAppState, OidcAccount, PreferencesUserFactory>();
builder.Services.AddSingleton<StateService>();
builder.RootComponents.Add<App>("app"); builder.RootComponents.Add<App>("app");

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
namespace Wasm.Authentication.Client
{
public class RemoteAppState : RemoteAuthenticationState
{
public string State { get; set; }
}
}

View File

@ -27,6 +27,17 @@
<span class="oi oi-list-rich" aria-hidden="true"></span> User <span class="oi oi-list-rich" aria-hidden="true"></span> User
</NavLink> </NavLink>
</li> </li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="new-admin">
<span class="oi oi-list-rich" aria-hidden="true"></span> Make admin
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="admin-settings">
<span class="oi oi-list-rich" aria-hidden="true"></span> Settings
</NavLink>
</li>
</ul> </ul>
</div> </div>

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Wasm.Authentication.Client
{
public class StateService
{
private string _state;
public string GetCurrentState() => _state ??= Guid.NewGuid().ToString();
public void RestoreCurrentState(string state) => _state = state;
}
}

View File

@ -21,5 +21,4 @@
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script> <script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
<script src="_framework/blazor.webassembly.js"></script> <script src="_framework/blazor.webassembly.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,54 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore.Internal;
using Wasm.Authentication.Server.Data;
using Wasm.Authentication.Server.Models;
namespace Wasm.Authentication.Server.Controllers
{
[ApiController]
[Authorize]
public class PreferencesController : ControllerBase
{
private readonly ApplicationDbContext _context;
public PreferencesController(ApplicationDbContext context)
{
_context = context;
}
[HttpGet("[controller]/[action]")]
public IActionResult HasCompletedAdditionalInformation()
{
var id = User.FindFirst(ClaimTypes.NameIdentifier);
if (!_context.UserPreferences.Where(u => u.ApplicationUserId == id.Value).Any())
{
return Ok(false);
}
else
{
return Ok(true);
}
}
[HttpPost("[controller]/[action]")]
public async Task<IActionResult> AddPreferences([FromBody] UserPreference preferences)
{
var id = User.FindFirst(ClaimTypes.NameIdentifier);
if (!_context.UserPreferences.Where(u => u.ApplicationUserId == id.Value).Any())
{
preferences.ApplicationUserId = id.Value;
_context.UserPreferences.Add(preferences);
await _context.SaveChangesAsync();
return Ok();
}
else
{
return BadRequest();
}
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Wasm.Authentication.Server.Models;
namespace Wasm.Authentication.Server.Controllers
{
[Authorize]
public class RolesController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly IOptions<IdentityOptions> _options;
public RolesController(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> options)
{
_userManager = userManager;
_roleManager = roleManager;
_options = options;
}
[HttpPost("[controller]/[action]")]
public async Task<IActionResult> MakeAdmin()
{
var admin = await _roleManager.FindByNameAsync("admin");
if (admin == null)
{
await _roleManager.CreateAsync(new IdentityRole { Name = "admin" });
}
var id = User.FindFirst(ClaimTypes.NameIdentifier);
if (id == null)
{
return BadRequest();
}
var currentUser = await _userManager.FindByIdAsync(id.Value);
await _userManager.AddToRoleAsync(currentUser, "admin");
return Ok();
}
[HttpPost("[controller]/[action]")]
[Authorize(Roles = "admin")]
public IActionResult AdminOnly()
{
return Ok();
}
}
}

View File

@ -13,5 +13,21 @@ namespace Wasm.Authentication.Server.Data
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions) IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
{ {
} }
public DbSet<UserPreference> UserPreferences { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<ApplicationUser>().HasOne(u => u.UserPreference);
builder.Entity<UserPreference>()
.Property(u => u.Id).ValueGeneratedOnAdd();
builder.Entity<UserPreference>()
.HasKey(p => p.Id);
}
} }
} }

View File

@ -9,14 +9,14 @@ using Wasm.Authentication.Server.Data;
namespace Wasm.Authentication.Server.Data.Migrations namespace Wasm.Authentication.Server.Data.Migrations
{ {
[DbContext(typeof(ApplicationDbContext))] [DbContext(typeof(ApplicationDbContext))]
[Migration("20200123141439_Initial")] [Migration("20200324213904_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "3.1.0"); .HasAnnotation("ProductVersion", "3.1.2");
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
{ {
@ -296,6 +296,26 @@ namespace Wasm.Authentication.Server.Data.Migrations
b.ToTable("AspNetUsers"); b.ToTable("AspNetUsers");
}); });
modelBuilder.Entity("Wasm.Authentication.Server.Models.UserPreference", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("Color")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("UserPreferences");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
@ -346,6 +366,13 @@ namespace Wasm.Authentication.Server.Data.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("Wasm.Authentication.Server.Models.UserPreference", b =>
{
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
.WithOne("UserPreference")
.HasForeignKey("Wasm.Authentication.Server.Models.UserPreference", "ApplicationUserId");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -186,6 +186,25 @@ namespace Wasm.Authentication.Server.Data.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "UserPreferences",
columns: table => new
{
Id = table.Column<string>(nullable: false),
ApplicationUserId = table.Column<string>(nullable: true),
Color = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserPreferences", x => x.Id);
table.ForeignKey(
name: "FK_UserPreferences_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId", name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims", table: "AspNetRoleClaims",
@ -243,6 +262,12 @@ namespace Wasm.Authentication.Server.Data.Migrations
name: "IX_PersistedGrants_SubjectId_ClientId_Type", name: "IX_PersistedGrants_SubjectId_ClientId_Type",
table: "PersistedGrants", table: "PersistedGrants",
columns: new[] { "SubjectId", "ClientId", "Type" }); columns: new[] { "SubjectId", "ClientId", "Type" });
migrationBuilder.CreateIndex(
name: "IX_UserPreferences_ApplicationUserId",
table: "UserPreferences",
column: "ApplicationUserId",
unique: true);
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
@ -268,6 +293,9 @@ namespace Wasm.Authentication.Server.Data.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PersistedGrants"); name: "PersistedGrants");
migrationBuilder.DropTable(
name: "UserPreferences");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "AspNetRoles"); name: "AspNetRoles");

View File

@ -14,7 +14,7 @@ namespace Wasm.Authentication.Server.Data.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "3.1.0"); .HasAnnotation("ProductVersion", "3.1.2");
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
{ {
@ -294,6 +294,26 @@ namespace Wasm.Authentication.Server.Data.Migrations
b.ToTable("AspNetUsers"); b.ToTable("AspNetUsers");
}); });
modelBuilder.Entity("Wasm.Authentication.Server.Models.UserPreference", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("Color")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("UserPreferences");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
@ -344,6 +364,13 @@ namespace Wasm.Authentication.Server.Data.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("Wasm.Authentication.Server.Models.UserPreference", b =>
{
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
.WithOne("UserPreference")
.HasForeignKey("Wasm.Authentication.Server.Models.UserPreference", "ApplicationUserId");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -8,5 +8,6 @@ namespace Wasm.Authentication.Server.Models
{ {
public class ApplicationUser : IdentityUser public class ApplicationUser : IdentityUser
{ {
public UserPreference UserPreference { get; set; }
} }
} }

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace Wasm.Authentication.Server.Models
{
public class UserPreference
{
public string Id { get; set; }
public string ApplicationUserId { get; set; }
public string Color { get; set; }
}
}

View File

@ -1,6 +1,9 @@
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -27,10 +30,17 @@ namespace Wasm.Authentication.Server
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true) services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>(); .AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer() services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(); .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
// Need to do this as it maps "role" to ClaimTypes.Role and causes issues
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
services.AddAuthentication() services.AddAuthentication()
.AddIdentityServerJwt(); .AddIdentityServerJwt();

View File

@ -19,7 +19,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using OpenQA.Selenium; using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI; using OpenQA.Selenium.Support.UI;
using Wasm.Authentication.Server; using Wasm.Authentication.Server;
using Wasm.Authentication.Server.Data; using Wasm.Authentication.Server.Data;
@ -28,7 +27,7 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{ {
public class WebAssemblyAuthenticationTests : ServerTestBase<AspNetSiteServerFixture>, IDisposable public class WebAssemblyAuthenticationTests : ServerTestBase<AspNetSiteServerFixture>
{ {
private static readonly SqliteConnection _connection; private static readonly SqliteConnection _connection;
@ -55,15 +54,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
.Build(); .Build();
} }
public override Task InitializeAsync() => base.InitializeAsync(Guid.NewGuid().ToString());
protected override void InitializeAsyncCore() protected override void InitializeAsyncCore()
{ {
Browser.Navigate().GoToUrl("data:");
Navigate("/", noReload: true); Navigate("/", noReload: true);
EnsureDatabaseCreated(_serverFixture.Host.Services); EnsureDatabaseCreated(_serverFixture.Host.Services);
Browser.ExecuteJavaScript("sessionStorage.clear()");
Browser.ExecuteJavaScript("localStorage.clear()");
Browser.Manage().Cookies.DeleteAllCookies();
Browser.Navigate().Refresh();
WaitUntilLoaded(); WaitUntilLoaded();
} }
@ -89,6 +85,50 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
ValidateFetchData(); ValidateFetchData();
} }
[Fact]
public void CanPreserveApplicationState_DuringLogIn()
{
var originalAppState = Browser.Exists(By.Id("app-state")).Text;
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();
var homeLink = By.PartialLinkText("Home");
var homePage = "/";
ClickAndNavigate(homeLink, homePage);
var restoredAppState = Browser.Exists(By.Id("app-state")).Text;
Assert.Equal(originalAppState, restoredAppState);
}
[Fact]
public void CanShareUserRolesBetweenClientAndServer()
{
ClickAndNavigate(By.PartialLinkText("Log in"), "/Identity/Account/Login");
var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$";
FirstTimeRegister(userName, password);
ClickAndNavigate(By.PartialLinkText("Make admin"), "/new-admin");
ClickAndNavigate(By.PartialLinkText("Settings"), "/admin-settings");
Browser.Exists(By.Id("admin-action")).Click();
Browser.Exists(By.Id("admin-success"));
}
private void ClickAndNavigate(By link, string page) private void ClickAndNavigate(By link, string page)
{ {
Browser.FindElement(link).Click(); Browser.FindElement(link).Click();
@ -103,6 +143,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var userName = $"{Guid.NewGuid()}@example.com"; var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$"; var password = $"!Test.Password1$";
RegisterCore(userName, password); RegisterCore(userName, password);
CompleteProfileDetails();
// Need to navigate to fetch page // Need to navigate to fetch page
Browser.FindElement(By.PartialLinkText("Fetch data")).Click(); Browser.FindElement(By.PartialLinkText("Fetch data")).Click();
@ -133,13 +174,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
.OrderBy(o => o.Item1) .OrderBy(o => o.Item1)
.ToArray(); .ToArray();
Assert.Equal(4, claims.Length); Assert.Equal(5, claims.Length);
Assert.Equal(new[] Assert.Equal(new[]
{ {
("amr", "pwd"), ("amr", "pwd"),
("idp", "local"), ("idp", "local"),
("name", userName), ("name", userName),
("NewUser", "true"),
("preferred_username", userName) ("preferred_username", userName)
}, },
claims); claims);
@ -173,9 +215,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var userName = $"{Guid.NewGuid()}@example.com"; var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$"; var password = $"!Test.Password1$";
RegisterCore(userName, password); RegisterCore(userName, password);
CompleteProfileDetails();
Browser.Exists(By.PartialLinkText($"Hello, {userName}!")).Click(); ClickAndNavigate(By.PartialLinkText($"Hello, {userName}!"), "/Identity/Account/Manage");
Browser.Contains("/Identity/Account/Manage", () => Browser.Url);
Browser.Navigate().Back(); Browser.Navigate().Back();
Browser.Equal("/", () => new Uri(Browser.Url).PathAndQuery); Browser.Equal("/", () => new Uri(Browser.Url).PathAndQuery);
@ -215,6 +257,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var userName = $"{Guid.NewGuid()}@example.com"; var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$"; var password = $"!Test.Password1$";
RegisterCore(userName, password); RegisterCore(userName, password);
CompleteProfileDetails();
ValidateLogout(); ValidateLogout();
} }
@ -227,6 +270,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var userName = $"{Guid.NewGuid()}@example.com"; var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$"; var password = $"!Test.Password1$";
RegisterCore(userName, password); RegisterCore(userName, password);
CompleteProfileDetails();
ValidateLogout(); ValidateLogout();
@ -252,14 +296,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var userName = $"{Guid.NewGuid()}@example.com"; var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$"; var password = $"!Test.Password1$";
RegisterCore(userName, password); RegisterCore(userName, password);
CompleteProfileDetails();
ValidateLoggedIn(userName); ValidateLoggedIn(userName);
// Clear the existing storage on the page and refresh // Clear the existing storage on the page and refresh
Browser.ExecuteJavaScript("sessionStorage.clear()"); Browser.Exists(By.Id("test-clear-storage")).Click();
Browser.Navigate().Refresh(); Browser.Exists(By.Id("test-refresh-page")).Click();
Browser.Exists(By.PartialLinkText("Log in"));
Browser.FindElement(By.PartialLinkText("Log in")).Click();
ValidateLoggedIn(userName); ValidateLoggedIn(userName);
} }
@ -268,6 +311,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{ {
Browser.Navigate().GoToUrl(new Uri(new Uri(Browser.Url), "/authentication/login?returnUrl=https%3A%2F%2Fwww.bing.com").AbsoluteUri); Browser.Navigate().GoToUrl(new Uri(new Uri(Browser.Url), "/authentication/login?returnUrl=https%3A%2F%2Fwww.bing.com").AbsoluteUri);
WaitUntilLoaded(skipHeader: true); WaitUntilLoaded(skipHeader: true);
Browser.Exists(By.CssSelector("[style=\"display: block;\"]"));
Assert.NotEmpty(Browser.GetBrowserLogs(LogLevel.Severe)); Assert.NotEmpty(Browser.GetBrowserLogs(LogLevel.Severe));
} }
@ -322,6 +366,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{ {
Browser.FindElement(By.PartialLinkText("Register as a new user")).Click(); Browser.FindElement(By.PartialLinkText("Register as a new user")).Click();
RegisterCore(userName, password); RegisterCore(userName, password);
CompleteProfileDetails();
}
private void CompleteProfileDetails()
{
Browser.Exists(By.PartialLinkText("Home"));
Browser.Contains("/preferences", () => Browser.Url);
Browser.FindElement(By.Id("color-preference")).SendKeys("Red");
Browser.FindElement(By.Id("submit-preference")).Click();
} }
private void RegisterCore(string userName, string password) private void RegisterCore(string userName, string password)
@ -410,13 +463,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
} }
} }
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 private class JwtPayload
{ {
[JsonPropertyName("iss")] [JsonPropertyName("iss")]