Skip to content

Commit 22934f9

Browse files
committed
Allow customization of the user
1 parent 18a196b commit 22934f9

15 files changed

+430
-94
lines changed

src/Components/WebAssembly/Authentication.Msal/src/MsalWebAssemblyServiceCollectionExtensions.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,22 @@ public static IServiceCollection AddMsalAuthentication(this IServiceCollection s
3737
public static IServiceCollection AddMsalAuthentication<TRemoteAuthenticationState>(this IServiceCollection services, Action<RemoteAuthenticationOptions<MsalProviderOptions>> configure)
3838
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
3939
{
40-
services.AddRemoteAuthentication<RemoteAuthenticationState, MsalProviderOptions>(configure);
40+
AddMsalAuthentication<TRemoteAuthenticationState, RemoteUserAccount>(services, configure);
41+
return services;
42+
}
43+
44+
/// <summary>
45+
/// Adds authentication using msal.js to Blazor applications.
46+
/// </summary>
47+
/// <typeparam name="TRemoteAuthenticationState">The type of the remote authentication state.</typeparam>
48+
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
49+
/// <param name="configure">The <see cref="Action{RemoteAuthenticationOptions{MsalProviderOptions}}"/> to configure the <see cref="RemoteAuthenticationOptions{MsalProviderOptions}"/>.</param>
50+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
51+
public static IServiceCollection AddMsalAuthentication<TRemoteAuthenticationState, TAccount>(this IServiceCollection services, Action<RemoteAuthenticationOptions<MsalProviderOptions>> configure)
52+
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
53+
where TAccount : RemoteUserAccount
54+
{
55+
services.AddRemoteAuthentication<RemoteAuthenticationState, TAccount, MsalProviderOptions>(configure);
4156
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<MsalProviderOptions>>, MsalDefaultOptionsConfiguration>());
4257

4358
return services;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal
5+
{
6+
/// <summary>
7+
/// This is an internal API that supports the Microsoft.AspNetCore.Components.WebAssembly.Authentication
8+
/// infrastructure and not subject to the same compatibility standards as public APIs.
9+
/// It may be changed or removed without notice in any release.
10+
/// </summary>
11+
public interface IAccessTokenProviderAccessor
12+
{
13+
/// <summary>
14+
/// This is an internal API that supports the Microsoft.AspNetCore.Components.WebAssembly.Authentication
15+
/// infrastructure and not subject to the same compatibility standards as public APIs.
16+
/// It may be changed or removed without notice in any release.
17+
/// </summary>
18+
IAccessTokenProvider TokenProvider { get; }
19+
}
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
5+
6+
namespace Microsoft.Extensions.DependencyInjection
7+
{
8+
/// <summary>
9+
/// An interface for configuring remote authentication services.
10+
/// </summary>
11+
/// <typeparam name="TRemoteAuthenticationState">The remote authentication state type.</typeparam>
12+
/// <typeparam name="TAccount">The account type.</typeparam>
13+
public interface IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>
14+
where TRemoteAuthenticationState : RemoteAuthenticationState
15+
where TAccount : RemoteUserAccount
16+
{
17+
IServiceCollection Services { get; }
18+
}
19+
}

src/Components/WebAssembly/WebAssembly.Authentication/src/Interop/AuthenticationService.ts

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,32 @@ export interface AuthorizeService {
6363

6464
class OidcAuthorizeService implements AuthorizeService {
6565
private _userManager: UserManager;
66-
66+
private _intialSilentSignIn: Promise<void> | undefined;
6767
constructor(userManager: UserManager) {
6868
this._userManager = userManager;
6969
}
7070

71+
async trySilentSignIn() {
72+
if (!this._intialSilentSignIn) {
73+
this._intialSilentSignIn = (async () => {
74+
try {
75+
await this._userManager.signinSilent();
76+
return;
77+
} catch (e) {
78+
}
79+
})();
80+
}
81+
82+
return this._intialSilentSignIn;
83+
}
84+
7185
async getUser() {
86+
if (window.parent == window && !window.opener && !window.frameElement &&
87+
!location.href.startsWith(this._userManager.settings.redirect_uri!)) {
88+
// If we are not inside a hidden iframe, try authenticating silently.
89+
await AuthenticationService.instance.trySilentSignIn();
90+
}
91+
7292
const user = await this._userManager.getUser();
7393
return user && user.profile;
7494
}
@@ -253,21 +273,61 @@ class OidcAuthorizeService implements AuthorizeService {
253273
export class AuthenticationService {
254274

255275
static _infrastructureKey = 'Microsoft.AspNetCore.Components.WebAssembly.Authentication';
256-
static _initialized : Promise<void>;
276+
static _initialized: Promise<void>;
257277
static instance: OidcAuthorizeService;
278+
static _pendingOperations: { [key: string]: Promise<AuthenticationResult> | undefined } = {}
258279

259280
public static async init(settings: UserManagerSettings & AuthorizeServiceSettings) {
260281
// Multiple initializations can start concurrently and we want to avoid that.
261282
// In order to do so, we create an initialization promise and the first call to init
262283
// tries to initialize the app and sets up a promise other calls can await on.
263284
if (!AuthenticationService._initialized) {
264-
this._initialized = (async () => {
265-
const userManager = await this.createUserManager(settings);
285+
AuthenticationService._initialized = AuthenticationService.InitializeCore(settings);
286+
287+
await AuthenticationService._initialized;
288+
}
289+
290+
return AuthenticationService._initialized;
291+
}
292+
293+
public static handleCallback() {
294+
return AuthenticationService.InitializeCore();
295+
}
296+
297+
private static async InitializeCore(settings?: UserManagerSettings & AuthorizeServiceSettings) {
298+
let finalSettings = settings || AuthenticationService.resolveCachedSettings();
299+
if (!settings && finalSettings) {
300+
const userManager = AuthenticationService.createUserManagerCore(finalSettings);
301+
302+
if (window.parent != window && !window.opener && (window.frameElement &&
303+
location.href.startsWith(userManager.settings.redirect_uri!))) {
304+
// If we are inside a hidden iframe, try completing the sign in early.
266305
AuthenticationService.instance = new OidcAuthorizeService(userManager);
267-
})();
306+
AuthenticationService._initialized = (async (): Promise<void> => {
307+
await AuthenticationService.instance.completeSignIn(location.href);
308+
return;
309+
})();
310+
}
311+
312+
return;
313+
} else if (settings) {
314+
const userManager = await AuthenticationService.createUserManager(settings);
315+
AuthenticationService.instance = new OidcAuthorizeService(userManager);
316+
} else {
317+
// HandleCallback gets called unconditionally, so we do nothing for normal paths.
318+
// Cached settings are only used on handling the redirect_uri path and if the settings are not there
319+
// the app will fallback to the default logic for handling the redirect.
268320
}
321+
}
269322

270-
await this._initialized;
323+
private static resolveCachedSettings(): UserManagerSettings | undefined {
324+
let finalSettings: UserManagerSettings | undefined;
325+
const cachedSettings = window.sessionStorage.getItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`);
326+
if (cachedSettings) {
327+
finalSettings = JSON.parse(cachedSettings);
328+
}
329+
330+
return finalSettings;
271331
}
272332

273333
public static getUser() {
@@ -282,16 +342,30 @@ export class AuthenticationService {
282342
return AuthenticationService.instance.signIn(state);
283343
}
284344

285-
public static completeSignIn(url: string) {
286-
return AuthenticationService.instance.completeSignIn(url);
345+
public static async completeSignIn(url: string) {
346+
let operation = this._pendingOperations[url];
347+
if (!operation) {
348+
operation = AuthenticationService.instance.completeSignIn(url);
349+
await operation;
350+
this._pendingOperations[url] = undefined;
351+
}
352+
353+
return operation;
287354
}
288355

289356
public static signOut(state: any) {
290357
return AuthenticationService.instance.signOut(state);
291358
}
292359

293-
public static completeSignOut(url: string) {
294-
return AuthenticationService.instance.completeSignOut(url);
360+
public static async completeSignOut(url: string) {
361+
let operation = this._pendingOperations[url];
362+
if (!operation) {
363+
operation = AuthenticationService.instance.completeSignOut(url);
364+
await operation;
365+
this._pendingOperations[url] = undefined;
366+
}
367+
368+
return operation;
295369
}
296370

297371
private static async createUserManager(settings: OidcAuthorizeServiceSettings): Promise<UserManager> {
@@ -304,11 +378,6 @@ export class AuthenticationService {
304378

305379
const downloadedSettings = await response.json();
306380

307-
window.sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`, JSON.stringify(settings));
308-
309-
downloadedSettings.automaticSilentRenew = true;
310-
downloadedSettings.includeIdTokenInSilentRenew = true;
311-
312381
finalSettings = downloadedSettings;
313382
} else {
314383
if (!settings.scope) {
@@ -323,12 +392,16 @@ export class AuthenticationService {
323392
finalSettings = settings;
324393
}
325394

326-
const userManager = new UserManager(finalSettings);
395+
window.sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`, JSON.stringify(finalSettings));
327396

397+
return AuthenticationService.createUserManagerCore(finalSettings);
398+
}
399+
400+
private static createUserManagerCore(finalSettings: UserManagerSettings) {
401+
const userManager = new UserManager(finalSettings);
328402
userManager.events.addUserSignedOut(async () => {
329-
await userManager.removeUser();
403+
userManager.removeUser();
330404
});
331-
332405
return userManager;
333406
}
334407
}
@@ -337,4 +410,6 @@ declare global {
337410
interface Window { AuthenticationService: AuthenticationService; }
338411
}
339412

413+
AuthenticationService.handleCallback();
414+
340415
window.AuthenticationService = AuthenticationService;

src/Components/WebAssembly/WebAssembly.Authentication/src/Interop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"build": "npm run build:release",
55
"build:release": "webpack --mode production --env.production --env.configuration=Release",
66
"build:debug": "webpack --mode development --env.configuration=Debug",
7-
"watch": "webpack --watch --mode development"
7+
"watch": "webpack --watch --mode development --env.configuration=Debug"
88
},
99
"devDependencies": {
1010
"ts-loader": "^6.2.1",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Text.Json.Serialization;
6+
7+
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
8+
{
9+
/// <summary>
10+
/// A user account.
11+
/// </summary>
12+
/// <remarks>
13+
/// The information in this type will be use to produce a <see cref="System.Security.Claims.ClaimsPrincipal"/> for the application.
14+
/// </remarks>
15+
public class RemoteUserAccount
16+
{
17+
/// <summary>
18+
/// Gets or sets properties not explicitly mapped about the user.
19+
/// </summary>
20+
[JsonExtensionData]
21+
public IDictionary<string, object> AdditionalProperties { get; set; }
22+
}
23+
}

src/Components/WebAssembly/WebAssembly.Authentication/src/Options/DefaultApiAuthorizationOptionsConfiguration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public void Configure(RemoteAuthenticationOptions<ApiAuthorizationProviderOption
1717
options.AuthenticationPaths.RemoteRegisterPath ??= "Identity/Account/Register";
1818
options.AuthenticationPaths.RemoteProfilePath ??= "Identity/Account/Manage";
1919
options.UserOptions.ScopeClaim ??= "scope";
20+
options.UserOptions.RoleClaim ??= "role";
2021
options.UserOptions.AuthenticationType ??= _applicationName;
2122
}
2223

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
7+
{
8+
internal class RemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>
9+
: IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount>
10+
where TRemoteAuthenticationState : RemoteAuthenticationState
11+
where TAccount : RemoteUserAccount
12+
{
13+
public RemoteAuthenticationBuilder(IServiceCollection services) => Services = services;
14+
15+
public IServiceCollection Services { get; }
16+
}
17+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.DependencyInjection.Extensions;
6+
7+
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
8+
{
9+
/// <summary>
10+
/// Extensions for remote authentication services.
11+
/// </summary>
12+
public static class RemoteAuthenticationBuilderExtensions
13+
{
14+
/// <summary>
15+
/// Replaces the existing <see cref="UserFactory{TAccount}"/> with the user factory defined by <typeparamref name="TUserFactory"/>.
16+
/// </summary>
17+
/// <typeparam name="TRemoteAuthenticationState">The remote authentication state.</typeparam>
18+
/// <typeparam name="TAccount">The account type.</typeparam>
19+
/// <typeparam name="TUserFactory">The new user factory type.</typeparam>
20+
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, TAccount}"/>.</param>
21+
/// <returns>The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, TAccount}"/>.</returns>
22+
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddUserFactory<TRemoteAuthenticationState, TAccount, TUserFactory>(
23+
this IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> builder)
24+
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
25+
where TAccount : RemoteUserAccount
26+
where TUserFactory : UserFactory<TAccount>
27+
{
28+
builder.Services.Replace(ServiceDescriptor.Scoped<UserFactory<TAccount>, TUserFactory>());
29+
30+
return builder;
31+
}
32+
33+
/// <summary>
34+
/// Replaces the existing <see cref="UserFactory{Account}"/> with the user factory defined by <typeparamref name="TUserFactory"/>.
35+
/// </summary>
36+
/// <typeparam name="TRemoteAuthenticationState">The remote authentication state.</typeparam>
37+
/// <typeparam name="TUserFactory">The new user factory type.</typeparam>
38+
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, Account}"/>.</param>
39+
/// <returns>The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, Account}"/>.</returns>
40+
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddUserFactory<TRemoteAuthenticationState, TUserFactory>(
41+
this IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> builder)
42+
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
43+
where TUserFactory : UserFactory<RemoteUserAccount> => builder.AddUserFactory<TRemoteAuthenticationState, RemoteUserAccount, TUserFactory>();
44+
45+
/// <summary>
46+
/// Replaces the existing <see cref="UserFactory{TAccount}"/> with the user factory defined by <typeparamref name="TUserFactory"/>.
47+
/// </summary>
48+
/// <typeparam name="TUserFactory">The new user factory type.</typeparam>
49+
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{RemoteAuthenticationState, Account}"/>.</param>
50+
/// <returns>The <see cref="IRemoteAuthenticationBuilder{RemoteAuthenticationState, Account}"/>.</returns>
51+
public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddUserFactory<TUserFactory>(
52+
this IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> builder)
53+
where TUserFactory : UserFactory<RemoteUserAccount> => builder.AddUserFactory<RemoteAuthenticationState, RemoteUserAccount, TUserFactory>();
54+
}
55+
}

0 commit comments

Comments
 (0)