Skip to content

Commit 84fa0d4

Browse files
committed
[Blazor] Core authentication library + Tests
1 parent f5f51f5 commit 84fa0d4

File tree

91 files changed

+10146
-20
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+10146
-20
lines changed

eng/ProjectReferences.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<ProjectReferenceProvider Include="Mono.WebAssembly.Interop" ProjectPath="$(RepoRoot)src\Components\Blazor\Mono.WebAssembly.Interop\src\Mono.WebAssembly.Interop.csproj" />
1111
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor.Server" ProjectPath="$(RepoRoot)src\Components\Blazor\Server\src\Microsoft.AspNetCore.Blazor.Server.csproj" />
1212
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation" ProjectPath="$(RepoRoot)src\Components\Blazor\Validation\src\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj" />
13+
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" ProjectPath="$(RepoRoot)src\Components\Blazor\WebAssembly.Authentication\src\Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj" />
1314
<ProjectReferenceProvider Include="BlazorServerApp" ProjectPath="$(RepoRoot)src\Components\Samples\BlazorServerApp\BlazorServerApp.csproj" />
1415
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor" ProjectPath="$(RepoRoot)src\Components\Blazor\Blazor\src\Microsoft.AspNetCore.Blazor.csproj" RefProjectPath="$(RepoRoot)src\Components\Blazor\Blazor\ref\Microsoft.AspNetCore.Blazor.csproj" />
1516
</ItemGroup>

eng/Versions.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@
206206
<SystemThreadingTasksExtensionsPackageVersion>4.5.2</SystemThreadingTasksExtensionsPackageVersion>
207207
<!-- Packages developed by @aspnet, but manually updated as necessary. -->
208208
<LibuvPackageVersion>1.10.0</LibuvPackageVersion>
209+
<MicrosoftAspNetCoreIdentityUIPackageVersion>3.1.0</MicrosoftAspNetCoreIdentityUIPackageVersion>
210+
<MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>3.1.0</MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>
211+
<MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>3.1.0</MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>
212+
<MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion>3.1.0</MicrosoftAspNetCoreApiAuthorizationIdentityServerPackageVersion>
213+
<MicrosoftAspNetCoreComponentsAuthorizationPackageVersion>3.1.0</MicrosoftAspNetCoreComponentsAuthorizationPackageVersion>
209214
<MicrosoftAspNetWebApiClientPackageVersion>5.2.6</MicrosoftAspNetWebApiClientPackageVersion>
210215
<!-- Partner teams -->
211216
<MicrosoftAzureKeyVaultPackageVersion>2.3.2</MicrosoftAzureKeyVaultPackageVersion>

src/Components/Blazor/WebAssembly.Authentication/src/AuthenticationManager.cs

