[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)
|
public static IServiceCollection AddMsalAuthentication<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<MsalProviderOptions>> configure)
|
||||||
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||||
{
|
{
|
||||||
services.AddRemoteAuthentication<RemoteAuthenticationState, MsalProviderOptions>(configure);
|
AddMsalAuthentication<TRemoteAuthenticationState, RemoteUserAccount>(services, configure);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds authentication using msal.js to Blazor applications.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
|
||||||
|
/// <param name="configure">The <see cref="Action{RemoteAuthenticationOptions{MsalProviderOptions}}"/> to configure the <see cref="RemoteAuthenticationOptions{MsalProviderOptions}"/>.</param>
|
||||||
|
/// <returns>The <see cref="IServiceCollection"/>.</returns>
|
||||||
|
public static IServiceCollection AddMsalAuthentication<TRemoteAuthenticationState, TAccount>(this IServiceCollection services, Action<RemoteAuthenticationOptions<MsalProviderOptions>> configure)
|
||||||
|
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||||
|
where TAccount : RemoteUserAccount
|
||||||
|
{
|
||||||
|
services.AddRemoteAuthentication<RemoteAuthenticationState, TAccount, MsalProviderOptions>(configure);
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<MsalProviderOptions>>, MsalDefaultOptionsConfiguration>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<MsalProviderOptions>>, MsalDefaultOptionsConfiguration>());
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
export interface AuthenticationResult {
|
||||||
status: AuthenticationResultStatus;
|
status: AuthenticationResultStatus;
|
||||||
state?: any;
|
state?: unknown;
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthorizeService {
|
export interface AuthorizeService {
|
||||||
getUser(): Promise<any>;
|
getUser(): Promise<unknown>;
|
||||||
getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
|
getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
|
||||||
signIn(state: any): Promise<AuthenticationResult>;
|
signIn(state: unknown): Promise<AuthenticationResult>;
|
||||||
completeSignIn(state: any): Promise<AuthenticationResult>;
|
completeSignIn(state: unknown): Promise<AuthenticationResult>;
|
||||||
signOut(state: any): Promise<AuthenticationResult>;
|
signOut(state: unknown): Promise<AuthenticationResult>;
|
||||||
completeSignOut(url: string): Promise<AuthenticationResult>;
|
completeSignOut(url: string): Promise<AuthenticationResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class OidcAuthorizeService implements AuthorizeService {
|
class OidcAuthorizeService implements AuthorizeService {
|
||||||
private _userManager: UserManager;
|
private _userManager: UserManager;
|
||||||
|
private _intialSilentSignIn: Promise<void> | undefined;
|
||||||
constructor(userManager: UserManager) {
|
constructor(userManager: UserManager) {
|
||||||
this._userManager = userManager;
|
this._userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async trySilentSignIn() {
|
||||||
|
if (!this._intialSilentSignIn) {
|
||||||
|
this._intialSilentSignIn = (async () => {
|
||||||
|
try {
|
||||||
|
await this._userManager.signinSilent();
|
||||||
|
} catch (e) {
|
||||||
|
// It is ok to swallow the exception here.
|
||||||
|
// The user might not be logged in and in that case it
|
||||||
|
// is expected for signinSilent to fail and throw
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._intialSilentSignIn;
|
||||||
|
}
|
||||||
|
|
||||||
async getUser() {
|
async getUser() {
|
||||||
|
if (window.parent === window && !window.opener && !window.frameElement && this._userManager.settings.redirect_uri &&
|
||||||
|
!location.href.startsWith(this._userManager.settings.redirect_uri)) {
|
||||||
|
// If we are not inside a hidden iframe, try authenticating silently.
|
||||||
|
await AuthenticationService.instance.trySilentSignIn();
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this._userManager.getUser();
|
const user = await this._userManager.getUser();
|
||||||
return user && user.profile;
|
return user && user.profile;
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +142,7 @@ class OidcAuthorizeService implements AuthorizeService {
|
||||||
function hasAllScopes(request: AccessTokenRequestOptions | undefined, currentScopes: string[]) {
|
function hasAllScopes(request: AccessTokenRequestOptions | undefined, currentScopes: string[]) {
|
||||||
const set = new Set(currentScopes);
|
const set = new Set(currentScopes);
|
||||||
if (request && request.scopes) {
|
if (request && request.scopes) {
|
||||||
for (let current of request.scopes) {
|
for (const current of request.scopes) {
|
||||||
if (!set.has(current)) {
|
if (!set.has(current)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +153,7 @@ class OidcAuthorizeService implements AuthorizeService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signIn(state: any) {
|
async signIn(state: unknown) {
|
||||||
try {
|
try {
|
||||||
await this._userManager.clearStaleState();
|
await this._userManager.clearStaleState();
|
||||||
await this._userManager.signinSilent(this.createArguments());
|
await this._userManager.signinSilent(this.createArguments());
|
||||||
|
|
@ -166,7 +188,7 @@ class OidcAuthorizeService implements AuthorizeService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signOut(state: any) {
|
async signOut(state: unknown) {
|
||||||
try {
|
try {
|
||||||
if (!(await this._userManager.metadataService.getEndSessionEndpoint())) {
|
if (!(await this._userManager.metadataService.getEndSessionEndpoint())) {
|
||||||
await this._userManager.removeUser();
|
await this._userManager.removeUser();
|
||||||
|
|
@ -212,8 +234,8 @@ class OidcAuthorizeService implements AuthorizeService {
|
||||||
|
|
||||||
private async stateExists(url: string) {
|
private async stateExists(url: string) {
|
||||||
const stateParam = new URLSearchParams(new URL(url).search).get('state');
|
const stateParam = new URLSearchParams(new URL(url).search).get('state');
|
||||||
if (stateParam) {
|
if (stateParam && this._userManager.settings.stateStore) {
|
||||||
return await this._userManager.settings.stateStore!.get(stateParam);
|
return await this._userManager.settings.stateStore.get(stateParam);
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -221,15 +243,15 @@ class OidcAuthorizeService implements AuthorizeService {
|
||||||
|
|
||||||
private async loginRequired(url: string) {
|
private async loginRequired(url: string) {
|
||||||
const errorParameter = new URLSearchParams(new URL(url).search).get('error');
|
const errorParameter = new URLSearchParams(new URL(url).search).get('error');
|
||||||
if (errorParameter) {
|
if (errorParameter && this._userManager.settings.stateStore) {
|
||||||
const error = await this._userManager.settings.stateStore!.get(errorParameter);
|
const error = await this._userManager.settings.stateStore.get(errorParameter);
|
||||||
return error === 'login_required';
|
return error === 'login_required';
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createArguments(state?: any) {
|
private createArguments(state?: unknown) {
|
||||||
return { useReplaceToNavigate: true, data: state };
|
return { useReplaceToNavigate: true, data: state };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,7 +259,7 @@ class OidcAuthorizeService implements AuthorizeService {
|
||||||
return { status: AuthenticationResultStatus.Failure, errorMessage: message };
|
return { status: AuthenticationResultStatus.Failure, errorMessage: message };
|
||||||
}
|
}
|
||||||
|
|
||||||
private success(state: any) {
|
private success(state: unknown) {
|
||||||
return { status: AuthenticationResultStatus.Success, state };
|
return { status: AuthenticationResultStatus.Success, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,21 +275,57 @@ class OidcAuthorizeService implements AuthorizeService {
|
||||||
export class AuthenticationService {
|
export class AuthenticationService {
|
||||||
|
|
||||||
static _infrastructureKey = 'Microsoft.AspNetCore.Components.WebAssembly.Authentication';
|
static _infrastructureKey = 'Microsoft.AspNetCore.Components.WebAssembly.Authentication';
|
||||||
static _initialized : Promise<void>;
|
static _initialized: Promise<void>;
|
||||||
static instance: OidcAuthorizeService;
|
static instance: OidcAuthorizeService;
|
||||||
|
static _pendingOperations: { [key: string]: Promise<AuthenticationResult> | undefined } = {}
|
||||||
|
|
||||||
public static async init(settings: UserManagerSettings & AuthorizeServiceSettings) {
|
public static init(settings: UserManagerSettings & AuthorizeServiceSettings) {
|
||||||
// Multiple initializations can start concurrently and we want to avoid that.
|
// Multiple initializations can start concurrently and we want to avoid that.
|
||||||
// In order to do so, we create an initialization promise and the first call to init
|
// In order to do so, we create an initialization promise and the first call to init
|
||||||
// tries to initialize the app and sets up a promise other calls can await on.
|
// tries to initialize the app and sets up a promise other calls can await on.
|
||||||
if (!AuthenticationService._initialized) {
|
if (!AuthenticationService._initialized) {
|
||||||
this._initialized = (async () => {
|
AuthenticationService._initialized = AuthenticationService.initializeCore(settings);
|
||||||
const userManager = await this.createUserManager(settings);
|
|
||||||
AuthenticationService.instance = new OidcAuthorizeService(userManager);
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._initialized;
|
return AuthenticationService._initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static handleCallback() {
|
||||||
|
return AuthenticationService.initializeCore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async initializeCore(settings?: UserManagerSettings & AuthorizeServiceSettings) {
|
||||||
|
const finalSettings = settings || AuthenticationService.resolveCachedSettings();
|
||||||
|
if (!settings && finalSettings) {
|
||||||
|
const userManager = AuthenticationService.createUserManagerCore(finalSettings);
|
||||||
|
|
||||||
|
if (window.parent !== window && !window.opener && (window.frameElement && userManager.settings.redirect_uri &&
|
||||||
|
location.href.startsWith(userManager.settings.redirect_uri))) {
|
||||||
|
// If we are inside a hidden iframe, try completing the sign in early.
|
||||||
|
// This prevents loading the blazor app inside a hidden iframe, which speeds up the authentication operations
|
||||||
|
// and avoids wasting resources (CPU and memory from bootstrapping the Blazor app)
|
||||||
|
AuthenticationService.instance = new OidcAuthorizeService(userManager);
|
||||||
|
|
||||||
|
// This makes sure that if the blazor app has time to load inside the hidden iframe,
|
||||||
|
// it is not able to perform another auth operation until this operation has completed.
|
||||||
|
AuthenticationService._initialized = (async (): Promise<void> => {
|
||||||
|
await AuthenticationService.instance.completeSignIn(location.href);
|
||||||
|
return;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
} else if (settings) {
|
||||||
|
const userManager = await AuthenticationService.createUserManager(settings);
|
||||||
|
AuthenticationService.instance = new OidcAuthorizeService(userManager);
|
||||||
|
} else {
|
||||||
|
// HandleCallback gets called unconditionally, so we do nothing for normal paths.
|
||||||
|
// Cached settings are only used on handling the redirect_uri path and if the settings are not there
|
||||||
|
// the app will fallback to the default logic for handling the redirect.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static resolveCachedSettings(): UserManagerSettings | undefined {
|
||||||
|
const cachedSettings = window.sessionStorage.getItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`);
|
||||||
|
return cachedSettings ? JSON.parse(cachedSettings) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getUser() {
|
public static getUser() {
|
||||||
|
|
@ -278,37 +336,46 @@ export class AuthenticationService {
|
||||||
return AuthenticationService.instance.getAccessToken();
|
return AuthenticationService.instance.getAccessToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static signIn(state: any) {
|
public static signIn(state: unknown) {
|
||||||
return AuthenticationService.instance.signIn(state);
|
return AuthenticationService.instance.signIn(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static completeSignIn(url: string) {
|
public static async completeSignIn(url: string) {
|
||||||
return AuthenticationService.instance.completeSignIn(url);
|
let operation = this._pendingOperations[url];
|
||||||
|
if (!operation) {
|
||||||
|
operation = AuthenticationService.instance.completeSignIn(url);
|
||||||
|
await operation;
|
||||||
|
delete this._pendingOperations[url];
|
||||||
|
}
|
||||||
|
|
||||||
|
return operation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static signOut(state: any) {
|
public static signOut(state: unknown) {
|
||||||
return AuthenticationService.instance.signOut(state);
|
return AuthenticationService.instance.signOut(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static completeSignOut(url: string) {
|
public static async completeSignOut(url: string) {
|
||||||
return AuthenticationService.instance.completeSignOut(url);
|
let operation = this._pendingOperations[url];
|
||||||
|
if (!operation) {
|
||||||
|
operation = AuthenticationService.instance.completeSignOut(url);
|
||||||
|
await operation;
|
||||||
|
delete this._pendingOperations[url];
|
||||||
|
}
|
||||||
|
|
||||||
|
return operation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async createUserManager(settings: OidcAuthorizeServiceSettings): Promise<UserManager> {
|
private static async createUserManager(settings: OidcAuthorizeServiceSettings): Promise<UserManager> {
|
||||||
let finalSettings: UserManagerSettings;
|
let finalSettings: UserManagerSettings;
|
||||||
if (isApiAuthorizationSettings(settings)) {
|
if (isApiAuthorizationSettings(settings)) {
|
||||||
let response = await fetch(settings.configurationEndpoint);
|
const response = await fetch(settings.configurationEndpoint);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Could not load settings from '${settings.configurationEndpoint}'`);
|
throw new Error(`Could not load settings from '${settings.configurationEndpoint}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadedSettings = await response.json();
|
const downloadedSettings = await response.json();
|
||||||
|
|
||||||
window.sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`, JSON.stringify(settings));
|
|
||||||
|
|
||||||
downloadedSettings.automaticSilentRenew = true;
|
|
||||||
downloadedSettings.includeIdTokenInSilentRenew = true;
|
|
||||||
|
|
||||||
finalSettings = downloadedSettings;
|
finalSettings = downloadedSettings;
|
||||||
} else {
|
} else {
|
||||||
if (!settings.scope) {
|
if (!settings.scope) {
|
||||||
|
|
@ -323,18 +390,24 @@ export class AuthenticationService {
|
||||||
finalSettings = settings;
|
finalSettings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`, JSON.stringify(finalSettings));
|
||||||
|
|
||||||
|
return AuthenticationService.createUserManagerCore(finalSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createUserManagerCore(finalSettings: UserManagerSettings) {
|
||||||
const userManager = new UserManager(finalSettings);
|
const userManager = new UserManager(finalSettings);
|
||||||
|
|
||||||
userManager.events.addUserSignedOut(async () => {
|
userManager.events.addUserSignedOut(async () => {
|
||||||
await userManager.removeUser();
|
userManager.removeUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
return userManager;
|
return userManager;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window { AuthenticationService: AuthenticationService; }
|
interface Window { AuthenticationService: AuthenticationService }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AuthenticationService.handleCallback();
|
||||||
|
|
||||||
window.AuthenticationService = AuthenticationService;
|
window.AuthenticationService = AuthenticationService;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"build": "npm run build:release",
|
"build": "npm run build:release",
|
||||||
"build:release": "webpack --mode production --env.production --env.configuration=Release",
|
"build:release": "webpack --mode production --env.production --env.configuration=Release",
|
||||||
"build:debug": "webpack --mode development --env.configuration=Debug",
|
"build:debug": "webpack --mode development --env.configuration=Debug",
|
||||||
"watch": "webpack --watch --mode development"
|
"watch": "webpack --watch --mode development --env.configuration=Debug"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ts-loader": "^6.2.1",
|
"ts-loader": "^6.2.1",
|
||||||
|
|
|
||||||
|
|
@ -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.RemoteRegisterPath ??= "Identity/Account/Register";
|
||||||
options.AuthenticationPaths.RemoteProfilePath ??= "Identity/Account/Manage";
|
options.AuthenticationPaths.RemoteProfilePath ??= "Identity/Account/Manage";
|
||||||
options.UserOptions.ScopeClaim ??= "scope";
|
options.UserOptions.ScopeClaim ??= "scope";
|
||||||
|
options.UserOptions.RoleClaim ??= "role";
|
||||||
options.UserOptions.AuthenticationType ??= _applicationName;
|
options.UserOptions.AuthenticationType ??= _applicationName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
State = AuthenticationState
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (result.Status)
|
switch (result.Status)
|
||||||
{
|
{
|
||||||
case RemoteAuthenticationStatus.Redirect:
|
case RemoteAuthenticationStatus.Redirect:
|
||||||
break;
|
break;
|
||||||
case RemoteAuthenticationStatus.Success:
|
case RemoteAuthenticationStatus.Success:
|
||||||
await NavigateToReturnUrl(returnUrl);
|
await OnLogInSucceeded.InvokeAsync(result.State);
|
||||||
|
await NavigateToReturnUrl(GetReturnUrl(result.State, returnUrl));
|
||||||
break;
|
break;
|
||||||
case RemoteAuthenticationStatus.Failure:
|
case RemoteAuthenticationStatus.Failure:
|
||||||
var uri = Navigation.ToAbsoluteUri($"{ApplicationPaths.LogInFailedPath}?message={Uri.EscapeDataString(result.ErrorMessage)}").ToString();
|
var uri = Navigation.ToAbsoluteUri($"{ApplicationPaths.LogInFailedPath}?message={Uri.EscapeDataString(result.ErrorMessage)}").ToString();
|
||||||
|
|
@ -253,10 +255,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// is when we are doing a redirect sign in flow.
|
// is when we are doing a redirect sign in flow.
|
||||||
throw new InvalidOperationException("Should not redirect.");
|
throw new InvalidOperationException("Should not redirect.");
|
||||||
case RemoteAuthenticationStatus.Success:
|
case RemoteAuthenticationStatus.Success:
|
||||||
if (OnLogInSucceeded.HasDelegate)
|
await OnLogInSucceeded.InvokeAsync(result.State);
|
||||||
{
|
|
||||||
await OnLogInSucceeded.InvokeAsync(result.State);
|
|
||||||
}
|
|
||||||
await NavigateToReturnUrl(GetReturnUrl(result.State));
|
await NavigateToReturnUrl(GetReturnUrl(result.State));
|
||||||
break;
|
break;
|
||||||
case RemoteAuthenticationStatus.OperationCompleted:
|
case RemoteAuthenticationStatus.OperationCompleted:
|
||||||
|
|
@ -292,6 +291,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
case RemoteAuthenticationStatus.Redirect:
|
case RemoteAuthenticationStatus.Redirect:
|
||||||
break;
|
break;
|
||||||
case RemoteAuthenticationStatus.Success:
|
case RemoteAuthenticationStatus.Success:
|
||||||
|
await OnLogOutSucceeded.InvokeAsync(result.State);
|
||||||
await NavigateToReturnUrl(returnUrl);
|
await NavigateToReturnUrl(returnUrl);
|
||||||
break;
|
break;
|
||||||
case RemoteAuthenticationStatus.OperationCompleted:
|
case RemoteAuthenticationStatus.OperationCompleted:
|
||||||
|
|
@ -320,10 +320,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// is when we are doing a redirect sign in flow.
|
// is when we are doing a redirect sign in flow.
|
||||||
throw new InvalidOperationException("Should not redirect.");
|
throw new InvalidOperationException("Should not redirect.");
|
||||||
case RemoteAuthenticationStatus.Success:
|
case RemoteAuthenticationStatus.Success:
|
||||||
if (OnLogOutSucceeded.HasDelegate)
|
await OnLogOutSucceeded.InvokeAsync(result.State);
|
||||||
{
|
|
||||||
await OnLogOutSucceeded.InvokeAsync(result.State);
|
|
||||||
}
|
|
||||||
await NavigateToReturnUrl(GetReturnUrl(result.State, Navigation.ToAbsoluteUri(ApplicationPaths.LogOutSucceededPath).ToString()));
|
await NavigateToReturnUrl(GetReturnUrl(result.State, Navigation.ToAbsoluteUri(ApplicationPaths.LogOutSucceededPath).ToString()));
|
||||||
break;
|
break;
|
||||||
case RemoteAuthenticationStatus.OperationCompleted:
|
case RemoteAuthenticationStatus.OperationCompleted:
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -17,15 +16,15 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TRemoteAuthenticationState">The state to preserve across authentication operations.</typeparam>
|
/// <typeparam name="TRemoteAuthenticationState">The state to preserve across authentication operations.</typeparam>
|
||||||
/// <typeparam name="TProviderOptions">The options to be passed down to the underlying JavaScript library handling the authentication operations.</typeparam>
|
/// <typeparam name="TProviderOptions">The options to be passed down to the underlying JavaScript library handling the authentication operations.</typeparam>
|
||||||
public class RemoteAuthenticationService<TRemoteAuthenticationState, TProviderOptions> :
|
public class RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions> :
|
||||||
AuthenticationStateProvider,
|
AuthenticationStateProvider,
|
||||||
IRemoteAuthenticationService<TRemoteAuthenticationState>,
|
IRemoteAuthenticationService<TRemoteAuthenticationState>,
|
||||||
IAccessTokenProvider
|
IAccessTokenProvider
|
||||||
where TRemoteAuthenticationState : RemoteAuthenticationState
|
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||||
where TProviderOptions : new()
|
where TProviderOptions : new()
|
||||||
|
where TAccount : RemoteUserAccount
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan _userCacheRefreshInterval = TimeSpan.FromSeconds(60);
|
private static readonly TimeSpan _userCacheRefreshInterval = TimeSpan.FromSeconds(60);
|
||||||
|
|
||||||
private bool _initialized = false;
|
private bool _initialized = false;
|
||||||
|
|
||||||
// This defaults to 1/1/1970
|
// This defaults to 1/1/1970
|
||||||
|
|
@ -33,33 +32,42 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity());
|
private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.
|
/// Gets the <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly IJSRuntime _jsRuntime;
|
protected IJSRuntime JsRuntime { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="NavigationManager"/> used to compute absolute urls.
|
/// Gets the <see cref="NavigationManager"/> used to compute absolute urls.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly NavigationManager _navigation;
|
protected NavigationManager Navigation { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The options for the underlying JavaScript library handling the authentication operations.
|
/// Gets the <see cref="UserFactory{TAccount}"/> to map accounts to <see cref="ClaimsPrincipal"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly RemoteAuthenticationOptions<TProviderOptions> _options;
|
protected UserFactory<TAccount> UserFactory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the options for the underlying JavaScript library handling the authentication operations.
|
||||||
|
/// </summary>
|
||||||
|
protected RemoteAuthenticationOptions<TProviderOptions> Options { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="RemoteAuthenticationService{TRemoteAuthenticationState, TProviderOptions}"/>.
|
/// Initializes a new instance of <see cref="RemoteAuthenticationService{TRemoteAuthenticationState, TProviderOptions}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.</param>
|
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.</param>
|
||||||
/// <param name="options">The options to be passed down to the underlying JavaScript library handling the authentication operations.</param>
|
/// <param name="options">The options to be passed down to the underlying JavaScript library handling the authentication operations.</param>
|
||||||
|
/// <param name="navigation">The <see cref="NavigationManager"/> used to generate URLs.</param>
|
||||||
|
/// <param name="userFactory">The <see cref="UserFactory{TAccount}"/> used to generate the <see cref="ClaimsPrincipal"/> for the user.</param>
|
||||||
public RemoteAuthenticationService(
|
public RemoteAuthenticationService(
|
||||||
IJSRuntime jsRuntime,
|
IJSRuntime jsRuntime,
|
||||||
IOptions<RemoteAuthenticationOptions<TProviderOptions>> options,
|
IOptions<RemoteAuthenticationOptions<TProviderOptions>> options,
|
||||||
NavigationManager navigation)
|
NavigationManager navigation,
|
||||||
|
UserFactory<TAccount> userFactory)
|
||||||
{
|
{
|
||||||
_jsRuntime = jsRuntime;
|
JsRuntime = jsRuntime;
|
||||||
_navigation = navigation;
|
Navigation = navigation;
|
||||||
_options = options.Value;
|
UserFactory = userFactory;
|
||||||
|
Options = options.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -70,11 +78,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||||
{
|
{
|
||||||
await EnsureAuthService();
|
await EnsureAuthService();
|
||||||
var internalResult = await _jsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signIn", context.State);
|
var internalResult = await JsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signIn", context.State);
|
||||||
var result = internalResult.Convert();
|
var result = internalResult.Convert();
|
||||||
if (result.Status == RemoteAuthenticationStatus.Success)
|
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||||
{
|
{
|
||||||
UpdateUser(GetUser());
|
var getUserTask = GetUser();
|
||||||
|
await getUserTask;
|
||||||
|
UpdateUser(getUserTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -85,11 +95,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||||
{
|
{
|
||||||
await EnsureAuthService();
|
await EnsureAuthService();
|
||||||
var internalResult = await _jsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignIn", context.Url);
|
var internalResult = await JsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignIn", context.Url);
|
||||||
var result = internalResult.Convert();
|
var result = internalResult.Convert();
|
||||||
if (result.Status == RemoteAuthenticationStatus.Success)
|
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||||
{
|
{
|
||||||
UpdateUser(GetUser());
|
var getUserTask = GetUser();
|
||||||
|
await getUserTask;
|
||||||
|
UpdateUser(getUserTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -100,11 +112,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||||
{
|
{
|
||||||
await EnsureAuthService();
|
await EnsureAuthService();
|
||||||
var internalResult = await _jsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signOut", context.State);
|
var internalResult = await JsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signOut", context.State);
|
||||||
var result = internalResult.Convert();
|
var result = internalResult.Convert();
|
||||||
if (result.Status == RemoteAuthenticationStatus.Success)
|
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||||
{
|
{
|
||||||
UpdateUser(GetUser());
|
var getUserTask = GetUser();
|
||||||
|
await getUserTask;
|
||||||
|
UpdateUser(getUserTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -115,11 +129,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
|
||||||
{
|
{
|
||||||
await EnsureAuthService();
|
await EnsureAuthService();
|
||||||
var internalResult = await _jsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignOut", context.Url);
|
var internalResult = await JsRuntime.InvokeAsync<InternalRemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignOut", context.Url);
|
||||||
var result = internalResult.Convert();
|
var result = internalResult.Convert();
|
||||||
if (result.Status == RemoteAuthenticationStatus.Success)
|
if (result.Status == RemoteAuthenticationStatus.Success)
|
||||||
{
|
{
|
||||||
UpdateUser(GetUser());
|
var getUserTask = GetUser();
|
||||||
|
await getUserTask;
|
||||||
|
UpdateUser(getUserTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -129,7 +145,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
public virtual async ValueTask<AccessTokenResult> RequestAccessToken()
|
public virtual async ValueTask<AccessTokenResult> RequestAccessToken()
|
||||||
{
|
{
|
||||||
await EnsureAuthService();
|
await EnsureAuthService();
|
||||||
var result = await _jsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken");
|
var result = await JsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken");
|
||||||
|
|
||||||
if (!Enum.TryParse<AccessTokenResultStatus>(result.Status, ignoreCase: true, out var parsedStatus))
|
if (!Enum.TryParse<AccessTokenResultStatus>(result.Status, ignoreCase: true, out var parsedStatus))
|
||||||
{
|
{
|
||||||
|
|
@ -154,7 +170,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
}
|
}
|
||||||
|
|
||||||
await EnsureAuthService();
|
await EnsureAuthService();
|
||||||
var result = await _jsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken", options);
|
var result = await JsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken", options);
|
||||||
|
|
||||||
if (!Enum.TryParse<AccessTokenResultStatus>(result.Status, ignoreCase: true, out var parsedStatus))
|
if (!Enum.TryParse<AccessTokenResultStatus>(result.Status, ignoreCase: true, out var parsedStatus))
|
||||||
{
|
{
|
||||||
|
|
@ -172,13 +188,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
|
||||||
private Uri GetRedirectUrl(string customReturnUrl)
|
private Uri GetRedirectUrl(string customReturnUrl)
|
||||||
{
|
{
|
||||||
var returnUrl = customReturnUrl != null ? _navigation.ToAbsoluteUri(customReturnUrl).ToString() : null;
|
var returnUrl = customReturnUrl != null ? Navigation.ToAbsoluteUri(customReturnUrl).ToString() : null;
|
||||||
var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? _navigation.Uri);
|
var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? Navigation.Uri);
|
||||||
var redirectUrl = _navigation.ToAbsoluteUri($"{_options.AuthenticationPaths.LogInPath}?returnUrl={encodedReturnUrl}");
|
var redirectUrl = Navigation.ToAbsoluteUri($"{Options.AuthenticationPaths.LogInPath}?returnUrl={encodedReturnUrl}");
|
||||||
return redirectUrl;
|
return redirectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false)
|
private async Task<ClaimsPrincipal> GetUser(bool useCache = false)
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.Now;
|
var now = DateTimeOffset.Now;
|
||||||
if (useCache && now < _userLastCheck + _userCacheRefreshInterval)
|
if (useCache && now < _userLastCheck + _userCacheRefreshInterval)
|
||||||
|
|
@ -196,56 +212,29 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// Gets the current authenticated used using JavaScript interop.
|
/// Gets the current authenticated used using JavaScript interop.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A <see cref="Task{ClaimsPrincipal}"/>that will return the current authenticated user when completes.</returns>
|
/// <returns>A <see cref="Task{ClaimsPrincipal}"/>that will return the current authenticated user when completes.</returns>
|
||||||
protected internal virtual async Task<ClaimsPrincipal> GetAuthenticatedUser()
|
protected internal virtual async ValueTask<ClaimsPrincipal> GetAuthenticatedUser()
|
||||||
{
|
{
|
||||||
await EnsureAuthService();
|
await EnsureAuthService();
|
||||||
var user = await _jsRuntime.InvokeAsync<IDictionary<string, object>>("AuthenticationService.getUser");
|
var account = await JsRuntime.InvokeAsync<TAccount>("AuthenticationService.getUser");
|
||||||
|
var user = await UserFactory.CreateUserAsync(account, Options.UserOptions);
|
||||||
|
|
||||||
var identity = user != null ? new ClaimsIdentity(
|
return user;
|
||||||
_options.UserOptions.AuthenticationType,
|
|
||||||
_options.UserOptions.NameClaim,
|
|
||||||
_options.UserOptions.RoleClaim) : new ClaimsIdentity();
|
|
||||||
|
|
||||||
if (user != null)
|
|
||||||
{
|
|
||||||
foreach (var kvp in user)
|
|
||||||
{
|
|
||||||
var name = kvp.Key;
|
|
||||||
var value = kvp.Value;
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
if (value is JsonElement element && element.ValueKind == JsonValueKind.Array)
|
|
||||||
{
|
|
||||||
foreach (var item in element.EnumerateArray())
|
|
||||||
{
|
|
||||||
identity.AddClaim(new Claim(name, JsonSerializer.Deserialize<object>(item.GetRawText()).ToString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
identity.AddClaim(new Claim(name, value.ToString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ClaimsPrincipal(identity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask EnsureAuthService()
|
private async ValueTask EnsureAuthService()
|
||||||
{
|
{
|
||||||
if (!_initialized)
|
if (!_initialized)
|
||||||
{
|
{
|
||||||
await _jsRuntime.InvokeVoidAsync("AuthenticationService.init", _options.ProviderOptions);
|
await JsRuntime.InvokeVoidAsync("AuthenticationService.init", Options.ProviderOptions);
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateUser(ValueTask<ClaimsPrincipal> task)
|
private void UpdateUser(Task<ClaimsPrincipal> task)
|
||||||
{
|
{
|
||||||
NotifyAuthenticationStateChanged(UpdateAuthenticationState(task));
|
NotifyAuthenticationStateChanged(UpdateAuthenticationState(task));
|
||||||
|
|
||||||
static async Task<AuthenticationState> UpdateAuthenticationState(ValueTask<ClaimsPrincipal> futureUser) => new AuthenticationState(await futureUser);
|
static async Task<AuthenticationState> UpdateAuthenticationState(Task<ClaimsPrincipal> futureUser) => new AuthenticationState(await futureUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"/>.
|
/// <typeparamref name="TRemoteAuthenticationState"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam>
|
/// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam>
|
||||||
|
/// <typeparam name="TAccount">The account type.</typeparam>
|
||||||
/// <typeparam name="TProviderOptions">The configuration options of the underlying provider being used for handling the authentication operations.</typeparam>
|
/// <typeparam name="TProviderOptions">The configuration options of the underlying provider being used for handling the authentication operations.</typeparam>
|
||||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
public static IServiceCollection AddRemoteAuthentication<TRemoteAuthenticationState, TProviderOptions>(this IServiceCollection services)
|
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, TProviderOptions>(this IServiceCollection services)
|
||||||
where TRemoteAuthenticationState : RemoteAuthenticationState
|
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||||
|
where TAccount : RemoteUserAccount
|
||||||
where TProviderOptions : class, new()
|
where TProviderOptions : class, new()
|
||||||
{
|
{
|
||||||
services.AddOptions();
|
services.AddOptions();
|
||||||
services.AddAuthorizationCore();
|
services.AddAuthorizationCore();
|
||||||
services.TryAddSingleton<AuthenticationStateProvider, RemoteAuthenticationService<TRemoteAuthenticationState, TProviderOptions>>();
|
services.TryAddScoped<AuthenticationStateProvider, RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions>>();
|
||||||
services.TryAddSingleton(sp =>
|
services.TryAddScoped(sp =>
|
||||||
{
|
{
|
||||||
return (IRemoteAuthenticationService<TRemoteAuthenticationState>)sp.GetRequiredService<AuthenticationStateProvider>();
|
return (IRemoteAuthenticationService<TRemoteAuthenticationState>)sp.GetRequiredService<AuthenticationStateProvider>();
|
||||||
});
|
});
|
||||||
|
|
||||||
services.TryAddSingleton(sp =>
|
services.TryAddScoped(sp =>
|
||||||
{
|
{
|
||||||
return (IAccessTokenProvider)sp.GetRequiredService<AuthenticationStateProvider>();
|
return (IAccessTokenProvider)sp.GetRequiredService<AuthenticationStateProvider>();
|
||||||
});
|
});
|
||||||
|
|
||||||
services.TryAddSingleton<IRemoteAuthenticationPathsProvider, DefaultRemoteApplicationPathsProvider<TProviderOptions>>();
|
services.TryAddScoped<IRemoteAuthenticationPathsProvider, DefaultRemoteApplicationPathsProvider<TProviderOptions>>();
|
||||||
|
services.TryAddScoped<IAccessTokenProviderAccessor, AccessTokenProviderAccessor>();
|
||||||
|
services.TryAddScoped<SignOutSessionStateManager>();
|
||||||
|
|
||||||
services.TryAddSingleton<SignOutSessionStateManager>();
|
services.TryAddScoped<UserFactory<TAccount>>();
|
||||||
|
|
||||||
return services;
|
return new RemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>(services);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -53,21 +57,23 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
/// <typeparamref name="TRemoteAuthenticationState"/>.
|
/// <typeparamref name="TRemoteAuthenticationState"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam>
|
/// <typeparam name="TRemoteAuthenticationState">The state to be persisted across authentication operations.</typeparam>
|
||||||
|
/// <typeparam name="TAccount">The account type.</typeparam>
|
||||||
/// <typeparam name="TProviderOptions">The configuration options of the underlying provider being used for handling the authentication operations.</typeparam>
|
/// <typeparam name="TProviderOptions">The configuration options of the underlying provider being used for handling the authentication operations.</typeparam>
|
||||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
||||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
public static IServiceCollection AddRemoteAuthentication<TRemoteAuthenticationState, TProviderOptions>(this IServiceCollection services, Action<RemoteAuthenticationOptions<TProviderOptions>> configure)
|
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, TProviderOptions>(this IServiceCollection services, Action<RemoteAuthenticationOptions<TProviderOptions>> configure)
|
||||||
where TRemoteAuthenticationState : RemoteAuthenticationState
|
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||||
|
where TAccount : RemoteUserAccount
|
||||||
where TProviderOptions : class, new()
|
where TProviderOptions : class, new()
|
||||||
{
|
{
|
||||||
services.AddRemoteAuthentication<RemoteAuthenticationState, TProviderOptions>();
|
services.AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, TProviderOptions>();
|
||||||
if (configure != null)
|
if (configure != null)
|
||||||
{
|
{
|
||||||
services.Configure(configure);
|
services.Configure(configure);
|
||||||
}
|
}
|
||||||
|
|
||||||
return services;
|
return new RemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>(services);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -76,7 +82,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
||||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
public static IServiceCollection AddOidcAuthentication(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure)
|
public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddOidcAuthentication(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure)
|
||||||
{
|
{
|
||||||
return AddOidcAuthentication<RemoteAuthenticationState>(services, configure);
|
return AddOidcAuthentication<RemoteAuthenticationState>(services, configure);
|
||||||
}
|
}
|
||||||
|
|
@ -88,23 +94,37 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
||||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
public static IServiceCollection AddOidcAuthentication<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure)
|
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddOidcAuthentication<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure)
|
||||||
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||||
|
{
|
||||||
|
return AddOidcAuthentication<TRemoteAuthenticationState, RemoteUserAccount>(services, configure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds support for authentication for SPA applications using <see cref="OidcProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
|
||||||
|
/// <typeparam name="TAccount">The account type.</typeparam>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
|
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
|
||||||
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
|
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddOidcAuthentication<TRemoteAuthenticationState, TAccount>(this IServiceCollection services, Action<RemoteAuthenticationOptions<OidcProviderOptions>> configure)
|
||||||
|
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||||
|
where TAccount : RemoteUserAccount
|
||||||
{
|
{
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<OidcProviderOptions>>, DefaultOidcOptionsConfiguration>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<OidcProviderOptions>>, DefaultOidcOptionsConfiguration>());
|
||||||
|
|
||||||
return AddRemoteAuthentication<TRemoteAuthenticationState, OidcProviderOptions>(services, configure);
|
return AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, OidcProviderOptions>(services, configure);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
|
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
|
|
||||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
public static IServiceCollection AddApiAuthorization(this IServiceCollection services)
|
public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddApiAuthorization(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
return AddApiauthorizationCore<RemoteAuthenticationState>(services, configure: null, Assembly.GetCallingAssembly().GetName().Name);
|
return AddApiauthorizationCore<RemoteAuthenticationState, RemoteUserAccount>(services, configure: null, Assembly.GetCallingAssembly().GetName().Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -113,10 +133,35 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
|
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
|
||||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
public static IServiceCollection AddApiAuthorization<TRemoteAuthenticationState>(this IServiceCollection services)
|
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddApiAuthorization<TRemoteAuthenticationState>(this IServiceCollection services)
|
||||||
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||||
{
|
{
|
||||||
return AddApiauthorizationCore<TRemoteAuthenticationState>(services, configure: null, Assembly.GetCallingAssembly().GetName().Name);
|
return AddApiauthorizationCore<TRemoteAuthenticationState, RemoteUserAccount>(services, configure: null, Assembly.GetCallingAssembly().GetName().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
|
||||||
|
/// <typeparam name="TAccount">The account type.</typeparam>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
|
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddApiAuthorization<TRemoteAuthenticationState, TAccount>(this IServiceCollection services)
|
||||||
|
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||||
|
where TAccount : RemoteUserAccount
|
||||||
|
{
|
||||||
|
return AddApiauthorizationCore<TRemoteAuthenticationState, TAccount>(services, configure: null, Assembly.GetCallingAssembly().GetName().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
|
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{ApiAuthorizationProviderOptions}"/>.</param>
|
||||||
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
|
public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddApiAuthorization(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure)
|
||||||
|
{
|
||||||
|
return AddApiauthorizationCore<RemoteAuthenticationState, RemoteUserAccount>(services, configure, Assembly.GetCallingAssembly().GetName().Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -126,37 +171,41 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{ApiAuthorizationProviderOptions}"/>.</param>
|
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{ApiAuthorizationProviderOptions}"/>.</param>
|
||||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
public static IServiceCollection AddApiAuthorization(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure)
|
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddApiAuthorization<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure)
|
||||||
|
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||||
{
|
{
|
||||||
return AddApiauthorizationCore<RemoteAuthenticationState>(services, configure, Assembly.GetCallingAssembly().GetName().Name);
|
return AddApiauthorizationCore<TRemoteAuthenticationState, RemoteUserAccount>(services, configure, Assembly.GetCallingAssembly().GetName().Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
|
/// Adds support for authentication for SPA applications using <see cref="ApiAuthorizationProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
|
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
|
||||||
|
/// <typeparam name="TAccount">The account type.</typeparam>
|
||||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
|
||||||
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{ApiAuthorizationProviderOptions}"/>.</param>
|
/// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{ApiAuthorizationProviderOptions}"/>.</param>
|
||||||
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
/// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
|
||||||
public static IServiceCollection AddApiAuthorization<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure)
|
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddApiAuthorization<TRemoteAuthenticationState, TAccount>(this IServiceCollection services, Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure)
|
||||||
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||||
|
where TAccount : RemoteUserAccount
|
||||||
{
|
{
|
||||||
return AddApiauthorizationCore<TRemoteAuthenticationState>(services, configure, Assembly.GetCallingAssembly().GetName().Name);
|
return AddApiauthorizationCore<TRemoteAuthenticationState, TAccount>(services, configure, Assembly.GetCallingAssembly().GetName().Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IServiceCollection AddApiauthorizationCore<TRemoteAuthenticationState>(
|
private static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddApiauthorizationCore<TRemoteAuthenticationState, TAccount>(
|
||||||
IServiceCollection services,
|
IServiceCollection services,
|
||||||
Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure,
|
Action<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> configure,
|
||||||
string inferredClientId)
|
string inferredClientId)
|
||||||
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||||
|
where TAccount : RemoteUserAccount
|
||||||
{
|
{
|
||||||
services.TryAddEnumerable(
|
services.TryAddEnumerable(
|
||||||
ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>, DefaultApiAuthorizationOptionsConfiguration>(_ =>
|
ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>, DefaultApiAuthorizationOptionsConfiguration>(_ =>
|
||||||
new DefaultApiAuthorizationOptionsConfiguration(inferredClientId)));
|
new DefaultApiAuthorizationOptionsConfiguration(inferredClientId)));
|
||||||
|
|
||||||
services.AddRemoteAuthentication<TRemoteAuthenticationState, ApiAuthorizationProviderOptions>(configure);
|
services.AddRemoteAuthentication<TRemoteAuthenticationState, TAccount, ApiAuthorizationProviderOptions>(configure);
|
||||||
|
|
||||||
return services;
|
return new RemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>(services);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
|
@ -21,10 +24,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.SignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
testJsRuntime.SignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
|
@ -51,10 +55,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.SignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
testJsRuntime.SignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
|
@ -77,10 +82,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.CompleteSignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
testJsRuntime.CompleteSignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
|
@ -107,10 +113,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.CompleteSignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
testJsRuntime.CompleteSignInResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
|
@ -133,10 +140,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.SignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
testJsRuntime.SignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
|
@ -163,10 +171,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.SignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
testJsRuntime.SignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
|
@ -189,10 +198,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.CompleteSignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
testJsRuntime.CompleteSignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
|
@ -219,10 +229,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.CompleteSignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
testJsRuntime.CompleteSignOutResult = new InternalRemoteAuthenticationResult<RemoteAuthenticationState>
|
||||||
|
|
@ -245,10 +256,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||||
|
|
@ -282,10 +294,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||||
|
|
@ -321,10 +334,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var state = new RemoteAuthenticationState();
|
var state = new RemoteAuthenticationState();
|
||||||
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||||
|
|
@ -361,12 +375,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
testJsRuntime.GetUserResult = null;
|
testJsRuntime.GetUserResult = default;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await runtime.GetAuthenticatedUser();
|
var result = await runtime.GetAuthenticatedUser();
|
||||||
|
|
@ -387,19 +402,22 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions();
|
var options = CreateOptions();
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, CoolRoleAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new TestUserFactory(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var serializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true };
|
var account = new CoolRoleAccount
|
||||||
var serializedUser = JsonSerializer.Serialize(new
|
|
||||||
{
|
{
|
||||||
CoolName = "Alfred",
|
CoolRole = new[] { "admin", "cool", "fantastic" },
|
||||||
CoolRole = new[] { "admin", "cool", "fantastic" }
|
AdditionalProperties = new Dictionary<string, object>
|
||||||
}, serializationOptions);
|
{
|
||||||
|
["CoolName"] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize("Alfred"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
testJsRuntime.GetUserResult = JsonSerializer.Deserialize<IDictionary<string, object>>(serializedUser);
|
testJsRuntime.GetUserResult = account;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await runtime.GetAuthenticatedUser();
|
var result = await runtime.GetAuthenticatedUser();
|
||||||
|
|
@ -420,19 +438,22 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
// Arrange
|
// Arrange
|
||||||
var testJsRuntime = new TestJsRuntime();
|
var testJsRuntime = new TestJsRuntime();
|
||||||
var options = CreateOptions("scope");
|
var options = CreateOptions("scope");
|
||||||
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
|
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, CoolRoleAccount, OidcProviderOptions>(
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager());
|
new TestNavigationManager(),
|
||||||
|
new TestUserFactory(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var serializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true };
|
var account = new CoolRoleAccount
|
||||||
var serializedUser = JsonSerializer.Serialize(new
|
|
||||||
{
|
{
|
||||||
CoolName = "Alfred",
|
CoolRole = new[] { "admin", "cool", "fantastic" },
|
||||||
CoolRole = new[] { "admin", "cool", "fantastic" }
|
AdditionalProperties = new Dictionary<string, object>
|
||||||
}, serializationOptions);
|
{
|
||||||
|
["CoolName"] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize("Alfred")),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
testJsRuntime.GetUserResult = JsonSerializer.Deserialize<IDictionary<string, object>>(serializedUser);
|
testJsRuntime.GetUserResult = account;
|
||||||
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
testJsRuntime.GetAccessTokenResult = new InternalAccessTokenResult
|
||||||
{
|
{
|
||||||
Status = "success",
|
Status = "success",
|
||||||
|
|
@ -509,22 +530,21 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
|
||||||
public InternalAccessTokenResult GetAccessTokenResult { get; set; }
|
public InternalAccessTokenResult GetAccessTokenResult { get; set; }
|
||||||
|
|
||||||
public IDictionary<string, object> GetUserResult { get; set; }
|
public RemoteUserAccount GetUserResult { get; set; }
|
||||||
|
|
||||||
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
|
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
|
||||||
{
|
{
|
||||||
PastInvocations.Add((identifier, args));
|
PastInvocations.Add((identifier, args));
|
||||||
return new ValueTask<TValue>((TValue)GetInvocationResult<TValue>(identifier));
|
return new ValueTask<TValue>((TValue)GetInvocationResult(identifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args)
|
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args)
|
||||||
{
|
{
|
||||||
PastInvocations.Add((identifier, args));
|
PastInvocations.Add((identifier, args));
|
||||||
return new ValueTask<TValue>((TValue)GetInvocationResult<TValue>(identifier));
|
return new ValueTask<TValue>((TValue)GetInvocationResult(identifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
private object GetInvocationResult<TValue>(string identifier)
|
private object GetInvocationResult(string identifier)
|
||||||
{
|
{
|
||||||
switch (identifier)
|
switch (identifier)
|
||||||
{
|
{
|
||||||
|
|
@ -551,6 +571,35 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class TestUserFactory : UserFactory<CoolRoleAccount>
|
||||||
|
{
|
||||||
|
public TestUserFactory(IAccessTokenProviderAccessor accessor) : base(accessor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
|
||||||
|
CoolRoleAccount account,
|
||||||
|
RemoteAuthenticationUserOptions options)
|
||||||
|
{
|
||||||
|
var user = await base.CreateUserAsync(account, options);
|
||||||
|
|
||||||
|
if (account.CoolRole != null)
|
||||||
|
{
|
||||||
|
foreach (var role in account.CoolRole)
|
||||||
|
{
|
||||||
|
((ClaimsIdentity)user.Identity).AddClaim(new Claim("CoolRole", role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class CoolRoleAccount : RemoteUserAccount
|
||||||
|
{
|
||||||
|
public string[] CoolRole { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
internal class TestNavigationManager : NavigationManager
|
internal class TestNavigationManager : NavigationManager
|
||||||
{
|
{
|
||||||
public TestNavigationManager() =>
|
public TestNavigationManager() =>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Microsoft.AspNetCore.Components.RenderTree;
|
using Microsoft.AspNetCore.Components.RenderTree;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
@ -21,8 +22,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
public class RemoteAuthenticatorCoreTests
|
public class RemoteAuthenticatorCoreTests
|
||||||
{
|
{
|
||||||
private const string _action = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.Action);
|
private const string _action = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.Action);
|
||||||
private const string _onLogInSucceded = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.OnLogInSucceeded);
|
private const string _onLogInSucceded = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.OnLogInSucceeded);
|
||||||
private const string _onLogOutSucceeded = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.OnLogOutSucceeded);
|
private const string _onLogOutSucceeded = nameof(RemoteAuthenticatorViewCore<RemoteAuthenticationState>.OnLogOutSucceeded);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AuthenticationManager_Throws_ForInvalidAction()
|
public async Task AuthenticationManager_Throws_ForInvalidAction()
|
||||||
|
|
@ -242,7 +243,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/");
|
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/");
|
||||||
|
|
||||||
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
authServiceMock.GetAuthenticatedUserCallback = () => new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||||
|
|
||||||
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
{
|
{
|
||||||
|
|
@ -269,7 +270,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
"https://www.example.com/base/authentication/logout");
|
"https://www.example.com/base/authentication/logout");
|
||||||
|
|
||||||
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
authServiceMock.GetAuthenticatedUserCallback = () => new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||||
|
|
||||||
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
{
|
{
|
||||||
|
|
@ -296,7 +297,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
var originalUrl = "https://www.example.com/base/authentication/login?returnUrl=https://www.example.com/base/fetchData";
|
var originalUrl = "https://www.example.com/base/authentication/login?returnUrl=https://www.example.com/base/fetchData";
|
||||||
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(originalUrl);
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(originalUrl);
|
||||||
|
|
||||||
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
authServiceMock.GetAuthenticatedUserCallback = () => new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||||
|
|
||||||
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
{
|
{
|
||||||
|
|
@ -350,7 +351,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
var (remoteAuthenticator, renderer, authServiceMock, jsRuntime) = CreateAuthenticationManager(
|
||||||
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/fetchData");
|
"https://www.example.com/base/authentication/logout?returnUrl=https://www.example.com/base/fetchData");
|
||||||
|
|
||||||
authServiceMock.GetAuthenticatedUserCallback = () => Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
authServiceMock.GetAuthenticatedUserCallback = () => new ValueTask<ClaimsPrincipal>(new ClaimsPrincipal(new ClaimsIdentity("Test")));
|
||||||
|
|
||||||
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
authServiceMock.SignOutCallback = s => Task.FromResult(new RemoteAuthenticationResult<RemoteAuthenticationState>()
|
||||||
{
|
{
|
||||||
|
|
@ -660,13 +661,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestRemoteAuthenticationService : RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>
|
private class TestRemoteAuthenticationService : RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>
|
||||||
{
|
{
|
||||||
public TestRemoteAuthenticationService(
|
public TestRemoteAuthenticationService(
|
||||||
IJSRuntime jsRuntime,
|
IJSRuntime jsRuntime,
|
||||||
IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options,
|
IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options,
|
||||||
TestNavigationManager navigationManager) :
|
TestNavigationManager navigationManager) :
|
||||||
base(jsRuntime, options, navigationManager)
|
base(jsRuntime, options, navigationManager, new UserFactory<RemoteUserAccount>(Mock.Of<IAccessTokenProviderAccessor>()))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -674,13 +675,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> CompleteSignInCallback { get; set; }
|
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> CompleteSignInCallback { get; set; }
|
||||||
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> SignOutCallback { get; set; }
|
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> SignOutCallback { get; set; }
|
||||||
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> CompleteSignOutCallback { get; set; }
|
public Func<RemoteAuthenticationContext<RemoteAuthenticationState>, Task<RemoteAuthenticationResult<RemoteAuthenticationState>>> CompleteSignOutCallback { get; set; }
|
||||||
public Func<Task<ClaimsPrincipal>> GetAuthenticatedUserCallback { get; set; }
|
public Func<ValueTask<ClaimsPrincipal>> GetAuthenticatedUserCallback { get; set; }
|
||||||
|
|
||||||
public async override Task<AuthenticationState> GetAuthenticationStateAsync() => new AuthenticationState(await GetAuthenticatedUserCallback());
|
public async override Task<AuthenticationState> GetAuthenticationStateAsync() => new AuthenticationState(await GetAuthenticatedUserCallback());
|
||||||
|
|
||||||
public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> CompleteSignInAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => CompleteSignInCallback(context);
|
public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> CompleteSignInAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => CompleteSignInCallback(context);
|
||||||
|
|
||||||
protected internal override Task<ClaimsPrincipal> GetAuthenticatedUser() => GetAuthenticatedUserCallback();
|
protected internal override ValueTask<ClaimsPrincipal> GetAuthenticatedUser() => GetAuthenticatedUserCallback();
|
||||||
|
|
||||||
public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> CompleteSignOutAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => CompleteSignOutCallback(context);
|
public override Task<RemoteAuthenticationResult<RemoteAuthenticationState>> CompleteSignOutAsync(RemoteAuthenticationContext<RemoteAuthenticationState> context) => CompleteSignOutCallback(context);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.JSInterop.WebAssembly;
|
|
||||||
using Moq;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
|
@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
var user = options.Value.UserOptions;
|
var user = options.Value.UserOptions;
|
||||||
Assert.Equal("Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests", user.AuthenticationType);
|
Assert.Equal("Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests", user.AuthenticationType);
|
||||||
Assert.Equal("scope", user.ScopeClaim);
|
Assert.Equal("scope", user.ScopeClaim);
|
||||||
Assert.Null(user.RoleClaim);
|
Assert.Equal("role", user.RoleClaim);
|
||||||
Assert.Equal("name", user.NameClaim);
|
Assert.Equal("name", user.NameClaim);
|
||||||
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
|
|
@ -69,6 +69,131 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
options.Value.ProviderOptions.ConfigurationEndpoint);
|
options.Value.ProviderOptions.ConfigurationEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce()
|
||||||
|
{
|
||||||
|
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||||
|
var calls = 0;
|
||||||
|
builder.Services.AddApiAuthorization(options =>
|
||||||
|
{
|
||||||
|
calls++;
|
||||||
|
});
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
|
||||||
|
|
||||||
|
var user = options.Value.UserOptions;
|
||||||
|
Assert.Equal("Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests", user.AuthenticationType);
|
||||||
|
|
||||||
|
// Make sure that the defaults are applied on this overload
|
||||||
|
Assert.Equal("role", user.RoleClaim);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
|
||||||
|
options.Value.ProviderOptions.ConfigurationEndpoint);
|
||||||
|
|
||||||
|
Assert.Equal(1, calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration()
|
||||||
|
{
|
||||||
|
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||||
|
var calls = 0;
|
||||||
|
builder.Services.AddApiAuthorization<TestAuthenticationState>(options => calls++);
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
|
||||||
|
|
||||||
|
var user = options.Value.UserOptions;
|
||||||
|
// Make sure that the defaults are applied on this overload
|
||||||
|
Assert.Equal("role", user.RoleClaim);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
|
||||||
|
options.Value.ProviderOptions.ConfigurationEndpoint);
|
||||||
|
|
||||||
|
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
|
||||||
|
Assert.NotNull(authenticationService);
|
||||||
|
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, RemoteUserAccount, ApiAuthorizationProviderOptions>>(authenticationService);
|
||||||
|
|
||||||
|
Assert.Equal(1, calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfiguration()
|
||||||
|
{
|
||||||
|
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||||
|
builder.Services.AddApiAuthorization<TestAuthenticationState>();
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
|
||||||
|
|
||||||
|
var user = options.Value.UserOptions;
|
||||||
|
// Make sure that the defaults are applied on this overload
|
||||||
|
Assert.Equal("role", user.RoleClaim);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
|
||||||
|
options.Value.ProviderOptions.ConfigurationEndpoint);
|
||||||
|
|
||||||
|
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
|
||||||
|
Assert.NotNull(authenticationService);
|
||||||
|
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, RemoteUserAccount, ApiAuthorizationProviderOptions>>(authenticationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfiguration()
|
||||||
|
{
|
||||||
|
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||||
|
var calls = 0;
|
||||||
|
builder.Services.AddApiAuthorization<TestAuthenticationState, TestAccount>(options => calls++);
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
|
||||||
|
|
||||||
|
var user = options.Value.UserOptions;
|
||||||
|
// Make sure that the defaults are applied on this overload
|
||||||
|
Assert.Equal("role", user.RoleClaim);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
|
||||||
|
options.Value.ProviderOptions.ConfigurationEndpoint);
|
||||||
|
|
||||||
|
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
|
||||||
|
Assert.NotNull(authenticationService);
|
||||||
|
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, TestAccount, ApiAuthorizationProviderOptions>>(authenticationService);
|
||||||
|
|
||||||
|
Assert.Equal(1, calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpConfiguration()
|
||||||
|
{
|
||||||
|
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||||
|
builder.Services.AddApiAuthorization<TestAuthenticationState, TestAccount>();
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>>();
|
||||||
|
|
||||||
|
var user = options.Value.UserOptions;
|
||||||
|
// Make sure that the defaults are applied on this overload
|
||||||
|
Assert.Equal("role", user.RoleClaim);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"_configuration/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests",
|
||||||
|
options.Value.ProviderOptions.ConfigurationEndpoint);
|
||||||
|
|
||||||
|
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
|
||||||
|
Assert.NotNull(authenticationService);
|
||||||
|
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, TestAccount, ApiAuthorizationProviderOptions>>(authenticationService);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ApiAuthorizationOptions_DefaultsCanBeOverriden()
|
public void ApiAuthorizationOptions_DefaultsCanBeOverriden()
|
||||||
{
|
{
|
||||||
|
|
@ -236,6 +361,67 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
Assert.Equal("https://www.example.com/base/custom-logout", provider.PostLogoutRedirectUri);
|
Assert.Equal("https://www.example.com/base/custom-logout", provider.PostLogoutRedirectUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddOidc_ConfigurationGetsCalledOnce()
|
||||||
|
{
|
||||||
|
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||||
|
var calls = 0;
|
||||||
|
|
||||||
|
builder.Services.AddOidcAuthentication(options => calls++);
|
||||||
|
builder.Services.Replace(ServiceDescriptor.Singleton(typeof(NavigationManager), new TestNavigationManager()));
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>();
|
||||||
|
Assert.Equal("name", options.Value.UserOptions.NameClaim);
|
||||||
|
|
||||||
|
Assert.Equal(1, calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddOidc_CustomState_SetsUpConfiguration()
|
||||||
|
{
|
||||||
|
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||||
|
var calls = 0;
|
||||||
|
|
||||||
|
builder.Services.AddOidcAuthentication<TestAuthenticationState>(options => options.ProviderOptions.Authority = (++calls).ToString());
|
||||||
|
builder.Services.Replace(ServiceDescriptor.Singleton(typeof(NavigationManager), new TestNavigationManager()));
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>();
|
||||||
|
// Make sure options are applied
|
||||||
|
Assert.Equal("name", options.Value.UserOptions.NameClaim);
|
||||||
|
|
||||||
|
Assert.Equal("1", options.Value.ProviderOptions.Authority);
|
||||||
|
|
||||||
|
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
|
||||||
|
Assert.NotNull(authenticationService);
|
||||||
|
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, RemoteUserAccount, OidcProviderOptions>>(authenticationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddOidc_CustomStateAndAccount_SetsUpConfiguration()
|
||||||
|
{
|
||||||
|
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||||
|
var calls = 0;
|
||||||
|
|
||||||
|
builder.Services.AddOidcAuthentication<TestAuthenticationState, TestAccount>(options => options.ProviderOptions.Authority = (++calls).ToString());
|
||||||
|
builder.Services.Replace(ServiceDescriptor.Singleton(typeof(NavigationManager), new TestNavigationManager()));
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>();
|
||||||
|
// Make sure options are applied
|
||||||
|
Assert.Equal("name", options.Value.UserOptions.NameClaim);
|
||||||
|
|
||||||
|
Assert.Equal("1", options.Value.ProviderOptions.Authority);
|
||||||
|
|
||||||
|
var authenticationService = host.Services.GetService<IRemoteAuthenticationService<TestAuthenticationState>>();
|
||||||
|
Assert.NotNull(authenticationService);
|
||||||
|
Assert.IsType<RemoteAuthenticationService<TestAuthenticationState, TestAccount, OidcProviderOptions>>(authenticationService);
|
||||||
|
}
|
||||||
|
|
||||||
private class TestNavigationManager : NavigationManager
|
private class TestNavigationManager : NavigationManager
|
||||||
{
|
{
|
||||||
public TestNavigationManager()
|
public TestNavigationManager()
|
||||||
|
|
@ -245,5 +431,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
|
||||||
protected override void NavigateToCore(string uri, bool forceLoad) => throw new System.NotImplementedException();
|
protected override void NavigateToCore(string uri, bool forceLoad) => throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TestAuthenticationState : RemoteAuthenticationState
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestAccount : RemoteUserAccount
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
@page "/authentication/{action}"
|
||||||
|
@inject StateService State
|
||||||
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
<RemoteAuthenticatorView Action="@Action" />
|
<RemoteAuthenticatorViewCore TAuthenticationState="RemoteAppState"
|
||||||
|
AuthenticationState="AppState"
|
||||||
|
OnLogInSucceeded="CompleteLogin"
|
||||||
|
Action="@Action" />
|
||||||
|
|
||||||
@code{
|
@code{
|
||||||
[Parameter] public string Action { get; set; }
|
[Parameter] public string Action { get; set; }
|
||||||
|
|
||||||
|
public RemoteAppState AppState { get; set; } = new RemoteAppState();
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn, Action))
|
||||||
|
{
|
||||||
|
AppState.State = State.GetCurrentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CompleteLogin(RemoteAppState remoteState)
|
||||||
|
{
|
||||||
|
if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogInCallback, Action))
|
||||||
|
{
|
||||||
|
State.RestoreCurrentState(remoteState.State);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||||
|
var user = userState.User;
|
||||||
|
|
||||||
|
if (user.HasClaim("NewUser", "true"))
|
||||||
|
{
|
||||||
|
var originalReturnUrl = remoteState.ReturnUrl;
|
||||||
|
var preferencesUrl = Navigation.ToAbsoluteUri("preferences");
|
||||||
|
remoteState.ReturnUrl = $"{preferencesUrl}?returnUrl={Uri.EscapeDataString(originalReturnUrl)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,27 @@
|
||||||
@page "/"
|
@page "/"
|
||||||
|
@inject StateService State
|
||||||
|
@inject IJSRuntime JS
|
||||||
<h1>Hello, world!</h1>
|
<h1>Hello, world!</h1>
|
||||||
|
|
||||||
Welcome to your new app.
|
Welcome to your new app.
|
||||||
|
|
||||||
|
Current state is:
|
||||||
|
<p id="app-state">@State.GetCurrentState()</p>
|
||||||
|
|
||||||
|
<!-- Elements to help testing functionality -->
|
||||||
|
<p id="test-helpers">
|
||||||
|
<button id="test-clear-storage" @onclick="ClearStorage">Clear storage</button>
|
||||||
|
<button id="test-refresh-page" @onclick="TriggerPageRefresh">Refresh page</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@code{
|
||||||
|
public async Task ClearStorage()
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("sessionStorage.clear");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task TriggerPageRefresh()
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("location.reload", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
|
@ -13,7 +14,10 @@ namespace Wasm.Authentication.Client
|
||||||
{
|
{
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
builder.Services.AddApiAuthorization();
|
builder.Services.AddApiAuthorization<RemoteAppState, OidcAccount>()
|
||||||
|
.AddUserFactory<RemoteAppState, OidcAccount, PreferencesUserFactory>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<StateService>();
|
||||||
|
|
||||||
builder.RootComponents.Add<App>("app");
|
builder.RootComponents.Add<App>("app");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
<span class="oi oi-list-rich" aria-hidden="true"></span> User
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="new-admin">
|
||||||
|
<span class="oi oi-list-rich" aria-hidden="true"></span> Make admin
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="admin-settings">
|
||||||
|
<span class="oi oi-list-rich" aria-hidden="true"></span> Settings
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
|
||||||
<script src="_framework/blazor.webassembly.js"></script>
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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
|
namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
[Migration("20200123141439_Initial")]
|
[Migration("20200324213904_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "3.1.0");
|
.HasAnnotation("ProductVersion", "3.1.2");
|
||||||
|
|
||||||
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
|
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||||
{
|
{
|
||||||
|
|
@ -296,6 +296,26 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
b.ToTable("AspNetUsers");
|
b.ToTable("AspNetUsers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Wasm.Authentication.Server.Models.UserPreference", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ApplicationUserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Color")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ApplicationUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("UserPreferences");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
|
@ -346,6 +366,13 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Wasm.Authentication.Server.Models.UserPreference", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
|
||||||
|
.WithOne("UserPreference")
|
||||||
|
.HasForeignKey("Wasm.Authentication.Server.Models.UserPreference", "ApplicationUserId");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,6 +186,25 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserPreferences",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(nullable: false),
|
||||||
|
ApplicationUserId = table.Column<string>(nullable: true),
|
||||||
|
Color = table.Column<string>(nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserPreferences", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserPreferences_AspNetUsers_ApplicationUserId",
|
||||||
|
column: x => x.ApplicationUserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_AspNetRoleClaims_RoleId",
|
name: "IX_AspNetRoleClaims_RoleId",
|
||||||
table: "AspNetRoleClaims",
|
table: "AspNetRoleClaims",
|
||||||
|
|
@ -243,6 +262,12 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
name: "IX_PersistedGrants_SubjectId_ClientId_Type",
|
name: "IX_PersistedGrants_SubjectId_ClientId_Type",
|
||||||
table: "PersistedGrants",
|
table: "PersistedGrants",
|
||||||
columns: new[] { "SubjectId", "ClientId", "Type" });
|
columns: new[] { "SubjectId", "ClientId", "Type" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserPreferences_ApplicationUserId",
|
||||||
|
table: "UserPreferences",
|
||||||
|
column: "ApplicationUserId",
|
||||||
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
|
@ -268,6 +293,9 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "PersistedGrants");
|
name: "PersistedGrants");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserPreferences");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "AspNetRoles");
|
name: "AspNetRoles");
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "3.1.0");
|
.HasAnnotation("ProductVersion", "3.1.2");
|
||||||
|
|
||||||
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
|
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||||
{
|
{
|
||||||
|
|
@ -294,6 +294,26 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
b.ToTable("AspNetUsers");
|
b.ToTable("AspNetUsers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Wasm.Authentication.Server.Models.UserPreference", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ApplicationUserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Color")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ApplicationUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("UserPreferences");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
|
@ -344,6 +364,13 @@ namespace Wasm.Authentication.Server.Data.Migrations
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Wasm.Authentication.Server.Models.UserPreference", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Wasm.Authentication.Server.Models.ApplicationUser", null)
|
||||||
|
.WithOne("UserPreference")
|
||||||
|
.HasForeignKey("Wasm.Authentication.Server.Models.UserPreference", "ApplicationUserId");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,6 @@ namespace Wasm.Authentication.Server.Models
|
||||||
{
|
{
|
||||||
public class ApplicationUser : IdentityUser
|
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.Authentication;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
@ -27,10 +30,17 @@ namespace Wasm.Authentication.Server
|
||||||
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
|
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
|
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
|
||||||
|
.AddRoles<IdentityRole>()
|
||||||
.AddEntityFrameworkStores<ApplicationDbContext>();
|
.AddEntityFrameworkStores<ApplicationDbContext>();
|
||||||
|
|
||||||
services.AddIdentityServer()
|
services.AddIdentityServer()
|
||||||
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
|
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
|
||||||
|
options.IdentityResources["openid"].UserClaims.Add("role");
|
||||||
|
options.ApiResources.Single().UserClaims.Add("role");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Need to do this as it maps "role" to ClaimTypes.Role and causes issues
|
||||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
|
||||||
|
|
||||||
services.AddAuthentication()
|
services.AddAuthentication()
|
||||||
.AddIdentityServerJwt();
|
.AddIdentityServerJwt();
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using OpenQA.Selenium;
|
using OpenQA.Selenium;
|
||||||
using OpenQA.Selenium.Support.Extensions;
|
|
||||||
using OpenQA.Selenium.Support.UI;
|
using OpenQA.Selenium.Support.UI;
|
||||||
using Wasm.Authentication.Server;
|
using Wasm.Authentication.Server;
|
||||||
using Wasm.Authentication.Server.Data;
|
using Wasm.Authentication.Server.Data;
|
||||||
|
|
@ -28,7 +27,7 @@ using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
{
|
{
|
||||||
public class WebAssemblyAuthenticationTests : ServerTestBase<AspNetSiteServerFixture>, IDisposable
|
public class WebAssemblyAuthenticationTests : ServerTestBase<AspNetSiteServerFixture>
|
||||||
{
|
{
|
||||||
private static readonly SqliteConnection _connection;
|
private static readonly SqliteConnection _connection;
|
||||||
|
|
||||||
|
|
@ -55,15 +54,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Task InitializeAsync() => base.InitializeAsync(Guid.NewGuid().ToString());
|
||||||
|
|
||||||
protected override void InitializeAsyncCore()
|
protected override void InitializeAsyncCore()
|
||||||
{
|
{
|
||||||
Browser.Navigate().GoToUrl("data:");
|
|
||||||
Navigate("/", noReload: true);
|
Navigate("/", noReload: true);
|
||||||
EnsureDatabaseCreated(_serverFixture.Host.Services);
|
EnsureDatabaseCreated(_serverFixture.Host.Services);
|
||||||
Browser.ExecuteJavaScript("sessionStorage.clear()");
|
|
||||||
Browser.ExecuteJavaScript("localStorage.clear()");
|
|
||||||
Browser.Manage().Cookies.DeleteAllCookies();
|
|
||||||
Browser.Navigate().Refresh();
|
|
||||||
WaitUntilLoaded();
|
WaitUntilLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,6 +85,50 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
ValidateFetchData();
|
ValidateFetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanPreserveApplicationState_DuringLogIn()
|
||||||
|
{
|
||||||
|
var originalAppState = Browser.Exists(By.Id("app-state")).Text;
|
||||||
|
|
||||||
|
var link = By.PartialLinkText("Fetch data");
|
||||||
|
var page = "/Identity/Account/Login";
|
||||||
|
|
||||||
|
ClickAndNavigate(link, page);
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
|
||||||
|
FirstTimeRegister(userName, password);
|
||||||
|
|
||||||
|
ValidateFetchData();
|
||||||
|
|
||||||
|
var homeLink = By.PartialLinkText("Home");
|
||||||
|
var homePage = "/";
|
||||||
|
ClickAndNavigate(homeLink, homePage);
|
||||||
|
|
||||||
|
var restoredAppState = Browser.Exists(By.Id("app-state")).Text;
|
||||||
|
Assert.Equal(originalAppState, restoredAppState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanShareUserRolesBetweenClientAndServer()
|
||||||
|
{
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Log in"), "/Identity/Account/Login");
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
FirstTimeRegister(userName, password);
|
||||||
|
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Make admin"), "/new-admin");
|
||||||
|
|
||||||
|
ClickAndNavigate(By.PartialLinkText("Settings"), "/admin-settings");
|
||||||
|
|
||||||
|
Browser.Exists(By.Id("admin-action")).Click();
|
||||||
|
|
||||||
|
Browser.Exists(By.Id("admin-success"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void ClickAndNavigate(By link, string page)
|
private void ClickAndNavigate(By link, string page)
|
||||||
{
|
{
|
||||||
Browser.FindElement(link).Click();
|
Browser.FindElement(link).Click();
|
||||||
|
|
@ -103,6 +143,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
var userName = $"{Guid.NewGuid()}@example.com";
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
var password = $"!Test.Password1$";
|
var password = $"!Test.Password1$";
|
||||||
RegisterCore(userName, password);
|
RegisterCore(userName, password);
|
||||||
|
CompleteProfileDetails();
|
||||||
|
|
||||||
// Need to navigate to fetch page
|
// Need to navigate to fetch page
|
||||||
Browser.FindElement(By.PartialLinkText("Fetch data")).Click();
|
Browser.FindElement(By.PartialLinkText("Fetch data")).Click();
|
||||||
|
|
@ -133,13 +174,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
.OrderBy(o => o.Item1)
|
.OrderBy(o => o.Item1)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
Assert.Equal(4, claims.Length);
|
Assert.Equal(5, claims.Length);
|
||||||
|
|
||||||
Assert.Equal(new[]
|
Assert.Equal(new[]
|
||||||
{
|
{
|
||||||
("amr", "pwd"),
|
("amr", "pwd"),
|
||||||
("idp", "local"),
|
("idp", "local"),
|
||||||
("name", userName),
|
("name", userName),
|
||||||
|
("NewUser", "true"),
|
||||||
("preferred_username", userName)
|
("preferred_username", userName)
|
||||||
},
|
},
|
||||||
claims);
|
claims);
|
||||||
|
|
@ -173,9 +215,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
var userName = $"{Guid.NewGuid()}@example.com";
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
var password = $"!Test.Password1$";
|
var password = $"!Test.Password1$";
|
||||||
RegisterCore(userName, password);
|
RegisterCore(userName, password);
|
||||||
|
CompleteProfileDetails();
|
||||||
|
|
||||||
Browser.Exists(By.PartialLinkText($"Hello, {userName}!")).Click();
|
ClickAndNavigate(By.PartialLinkText($"Hello, {userName}!"), "/Identity/Account/Manage");
|
||||||
Browser.Contains("/Identity/Account/Manage", () => Browser.Url);
|
|
||||||
|
|
||||||
Browser.Navigate().Back();
|
Browser.Navigate().Back();
|
||||||
Browser.Equal("/", () => new Uri(Browser.Url).PathAndQuery);
|
Browser.Equal("/", () => new Uri(Browser.Url).PathAndQuery);
|
||||||
|
|
@ -215,6 +257,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
var userName = $"{Guid.NewGuid()}@example.com";
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
var password = $"!Test.Password1$";
|
var password = $"!Test.Password1$";
|
||||||
RegisterCore(userName, password);
|
RegisterCore(userName, password);
|
||||||
|
CompleteProfileDetails();
|
||||||
|
|
||||||
ValidateLogout();
|
ValidateLogout();
|
||||||
}
|
}
|
||||||
|
|
@ -227,6 +270,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
var userName = $"{Guid.NewGuid()}@example.com";
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
var password = $"!Test.Password1$";
|
var password = $"!Test.Password1$";
|
||||||
RegisterCore(userName, password);
|
RegisterCore(userName, password);
|
||||||
|
CompleteProfileDetails();
|
||||||
|
|
||||||
ValidateLogout();
|
ValidateLogout();
|
||||||
|
|
||||||
|
|
@ -252,14 +296,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
var userName = $"{Guid.NewGuid()}@example.com";
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
var password = $"!Test.Password1$";
|
var password = $"!Test.Password1$";
|
||||||
RegisterCore(userName, password);
|
RegisterCore(userName, password);
|
||||||
|
CompleteProfileDetails();
|
||||||
ValidateLoggedIn(userName);
|
ValidateLoggedIn(userName);
|
||||||
|
|
||||||
// Clear the existing storage on the page and refresh
|
// Clear the existing storage on the page and refresh
|
||||||
Browser.ExecuteJavaScript("sessionStorage.clear()");
|
Browser.Exists(By.Id("test-clear-storage")).Click();
|
||||||
Browser.Navigate().Refresh();
|
Browser.Exists(By.Id("test-refresh-page")).Click();
|
||||||
Browser.Exists(By.PartialLinkText("Log in"));
|
|
||||||
|
|
||||||
Browser.FindElement(By.PartialLinkText("Log in")).Click();
|
|
||||||
ValidateLoggedIn(userName);
|
ValidateLoggedIn(userName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,6 +311,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
{
|
{
|
||||||
Browser.Navigate().GoToUrl(new Uri(new Uri(Browser.Url), "/authentication/login?returnUrl=https%3A%2F%2Fwww.bing.com").AbsoluteUri);
|
Browser.Navigate().GoToUrl(new Uri(new Uri(Browser.Url), "/authentication/login?returnUrl=https%3A%2F%2Fwww.bing.com").AbsoluteUri);
|
||||||
WaitUntilLoaded(skipHeader: true);
|
WaitUntilLoaded(skipHeader: true);
|
||||||
|
Browser.Exists(By.CssSelector("[style=\"display: block;\"]"));
|
||||||
Assert.NotEmpty(Browser.GetBrowserLogs(LogLevel.Severe));
|
Assert.NotEmpty(Browser.GetBrowserLogs(LogLevel.Severe));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,6 +366,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
{
|
{
|
||||||
Browser.FindElement(By.PartialLinkText("Register as a new user")).Click();
|
Browser.FindElement(By.PartialLinkText("Register as a new user")).Click();
|
||||||
RegisterCore(userName, password);
|
RegisterCore(userName, password);
|
||||||
|
CompleteProfileDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteProfileDetails()
|
||||||
|
{
|
||||||
|
Browser.Exists(By.PartialLinkText("Home"));
|
||||||
|
Browser.Contains("/preferences", () => Browser.Url);
|
||||||
|
Browser.FindElement(By.Id("color-preference")).SendKeys("Red");
|
||||||
|
Browser.FindElement(By.Id("submit-preference")).Click();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterCore(string userName, string password)
|
private void RegisterCore(string userName, string password)
|
||||||
|
|
@ -410,13 +463,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// Make the tests run faster by navigating back to the home page when we are done
|
|
||||||
// If we don't, then the next test will reload the whole page before it starts
|
|
||||||
Browser.FindElement(By.LinkText("Home")).Click();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class JwtPayload
|
private class JwtPayload
|
||||||
{
|
{
|
||||||
[JsonPropertyName("iss")]
|
[JsonPropertyName("iss")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue