[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:
parent
e67e7a08ca
commit
fd9c786165
|
|
@ -37,7 +37,22 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static IServiceCollection AddMsalAuthentication<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<MsalProviderOptions>> configure)
|
||||
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>());
|
||||
|
||||
return services;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -48,27 +48,49 @@ export enum AuthenticationResultStatus {
|
|||
|
||||
export interface AuthenticationResult {
|
||||
status: AuthenticationResultStatus;
|
||||
state?: any;
|
||||
state?: unknown;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface AuthorizeService {
|
||||
getUser(): Promise<any>;
|
||||
getUser(): Promise<unknown>;
|
||||
getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
|
||||
signIn(state: any): Promise<AuthenticationResult>;
|
||||
completeSignIn(state: any): Promise<AuthenticationResult>;
|
||||
signOut(state: any): Promise<AuthenticationResult>;
|
||||
signIn(state: unknown): Promise<AuthenticationResult>;
|
||||
completeSignIn(state: unknown): Promise<AuthenticationResult>;
|
||||
signOut(state: unknown): Promise<AuthenticationResult>;
|
||||
completeSignOut(url: string): Promise<AuthenticationResult>;
|
||||
}
|
||||
|
||||
class OidcAuthorizeService implements AuthorizeService {
|
||||
private _userManager: UserManager;
|
||||
|
||||
private _intialSilentSignIn: Promise<void> | undefined;
|
||||
constructor(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() {
|
||||
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();
|
||||
return user && user.profile;
|
||||
}
|
||||
|
|
@ -120,7 +142,7 @@ class OidcAuthorizeService implements AuthorizeService {
|
|||
function hasAllScopes(request: AccessTokenRequestOptions | undefined, currentScopes: string[]) {
|
||||
const set = new Set(currentScopes);
|
||||
if (request && request.scopes) {
|
||||
for (let current of request.scopes) {
|
||||
for (const current of request.scopes) {
|
||||
if (!set.has(current)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -131,7 +153,7 @@ class OidcAuthorizeService implements AuthorizeService {
|
|||
}
|
||||
}
|
||||
|
||||
async signIn(state: any) {
|
||||
async signIn(state: unknown) {
|
||||
try {
|
||||
await this._userManager.clearStaleState();
|
||||
await this._userManager.signinSilent(this.createArguments());
|
||||
|
|
@ -166,7 +188,7 @@ class OidcAuthorizeService implements AuthorizeService {
|
|||
}
|
||||
}
|
||||
|
||||
async signOut(state: any) {
|
||||
async signOut(state: unknown) {
|
||||
try {
|
||||
if (!(await this._userManager.metadataService.getEndSessionEndpoint())) {
|
||||
await this._userManager.removeUser();
|
||||
|
|
@ -212,8 +234,8 @@ class OidcAuthorizeService implements AuthorizeService {
|
|||
|
||||
private async stateExists(url: string) {
|
||||
const stateParam = new URLSearchParams(new URL(url).search).get('state');
|
||||
if (stateParam) {
|
||||
return await this._userManager.settings.stateStore!.get(stateParam);
|
||||
if (stateParam && this._userManager.settings.stateStore) {
|
||||
return await this._userManager.settings.stateStore.get(stateParam);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -221,15 +243,15 @@ class OidcAuthorizeService implements AuthorizeService {
|
|||
|
||||
private async loginRequired(url: string) {
|
||||
const errorParameter = new URLSearchParams(new URL(url).search).get('error');
|
||||
if (errorParameter) {
|
||||
const error = await this._userManager.settings.stateStore!.get(errorParameter);
|
||||
if (errorParameter && this._userManager.settings.stateStore) {
|
||||
const error = await this._userManager.settings.stateStore.get(errorParameter);
|
||||
return error === 'login_required';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private createArguments(state?: any) {
|
||||
private createArguments(state?: unknown) {
|
||||
return { useReplaceToNavigate: true, data: state };
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +259,7 @@ class OidcAuthorizeService implements AuthorizeService {
|
|||
return { status: AuthenticationResultStatus.Failure, errorMessage: message };
|
||||
}
|
||||
|
||||
private success(state: any) {
|
||||
private success(state: unknown) {
|
||||
return { status: AuthenticationResultStatus.Success, state };
|
||||
}
|
||||
|
||||
|
|
@ -253,21 +275,57 @@ class OidcAuthorizeService implements AuthorizeService {
|
|||
export class AuthenticationService {
|
||||
|
||||
static _infrastructureKey = 'Microsoft.AspNetCore.Components.WebAssembly.Authentication';
|
||||
static _initialized : Promise<void>;
|
||||
static _initialized: Promise<void>;
|
||||
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.
|
||||
// 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.
|
||||
if (!AuthenticationService._initialized) {
|
||||
this._initialized = (async () => {
|
||||
const userManager = await this.createUserManager(settings);
|
||||
AuthenticationService.instance = new OidcAuthorizeService(userManager);
|
||||
})();
|
||||
AuthenticationService._initialized = AuthenticationService.initializeCore(settings);
|
||||
}
|
||||
|
||||
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() {
|
||||
|
|
@ -278,37 +336,46 @@ export class AuthenticationService {
|
|||
return AuthenticationService.instance.getAccessToken();
|
||||
}
|
||||
|
||||
public static signIn(state: any) {
|
||||
public static signIn(state: unknown) {
|
||||
return AuthenticationService.instance.signIn(state);
|
||||
}
|
||||
|
||||
public static completeSignIn(url: string) {
|
||||
return AuthenticationService.instance.completeSignIn(url);
|
||||
public static async completeSignIn(url: string) {
|
||||
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);
|
||||
}
|
||||
|
||||
public static completeSignOut(url: string) {
|
||||
return AuthenticationService.instance.completeSignOut(url);
|
||||
public static async completeSignOut(url: string) {
|
||||
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> {
|
||||
let finalSettings: UserManagerSettings;
|
||||
if (isApiAuthorizationSettings(settings)) {
|
||||
let response = await fetch(settings.configurationEndpoint);
|
||||
const response = await fetch(settings.configurationEndpoint);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not load settings from '${settings.configurationEndpoint}'`);
|
||||
}
|
||||
|
||||
const downloadedSettings = await response.json();
|
||||
|
||||
window.sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`, JSON.stringify(settings));
|
||||
|
||||
downloadedSettings.automaticSilentRenew = true;
|
||||
downloadedSettings.includeIdTokenInSilentRenew = true;
|
||||
|
||||
finalSettings = downloadedSettings;
|
||||
} else {
|
||||
if (!settings.scope) {
|
||||
|
|
@ -323,18 +390,24 @@ export class AuthenticationService {
|
|||
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);
|
||||
|
||||
userManager.events.addUserSignedOut(async () => {
|
||||
await userManager.removeUser();
|
||||
userManager.removeUser();
|
||||
});
|
||||
|
||||
return userManager;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window { AuthenticationService: AuthenticationService; }
|
||||
interface Window { AuthenticationService: AuthenticationService }
|
||||
}
|
||||
|
||||
AuthenticationService.handleCallback();
|
||||
|
||||
window.AuthenticationService = AuthenticationService;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"build": "npm run build:release",
|
||||
"build:release": "webpack --mode production --env.production --env.configuration=Release",
|
||||
"build:debug": "webpack --mode development --env.configuration=Debug",
|
||||
"watch": "webpack --watch --mode development"
|
||||
"watch": "webpack --watch --mode development --env.configuration=Debug"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-loader": "^6.2.1",
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
options.AuthenticationPaths.RemoteRegisterPath ??= "Identity/Account/Register";
|
||||
options.AuthenticationPaths.RemoteProfilePath ??= "Identity/Account/Manage";
|
||||
options.UserOptions.ScopeClaim ??= "scope";
|
||||
options.UserOptions.RoleClaim ??= "role";
|
||||
options.UserOptions.AuthenticationType ??= _applicationName;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -225,12 +225,14 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
{
|
||||
State = AuthenticationState
|
||||
});
|
||||
|
||||
switch (result.Status)
|
||||
{
|
||||
case RemoteAuthenticationStatus.Redirect:
|
||||
break;
|
||||
case RemoteAuthenticationStatus.Success:
|
||||
await NavigateToReturnUrl(returnUrl);
|
||||
await OnLogInSucceeded.InvokeAsync(result.State);
|
||||
await NavigateToReturnUrl(GetReturnUrl(result.State, returnUrl));
|
||||
break;
|
||||
case RemoteAuthenticationStatus.Failure:
|
||||
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.
|
||||
throw new InvalidOperationException("Should not redirect.");
|
||||
case RemoteAuthenticationStatus.Success:
|
||||
if (OnLogInSucceeded.HasDelegate)
|
||||
{
|
||||
await OnLogInSucceeded.InvokeAsync(result.State);
|
||||
}
|
||||
await OnLogInSucceeded.InvokeAsync(result.State);
|
||||
await NavigateToReturnUrl(GetReturnUrl(result.State));
|
||||
break;
|
||||
case RemoteAuthenticationStatus.OperationCompleted:
|
||||
|
|
@ -292,6 +291,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
case RemoteAuthenticationStatus.Redirect:
|
||||
break;
|
||||
case RemoteAuthenticationStatus.Success:
|
||||
await OnLogOutSucceeded.InvokeAsync(result.State);
|
||||
await NavigateToReturnUrl(returnUrl);
|
||||
break;
|
||||
case RemoteAuthenticationStatus.OperationCompleted:
|
||||
|
|
@ -320,10 +320,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// is when we are doing a redirect sign in flow.
|
||||
throw new InvalidOperationException("Should not redirect.");
|
||||
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()));
|
||||
break;
|
||||
case RemoteAuthenticationStatus.OperationCompleted:
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -17,15 +16,15 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
/// </summary>
|
||||
/// <typeparam name="TRemoteAuthenticationState">The state to preserve across authentication operations.</typeparam>
|
||||
/// <typeparam name="TProviderOptions">The options to be passed down to the underlying JavaScript library handling the authentication operations.</typeparam>
|
||||
public class RemoteAuthenticationService<TRemoteAuthenticationState, TProviderOptions> :
|
||||
public class RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions> :
|
||||
AuthenticationStateProvider,
|
||||
IRemoteAuthenticationService<TRemoteAuthenticationState>,
|
||||
IAccessTokenProvider
|
||||
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||
where TProviderOptions : new()
|
||||
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||
where TProviderOptions : new()
|
||||
where TAccount : RemoteUserAccount
|
||||
{
|
||||
private static readonly TimeSpan _userCacheRefreshInterval = TimeSpan.FromSeconds(60);
|
||||
|
||||
private bool _initialized = false;
|
||||
|
||||
// This defaults to 1/1/1970
|
||||
|
|
@ -33,33 +32,42 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
|
||||
/// <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>
|
||||
protected readonly IJSRuntime _jsRuntime;
|
||||
protected IJSRuntime JsRuntime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="NavigationManager"/> used to compute absolute urls.
|
||||
/// Gets the <see cref="NavigationManager"/> used to compute absolute urls.
|
||||
/// </summary>
|
||||
protected readonly NavigationManager _navigation;
|
||||
protected NavigationManager Navigation { get; }
|
||||
|
||||
/// <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>
|
||||
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>
|
||||
/// Initializes a new instance of <see cref="RemoteAuthenticationService{TRemoteAuthenticationState, TProviderOptions}"/>.
|
||||
/// </summary>
|
||||
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.</param>
|
||||
/// <param name="options">The options to be passed down to the underlying JavaScript library handling the authentication operations.</param>
|
||||
/// <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(
|
||||
IJSRuntime jsRuntime,
|
||||
IOptions<RemoteAuthenticationOptions<TProviderOptions>> options,
|
||||
NavigationManager navigation)
|
||||
NavigationManager navigation,
|
||||
UserFactory<TAccount> userFactory)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
_navigation = navigation;
|
||||
_options = options.Value;
|
||||
JsRuntime = jsRuntime;
|
||||
Navigation = navigation;
|
||||
UserFactory = userFactory;
|
||||
Options = options.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -70,11 +78,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||
{
|
||||
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();
|
||||
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||
{
|
||||
UpdateUser(GetUser());
|
||||
var getUserTask = GetUser();
|
||||
await getUserTask;
|
||||
UpdateUser(getUserTask);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -85,11 +95,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||
{
|
||||
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();
|
||||
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||
{
|
||||
UpdateUser(GetUser());
|
||||
var getUserTask = GetUser();
|
||||
await getUserTask;
|
||||
UpdateUser(getUserTask);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -100,11 +112,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||
{
|
||||
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();
|
||||
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||
{
|
||||
UpdateUser(GetUser());
|
||||
var getUserTask = GetUser();
|
||||
await getUserTask;
|
||||
UpdateUser(getUserTask);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -115,11 +129,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||
{
|
||||
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();
|
||||
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||
{
|
||||
UpdateUser(GetUser());
|
||||
var getUserTask = GetUser();
|
||||
await getUserTask;
|
||||
UpdateUser(getUserTask);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -129,7 +145,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
public virtual async ValueTask<AccessTokenResult> RequestAccessToken()
|
||||
{
|
||||
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))
|
||||
{
|
||||
|
|
@ -154,7 +170,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
}
|
||||
|
||||
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))
|
||||
{
|
||||
|
|
@ -172,13 +188,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
|
||||
private Uri GetRedirectUrl(string customReturnUrl)
|
||||
{
|
||||
var returnUrl = customReturnUrl != null ? _navigation.ToAbsoluteUri(customReturnUrl).ToString() : null;
|
||||
var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? _navigation.Uri);
|
||||
var redirectUrl = _navigation.ToAbsoluteUri($"{_options.AuthenticationPaths.LogInPath}?returnUrl={encodedReturnUrl}");
|
||||
var returnUrl = customReturnUrl != null ? Navigation.ToAbsoluteUri(customReturnUrl).ToString() : null;
|
||||
var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? Navigation.Uri);
|
||||
var redirectUrl = Navigation.ToAbsoluteUri($"{Options.AuthenticationPaths.LogInPath}?returnUrl={encodedReturnUrl}");
|
||||
return redirectUrl;
|
||||
}
|
||||
|
||||
private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false)
|
||||
private async Task<ClaimsPrincipal> GetUser(bool useCache = false)
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
if (useCache && now < _userLastCheck + _userCacheRefreshInterval)
|
||||
|
|
@ -196,56 +212,29 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
/// Gets the current authenticated used using JavaScript interop.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task{ClaimsPrincipal}"/>that will return the current authenticated user when completes.</returns>
|
||||
protected internal virtual async Task<ClaimsPrincipal> GetAuthenticatedUser()
|
||||
protected internal virtual async ValueTask<ClaimsPrincipal> GetAuthenticatedUser()
|
||||
{
|
||||
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(
|
||||
_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);
|
||||
return user;
|
||||
}
|
||||
|
||||
private async ValueTask EnsureAuthService()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("AuthenticationService.init", _options.ProviderOptions);
|
||||
await JsRuntime.InvokeVoidAsync("AuthenticationService.init", Options.ProviderOptions);
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUser(ValueTask<ClaimsPrincipal> task)
|
||||
private void UpdateUser(Task<ClaimsPrincipal> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,31 +21,35 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <typeparamref name="TRemoteAuthenticationState"/>.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||
public static IServiceCollection AddRemoteAuthentication<TRemoteAuthenticationState, TProviderOptions>(this IServiceCollection services)
|
||||
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, TProviderOptions>(this IServiceCollection services)
|
||||
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||
where TAccount : RemoteUserAccount
|
||||
where TProviderOptions : class, new()
|
||||
{
|
||||
services.AddOptions();
|
||||
services.AddAuthorizationCore();
|
||||
services.TryAddSingleton<AuthenticationStateProvider, RemoteAuthenticationService<TRemoteAuthenticationState, TProviderOptions>>();
|
||||
services.TryAddSingleton(sp =>
|
||||
services.TryAddScoped<AuthenticationStateProvider, RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions>>();
|
||||
services.TryAddScoped(sp =>
|
||||
{
|
||||
return (IRemoteAuthenticationService<TRemoteAuthenticationState>)sp.GetRequiredService<AuthenticationStateProvider>();
|
||||
});
|
||||
|
||||
services.TryAddSingleton(sp =>
|
||||
services.TryAddScoped(sp =>
|
||||
{
|
||||
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>
|
||||
|
|
@ -53,21 +57,23 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <typeparamref name="TRemoteAuthenticationState"/>.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||
public static IServiceCollection AddRemoteAuthentication<TRemoteAuthenticationState, TProviderOptions>(this IServiceCollection services, Action<RemoteAuthenticationOptions<TProviderOptions>> configure)
|
||||
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, TProviderOptions>(this IServiceCollection services, Action<RemoteAuthenticationOptions<TProviderOptions>> configure)
|
||||
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||
where TAccount : RemoteUserAccount
|
||||
where TProviderOptions : class, new()
|
||||
{
|
||||
services.AddRemoteAuthentication<RemoteAuthenticationState, TProviderOptions>();
|
||||
services.AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, TProviderOptions>();
|
||||
if (configure != null)
|
||||
{
|
||||
services.Configure(configure);
|
||||
}
|
||||
|
||||
return services;
|
||||
return new RemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>(services);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -76,7 +82,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||
public static IServiceCollection AddOidcAuthentication(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure)
|
||||
public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddOidcAuthentication(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> 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="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||
public static IServiceCollection AddOidcAuthentication<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()
|
||||
{
|
||||
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>());
|
||||
|
||||
return AddRemoteAuthentication<TRemoteAuthenticationState, OidcProviderOptions>(services, configure);
|
||||
return AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, OidcProviderOptions>(services, configure);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||
public static IServiceCollection AddApiAuthorization(this IServiceCollection services)
|
||||
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>
|
||||
|
|
@ -113,10 +133,35 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||
public static IServiceCollection AddApiAuthorization<TRemoteAuthenticationState>(this IServiceCollection services)
|
||||
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddApiAuthorization<TRemoteAuthenticationState>(this IServiceCollection services)
|
||||
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>
|
||||
|
|
@ -126,37 +171,41 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{ApiAuthorizationProviderOptions}"/>.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||
public static IServiceCollection AddApiAuthorization(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure)
|
||||
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>
|
||||
/// 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>
|
||||
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{ApiAuthorizationProviderOptions}"/>.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||
public static IServiceCollection AddApiAuthorization<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 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,
|
||||
Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure,
|
||||
string inferredClientId)
|
||||
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||
where TAccount : RemoteUserAccount
|
||||
{
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>, DefaultApiAuthorizationOptionsConfiguration>(_ =>
|
||||
new DefaultApiAuthorizationOptionsConfiguration(inferredClientId)));
|
||||
|
||||
services.AddRemoteAuthentication<TRemoteAuthenticationState, ApiAuthorizationProviderOptions>(configure);
|
||||
services.AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, ApiAuthorizationProviderOptions>(configure);
|
||||
|
||||
return services;
|
||||
return new RemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>(services);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.JSInterop;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||
|
|
@ -21,10 +24,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.SignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||
|
|
@ -51,10 +55,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.SignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||
|
|
@ -77,10 +82,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.CompleteSignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||
|
|
@ -107,10 +113,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.CompleteSignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||
|
|
@ -133,10 +140,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.SignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||
|
|
@ -163,10 +171,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.SignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||
|
|
@ -189,10 +198,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.CompleteSignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||
|
|
@ -219,10 +229,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.CompleteSignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||
|
|
@ -245,10 +256,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||
|
|
@ -282,10 +294,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||
|
|
@ -321,10 +334,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var state = new RemoteAuthenticationState();
|
||||
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||
|
|
@ -361,12 +375,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
testJsRuntime.GetUserResult = null;
|
||||
testJsRuntime.GetUserResult = default;
|
||||
|
||||
// Act
|
||||
var result = await runtime.GetAuthenticatedUser();
|
||||
|
|
@ -387,19 +402,22 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions();
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, CoolRoleAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new TestUserFactory(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var serializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true };
|
||||
var serializedUser = JsonSerializer.Serialize(new
|
||||
var account = new CoolRoleAccount
|
||||
{
|
||||
CoolName = "Alfred",
|
||||
CoolRole = new[] { "admin", "cool", "fantastic" }
|
||||
}, serializationOptions);
|
||||
CoolRole = new[] { "admin", "cool", "fantastic" },
|
||||
AdditionalProperties = new Dictionary<string, object>
|
||||
{
|
||||
["CoolName"] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize("Alfred"))
|
||||
}
|
||||
};
|
||||
|
||||
testJsRuntime.GetUserResult = JsonSerializer.Deserialize<IDictionary<string, object>>(serializedUser);
|
||||
testJsRuntime.GetUserResult = account;
|
||||
|
||||
// Act
|
||||
var result = await runtime.GetAuthenticatedUser();
|
||||
|
|
@ -420,19 +438,22 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
// Arrange
|
||||
var testJsRuntime = new TestJsRuntime();
|
||||
var options = CreateOptions("scope");
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, CoolRoleAccount, OidcProviderOptions>(
|
||||
testJsRuntime,
|
||||
options,
|
||||
new TestNavigationManager());
|
||||
new TestNavigationManager(),
|
||||
new TestUserFactory(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||
|
||||
var serializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true };
|
||||
var serializedUser = JsonSerializer.Serialize(new
|
||||
var account = new CoolRoleAccount
|
||||
{
|
||||
CoolName = "Alfred",
|
||||
CoolRole = new[] { "admin", "cool", "fantastic" }
|
||||
}, serializationOptions);
|
||||
CoolRole = new[] { "admin", "cool", "fantastic" },
|
||||
AdditionalProperties = new Dictionary<string, object>
|
||||
{
|
||||
["CoolName"] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize("Alfred")),
|
||||
}
|
||||
};
|
||||
|
||||
testJsRuntime.GetUserResult = JsonSerializer.Deserialize<IDictionary<string, object>>(serializedUser);
|
||||
testJsRuntime.GetUserResult = account;
|
||||
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||
{
|
||||
Status = "success",
|
||||
|
|
@ -509,22 +530,21 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
|
@ -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
|
||||
{
|
||||
public TestNavigationManager() =>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
|
@ -21,8 +22,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
public class RemoteAuthenticatorCoreTests
|
||||
{
|
||||
private const string _action = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.Action);
|
||||
private const string _onLogInSucceded = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.OnLogInSucceeded);
|
||||
private const string _onLogOutSucceeded = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.OnLogOutSucceeded);
|
||||
private const string _onLogInSucceded = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.OnLogInSucceeded);
|
||||
private const string _onLogOutSucceeded = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.OnLogOutSucceeded);
|
||||
|
||||
[Fact]
|
||||
public async Task AuthenticationManager_Throws_ForInvalidAction()
|
||||
|
|
@ -242,7 +243,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/");
|
||||
|
||||
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||
authServiceMock.GetAuthenticatedUserCallback = () => new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||
|
||||
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||
{
|
||||
|
|
@ -269,7 +270,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||
"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>()
|
||||
{
|
||||
|
|
@ -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 (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>()
|
||||
{
|
||||
|
|
@ -350,7 +351,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/fetchData");
|
||||
|
||||
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||
authServiceMock.GetAuthenticatedUserCallback = () => new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||
|
||||
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(
|
||||
IJSRuntime jsRuntime,
|
||||
IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options,
|
||||
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>>> SignOutCallback { 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 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.JSInterop.WebAssembly;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||
|
|
@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
var user = options.Value.UserOptions;
|
||||
Assert.Equal("Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests", user.AuthenticationType);
|
||||
Assert.Equal("scope", user.ScopeClaim);
|
||||
Assert.Null(user.RoleClaim);
|
||||
Assert.Equal("role", user.RoleClaim);
|
||||
Assert.Equal("name", user.NameClaim);
|
||||
|
||||
Assert.Equal(
|
||||
|
|
@ -69,6 +69,131 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
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]
|
||||
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);
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
public TestNavigationManager()
|
||||
|
|
@ -245,5 +431,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
|
||||
protected override void NavigateToCore(string uri, bool forceLoad) => throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
private class TestAuthenticationState : RemoteAuthenticationState
|
||||
{
|
||||
}
|
||||
|
||||
private class TestAccount : RemoteUserAccount
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,43 @@
|
|||
@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{
|
||||
[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)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,27 @@
|
|||
@page "/"
|
||||
|
||||
@inject StateService State
|
||||
@inject IJSRuntime JS
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
|
@ -13,7 +14,10 @@ namespace Wasm.Authentication.Client
|
|||
{
|
||||
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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,17 @@
|
|||
<span class="oi oi-list-rich" aria-hidden="true"></span> User
|
||||
</NavLink>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -21,5 +21,4 @@
|
|||
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,5 +13,21 @@ namespace Wasm.Authentication.Server.Data
|
|||
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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ using Wasm.Authentication.Server.Data;
|
|||
namespace Wasm.Authentication.Server.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20200123141439_Initial")]
|
||||
[Migration("20200324213904_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.0");
|
||||
.HasAnnotation("ProductVersion", "3.1.2");
|
||||
|
||||
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||
{
|
||||
|
|
@ -296,6 +296,26 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
|
|
@ -346,6 +366,13 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
|||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
|
@ -186,6 +186,25 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
|||
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(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
|
|
@ -243,6 +262,12 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
|||
name: "IX_PersistedGrants_SubjectId_ClientId_Type",
|
||||
table: "PersistedGrants",
|
||||
columns: new[] { "SubjectId", "ClientId", "Type" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserPreferences_ApplicationUserId",
|
||||
table: "UserPreferences",
|
||||
column: "ApplicationUserId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
|
|
@ -268,6 +293,9 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
|||
migrationBuilder.DropTable(
|
||||
name: "PersistedGrants");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserPreferences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
|||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.0");
|
||||
.HasAnnotation("ProductVersion", "3.1.2");
|
||||
|
||||
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||
{
|
||||
|
|
@ -294,6 +294,26 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
|
|
@ -344,6 +364,13 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
|||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ namespace Wasm.Authentication.Server.Models
|
|||
{
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
public UserPreference UserPreference { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -27,10 +30,17 @@ namespace Wasm.Authentication.Server
|
|||
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
|
||||
.AddRoles<IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>();
|
||||
|
||||
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()
|
||||
.AddIdentityServerJwt();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Wasm.Authentication.Server;
|
||||
using Wasm.Authentication.Server.Data;
|
||||
|
|
@ -28,7 +27,7 @@ using Xunit.Abstractions;
|
|||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||
{
|
||||
public class WebAssemblyAuthenticationTests : ServerTestBase<AspNetSiteServerFixture>, IDisposable
|
||||
public class WebAssemblyAuthenticationTests : ServerTestBase<AspNetSiteServerFixture>
|
||||
{
|
||||
private static readonly SqliteConnection _connection;
|
||||
|
||||
|
|
@ -55,15 +54,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
.Build();
|
||||
}
|
||||
|
||||
public override Task InitializeAsync() => base.InitializeAsync(Guid.NewGuid().ToString());
|
||||
|
||||
protected override void InitializeAsyncCore()
|
||||
{
|
||||
Browser.Navigate().GoToUrl("data:");
|
||||
Navigate("/", noReload: true);
|
||||
EnsureDatabaseCreated(_serverFixture.Host.Services);
|
||||
Browser.ExecuteJavaScript("sessionStorage.clear()");
|
||||
Browser.ExecuteJavaScript("localStorage.clear()");
|
||||
Browser.Manage().Cookies.DeleteAllCookies();
|
||||
Browser.Navigate().Refresh();
|
||||
WaitUntilLoaded();
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +85,50 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
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)
|
||||
{
|
||||
Browser.FindElement(link).Click();
|
||||
|
|
@ -103,6 +143,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var userName = $"{Guid.NewGuid()}@example.com";
|
||||
var password = $"!Test.Password1$";
|
||||
RegisterCore(userName, password);
|
||||
CompleteProfileDetails();
|
||||
|
||||
// Need to navigate to fetch page
|
||||
Browser.FindElement(By.PartialLinkText("Fetch data")).Click();
|
||||
|
|
@ -133,13 +174,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
.OrderBy(o => o.Item1)
|
||||
.ToArray();
|
||||
|
||||
Assert.Equal(4, claims.Length);
|
||||
Assert.Equal(5, claims.Length);
|
||||
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
("amr", "pwd"),
|
||||
("idp", "local"),
|
||||
("name", userName),
|
||||
("NewUser", "true"),
|
||||
("preferred_username", userName)
|
||||
},
|
||||
claims);
|
||||
|
|
@ -173,9 +215,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var userName = $"{Guid.NewGuid()}@example.com";
|
||||
var password = $"!Test.Password1$";
|
||||
RegisterCore(userName, password);
|
||||
CompleteProfileDetails();
|
||||
|
||||
Browser.Exists(By.PartialLinkText($"Hello, {userName}!")).Click();
|
||||
Browser.Contains("/Identity/Account/Manage", () => Browser.Url);
|
||||
ClickAndNavigate(By.PartialLinkText($"Hello, {userName}!"), "/Identity/Account/Manage");
|
||||
|
||||
Browser.Navigate().Back();
|
||||
Browser.Equal("/", () => new Uri(Browser.Url).PathAndQuery);
|
||||
|
|
@ -215,6 +257,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var userName = $"{Guid.NewGuid()}@example.com";
|
||||
var password = $"!Test.Password1$";
|
||||
RegisterCore(userName, password);
|
||||
CompleteProfileDetails();
|
||||
|
||||
ValidateLogout();
|
||||
}
|
||||
|
|
@ -227,6 +270,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var userName = $"{Guid.NewGuid()}@example.com";
|
||||
var password = $"!Test.Password1$";
|
||||
RegisterCore(userName, password);
|
||||
CompleteProfileDetails();
|
||||
|
||||
ValidateLogout();
|
||||
|
||||
|
|
@ -252,14 +296,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var userName = $"{Guid.NewGuid()}@example.com";
|
||||
var password = $"!Test.Password1$";
|
||||
RegisterCore(userName, password);
|
||||
CompleteProfileDetails();
|
||||
ValidateLoggedIn(userName);
|
||||
|
||||
// Clear the existing storage on the page and refresh
|
||||
Browser.ExecuteJavaScript("sessionStorage.clear()");
|
||||
Browser.Navigate().Refresh();
|
||||
Browser.Exists(By.PartialLinkText("Log in"));
|
||||
Browser.Exists(By.Id("test-clear-storage")).Click();
|
||||
Browser.Exists(By.Id("test-refresh-page")).Click();
|
||||
|
||||
Browser.FindElement(By.PartialLinkText("Log in")).Click();
|
||||
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);
|
||||
WaitUntilLoaded(skipHeader: true);
|
||||
Browser.Exists(By.CssSelector("[style=\"display: block;\"]"));
|
||||
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();
|
||||
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)
|
||||
|
|
@ -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
|
||||
{
|
||||
[JsonPropertyName("iss")]
|
||||
|
|
|
|||
Loading…
Reference in New Issue