Lines changed: 446 additions & 0 deletions
Large diffs are not rendered by default.
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+
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
5+
{
6+
/// <summary>
7+
/// An <see cref="AuthenticationManager{TAuthenticationState}"/> that uses <see cref="RemoteAuthenticationState"/> as the
8+
/// state to be persisted across authentication operations.
9+
/// </summary>
10+
public class DefaultAuthenticationManager : AuthenticationManager<RemoteAuthenticationState>
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of <see cref="DefaultAuthenticationManager"/>.
14+
/// </summary>
15+
public DefaultAuthenticationManager() => AuthenticationState = new RemoteAuthenticationState();
16+
}
17+
}
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import { UserManager, UserManagerSettings, User } from 'oidc-client'
2+
3+
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
4+
5+
type ExtendedUserManagerSettings = Writeable<UserManagerSettings & AuthorizeServiceSettings>
6+
7+
type OidcAuthorizeServiceSettings = ExtendedUserManagerSettings | ApiAuthorizationSettings;
8+
9+
function isApiAuthorizationSettings(settings: OidcAuthorizeServiceSettings): settings is ApiAuthorizationSettings {
10+
return settings.hasOwnProperty('configurationEndpoint');
11+
}
12+
13+
interface AuthorizeServiceSettings {
14+
defaultScopes: string[];
15+
}
16+
17+
interface ApiAuthorizationSettings {
18+
configurationEndpoint: string;
19+
}
20+
21+
export interface AccessTokenRequestOptions {
22+
scopes: string[];
23+
returnUrl: string;
24+
}
25+
26+
export interface AccessTokenResult {
27+
status: AccessTokenResultStatus;
28+
token?: AccessToken;
29+
}
30+
31+
export interface AccessToken {
32+
value: string;
33+
expires: Date;
34+
grantedScopes: string[];
35+
}
36+
37+
export enum AccessTokenResultStatus {
38+
Success = 'success',
39+
RequiresRedirect = 'requiresRedirect'
40+
}
41+
42+
export enum AuthenticationResultStatus {
43+
Redirect = 'redirect',
44+
Success = 'success',
45+
Failure = 'failure',
46+
OperationCompleted = 'operation-completed'
47+
};
48+
49+
export interface AuthenticationResult {
50+
status: AuthenticationResultStatus;
51+
state?: any;
52+
message?: string;
53+
}
54+
55+
export interface AuthorizeService {
56+
getUser(): Promise<any>;
57+
getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
58+
signIn(state: any): Promise<AuthenticationResult>;
59+
completeSignIn(state: any): Promise<AuthenticationResult>;
60+
signOut(state: any): Promise<AuthenticationResult>;
61+
completeSignOut(url: string): Promise<AuthenticationResult>;
62+
}
63+
64+
class OidcAuthorizeService implements AuthorizeService {
65+
private _userManager: UserManager;
66+
67+
constructor(userManager: UserManager) {
68+
this._userManager = userManager;
69+
}
70+
71+
async getUser() {
72+
const user = await this._userManager.getUser();
73+
return user && user.profile;
74+
}
75+
76+
async getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult> {
77+
const user = await this._userManager.getUser();
78+
if (hasValidAccessToken(user) && hasAllScopes(request, user.scopes)) {
79+
return {
80+
status: AccessTokenResultStatus.Success,
81+
token: {
82+
grantedScopes: user.scopes,
83+
expires: getExpiration(user.expires_in),
84+
value: user.access_token
85+
}
86+
};
87+
} else {
88+
try {
89+
const parameters = request && request.scopes ?
90+
{ scope: request.scopes.join(' ') } : undefined;
91+
92+
const newUser = await this._userManager.signinSilent(parameters);
93+
94+
return {
95+
status: AccessTokenResultStatus.Success,
96+
token: {
97+
grantedScopes: newUser.scopes,
98+
expires: getExpiration(newUser.expires_in),
99+
value: newUser.access_token
100+
}
101+
};
102+
103+
} catch (e) {
104+
return {
105+
status: AccessTokenResultStatus.RequiresRedirect
106+
};
107+
}
108+
}
109+
110+
function hasValidAccessToken(user: User | null): user is User {
111+
return !!(user && user.access_token && !user.expired && user.scopes);
112+
}
113+
114+
function getExpiration(expiresIn: number) {
115+
const now = new Date();
116+
now.setTime(now.getTime() + expiresIn * 1000);
117+
return now;
118+
}
119+
120+
function hasAllScopes(request: AccessTokenRequestOptions | undefined, currentScopes: string[]) {
121+
const set = new Set(currentScopes);
122+
if (request && request.scopes) {
123+
for (let current of request.scopes) {
124+
if (!set.has(current)) {
125+
return false;
126+
}
127+
}
128+
}
129+
130+
return true;
131+
}
132+
}
133+
134+
async signIn(state: any) {
135+
try {
136+
await this._userManager.clearStaleState();
137+
const silentUser = await this._userManager.signinSilent(this.createArguments());
138+
return this.success(state);
139+
} catch (silentError) {
140+
await this._userManager.clearStaleState();
141+
// User might not be authenticated, fallback to popup authentication
142+
console.log("Silent authentication error: ", silentError);
143+
144+
try {
145+
await this._userManager.signinRedirect(this.createArguments(state));
146+
return this.redirect();
147+
} catch (redirectError) {
148+
console.log("Redirect authentication error: ", redirectError);
149+
return this.error(redirectError);
150+
}
151+
}
152+
}
153+
154+
async completeSignIn(url: string) {
155+
const requiresLogin = await this.loginRequired(url);
156+
const stateExists = await this.stateExists(url);
157+
try {
158+
const user = await this._userManager.signinCallback(url);
159+
if (window.self !== window.top) {
160+
return this.operationCompleted();
161+
} else {
162+
return this.success(user && user.state);
163+
}
164+
} catch (error) {
165+
if (requiresLogin || window.self !== window.top || !stateExists) {
166+
return this.operationCompleted();
167+
}
168+
169+
console.log('There was an error signing in: ', error);
170+
return this.error('There was an error signing in.');
171+
}
172+
}
173+
174+
async signOut(state: any) {
175+
try {
176+
if (!(await this._userManager.metadataService.getEndSessionEndpoint())) {
177+
await this._userManager.removeUser();
178+
return this.success(state);
179+
}
180+
await this._userManager.signoutRedirect(this.createArguments(state));
181+
return this.redirect();
182+
} catch (redirectSignOutError) {
183+
console.log("Redirect signout error: ", redirectSignOutError);
184+
return this.error(redirectSignOutError);
185+
}
186+
}
187+
188+
async completeSignOut(url: string) {
189+
try {
190+
if (await this.stateExists(url)) {
191+
const response = await this._userManager.signoutCallback(url);
192+
return this.success(response && response.state);
193+
} else {
194+
return this.operationCompleted();
195+
}
196+
} catch (error) {
197+
console.log(`There was an error trying to log out '${error}'.`);
198+
console.log(url);
199+
return this.error(error);
200+
}
201+
}
202+
203+
private async stateExists(url: string) {
204+
const stateParam = new URLSearchParams(new URL(url).search).get('state');
205+
if (stateParam) {
206+
return await this._userManager.settings.stateStore!.get(stateParam);
207+
} else {
208+
return undefined;
209+
}
210+
}
211+
212+
private async loginRequired(url: string) {
213+
const errorParameter = new URLSearchParams(new URL(url).search).get('error');
214+
if (errorParameter) {
215+
const error = await this._userManager.settings.stateStore!.get(errorParameter);
216+
return error === 'login_required';
217+
} else {
218+
return false;
219+
}
220+
}
221+
222+
private createArguments(state?: any) {
223+
return { useReplaceToNavigate: true, data: state };
224+
}
225+
226+
private error(message: string) {
227+
return { status: AuthenticationResultStatus.Failure, errorMessage: message };
228+
}
229+
230+
private success(state: any) {
231+
return { status: AuthenticationResultStatus.Success, state };
232+
}
233+
234+
private redirect() {
235+
return { status: AuthenticationResultStatus.Redirect };
236+
}
237+
238+
private operationCompleted() {
239+
return { status: AuthenticationResultStatus.OperationCompleted };
240+
}
241+
}
242+
243+
export class AuthenticationService {
244+
245+
static _infrastructureKey = 'Microsoft.AspNetCore.Components.WebAssembly.Authentication';
246+
static _initialized = false;
247+
static instance: OidcAuthorizeService;
248+
249+
public static async init(settings: UserManagerSettings & AuthorizeServiceSettings) {
250+
if (!AuthenticationService._initialized) {
251+
AuthenticationService._initialized = true;
252+
const userManager = await this.createUserManager(settings);
253+
AuthenticationService.instance = new OidcAuthorizeService(userManager);
254+
}
255+
}
256+
257+
public static getUser() {
258+
return AuthenticationService.instance.getUser();
259+
}
260+
261+
public static getAccessToken() {
262+
return AuthenticationService.instance.getAccessToken();
263+
}
264+
265+
public static signIn(state: any) {
266+
return AuthenticationService.instance.signIn(state);
267+
}
268+
269+
public static completeSignIn(url: string) {
270+
return AuthenticationService.instance.completeSignIn(url);
271+
}
272+
273+
public static signOut(state: any) {
274+
return AuthenticationService.instance.signOut(state);
275+
}
276+
277+
public static completeSignOut(url: string) {
278+
return AuthenticationService.instance.completeSignOut(url);
279+
}
280+
281+
private static async createUserManager(settings: OidcAuthorizeServiceSettings): Promise<UserManager> {
282+
let finalSettings: UserManagerSettings;
283+
if (isApiAuthorizationSettings(settings)) {
284+
let response = await fetch(settings.configurationEndpoint);
285+
if (!response.ok) {
286+
throw new Error(`Could not load settings from '${settings.configurationEndpoint}'`);
287+
}
288+
289+
const downloadedSettings = await response.json();
290+
291+
window.sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`, JSON.stringify(settings));
292+
293+
downloadedSettings.automaticSilentRenew = true;
294+
downloadedSettings.includeIdTokenInSilentRenew = true;
295+
296+
finalSettings = downloadedSettings;
297+
} else {
298+
if (!settings.scope) {
299+
settings.scope = settings.defaultScopes.join(' ');
300+
}
301+
302+
finalSettings = settings;
303+
}
304+
305+
const userManager = new UserManager(finalSettings);
306+
307+
userManager.events.addUserSignedOut(async () => {
308+
await userManager.removeUser();
309+
});
310+
311+
return userManager;
312+
}
313+
}
314+
315+
declare global {
316+
interface Window { AuthenticationService: AuthenticationService; }
317+
}
318+
319+
window.AuthenticationService = AuthenticationService;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.js
2+
*.js.map
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "microsoft.aspnetcore.components.webassembly.authentication",
3+
"version": "0.1.0",
4+
"description": "Interop layer for Microsoft.AspNetCore.Components.WebAssembly.Authentication",
5+
"scripts": {
6+
"build": "npm run build:release",
7+
"build:release": "webpack --mode production --env.production --env.configuration=Release",
8+
"build:debug": "webpack --mode development --env.configuration=Debug",
9+
"watch": "webpack --watch --mode development"
10+
},
11+
"repository": {
12+
"type": "git",
13+
"url": "git+https://github.com/dotnet/aspnetcore.git"
14+
},
15+
"keywords": [
16+
"aspnetcore",
17+
"blazor",
18+
"oidc"
19+
],
20+
"author": "Microsoft corporation",
21+
"license": "MIT",
22+
"bugs": {
23+
"url": "https://github.com/dotnet/aspnetcore/issues"
24+
},
25+
"homepage": "https://github.com/dotnet/aspnetcore#readme",
26+
"devDependencies": {
27+
"ts-loader": "^6.2.1",
28+
"typescript": "^3.7.5",
29+
"webpack": "^4.41.5",
30+
"webpack-cli": "^3.3.10"
31+
},
32+
"dependencies": {
33+
"oidc-client": "^1.10.1"
34+
}
35+
}

0 commit comments

Comments
 (0)