[Blazor][Wasm] Adds support for AAD and AADB2C using msal.js (#19190)
* Adds a new library Microsoft.Authentication.WebAssembly.Msal that handles authentication for Blazor Webassembly applications using msal.js
This commit is contained in:
parent
4715de45eb
commit
0541e19ac2
|
|
@ -11,6 +11,7 @@
|
|||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly.Server" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Server\src\Microsoft.AspNetCore.Components.WebAssembly.Server.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.DataAnnotations.Validation" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Validation\src\Microsoft.AspNetCore.Components.DataAnnotations.Validation.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" ProjectPath="$(RepoRoot)src\Components\WebAssembly\WebAssembly.Authentication\src\Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.Authentication.WebAssembly.Msal" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly" ProjectPath="$(RepoRoot)src\Components\WebAssembly\WebAssembly\src\Microsoft.AspNetCore.Components.WebAssembly.csproj" RefProjectPath="$(RepoRoot)src\Components\WebAssembly\WebAssembly\ref\Microsoft.AspNetCore.Components.WebAssembly.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasm.Authentication.Server"
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasm.Authentication.Shared", "WebAssembly\testassets\Wasm.Authentication.Shared\Wasm.Authentication.Shared.csproj", "{EAF50654-98ED-44BB-A120-0436EC0CD3E0}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authentication.Msal", "Authentication.Msal", "{E4D756A7-A934-4D7F-BC6E-7B95FE4098AB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Authentication.WebAssembly.Msal", "WebAssembly\Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj", "{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -505,6 +509,18 @@ Global
|
|||
{EAF50654-98ED-44BB-A120-0436EC0CD3E0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EAF50654-98ED-44BB-A120-0436EC0CD3E0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EAF50654-98ED-44BB-A120-0436EC0CD3E0}.Release|x86.Build.0 = Release|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|x64.Build.0 = Release|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -555,6 +571,8 @@ Global
|
|||
{7EFB9CAF-6716-43BF-A6EF-C2878E95F8A6} = {CBD2BB24-3EC3-4950-ABE4-8C521D258DCD}
|
||||
{194EBC45-F98E-4919-B714-C1624EF17B31} = {CBD2BB24-3EC3-4950-ABE4-8C521D258DCD}
|
||||
{EAF50654-98ED-44BB-A120-0436EC0CD3E0} = {CBD2BB24-3EC3-4950-ABE4-8C521D258DCD}
|
||||
{E4D756A7-A934-4D7F-BC6E-7B95FE4098AB} = {B29FB58D-FAE5-405E-9695-BCF93582BE9A}
|
||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43} = {E4D756A7-A934-4D7F-BC6E-7B95FE4098AB}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {27A36094-AA50-4FFD-ADE6-C055E391F741}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,359 @@
|
|||
import * as Msal from 'msal';
|
||||
import { StringDict } from 'msal/lib-commonjs/MsalTypes';
|
||||
import { ClientAuthErrorMessage } from 'msal/lib-commonjs/error/ClientAuthError';
|
||||
|
||||
interface AccessTokenRequestOptions {
|
||||
scopes: string[];
|
||||
returnUrl: string;
|
||||
}
|
||||
|
||||
interface AccessTokenResult {
|
||||
status: AccessTokenResultStatus;
|
||||
token?: AccessToken;
|
||||
}
|
||||
|
||||
interface AccessToken {
|
||||
value: string;
|
||||
expires: Date;
|
||||
grantedScopes: string[];
|
||||
}
|
||||
|
||||
enum AccessTokenResultStatus {
|
||||
Success = "success",
|
||||
RequiresRedirect = "requiresRedirect"
|
||||
}
|
||||
|
||||
enum AuthenticationResultStatus {
|
||||
Redirect = "redirect",
|
||||
Success = "success",
|
||||
Failure = "failure",
|
||||
OperationCompleted = "operation-completed"
|
||||
}
|
||||
|
||||
interface AuthenticationResult {
|
||||
status: AuthenticationResultStatus;
|
||||
state?: any;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface AuthorizeService {
|
||||
getUser(): Promise<StringDict | undefined>;
|
||||
getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
|
||||
signIn(state: any): Promise<AuthenticationResult>;
|
||||
completeSignIn(state: any): Promise<AuthenticationResult>;
|
||||
signOut(state: any): Promise<AuthenticationResult>;
|
||||
completeSignOut(url: string): Promise<AuthenticationResult>;
|
||||
}
|
||||
|
||||
interface AuthorizeServiceConfiguration extends Msal.Configuration {
|
||||
defaultAccessTokenScopes: string[];
|
||||
additionalScopesToConsent: string[]
|
||||
}
|
||||
|
||||
class MsalAuthorizeService implements AuthorizeService {
|
||||
readonly _msalApplication: Msal.UserAgentApplication;
|
||||
readonly _callbackPromise: Promise<AuthenticationResult>;
|
||||
|
||||
constructor(private readonly _settings: AuthorizeServiceConfiguration) {
|
||||
|
||||
// It is important that we capture the callback-url here as msal will remove the auth parameters
|
||||
// from the url as soon as it gets initialized.
|
||||
const callbackUrl = location.href;
|
||||
this._msalApplication = new Msal.UserAgentApplication(this._settings);
|
||||
|
||||
// This promise will only resolve in callback-paths, which is where we check it.
|
||||
this._callbackPromise = this.createCallbackResult(callbackUrl);
|
||||
}
|
||||
|
||||
async getUser() {
|
||||
const account = this._msalApplication.getAccount();
|
||||
return account?.idTokenClaims;
|
||||
}
|
||||
|
||||
async getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult> {
|
||||
try {
|
||||
const newToken = await this.getTokenCore(request?.scopes);
|
||||
|
||||
return {
|
||||
status: AccessTokenResultStatus.Success,
|
||||
token: newToken
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
return {
|
||||
status: AccessTokenResultStatus.RequiresRedirect
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getTokenCore(scopes?: string[]): Promise<AccessToken | undefined> {
|
||||
const tokenScopes = {
|
||||
redirectUri: this._settings.auth.redirectUri as string,
|
||||
scopes: scopes || this._settings.defaultAccessTokenScopes
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this._msalApplication.acquireTokenSilent(tokenScopes);
|
||||
return {
|
||||
value: response.accessToken,
|
||||
grantedScopes: response.scopes,
|
||||
expires: response.expiresOn
|
||||
};
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async signIn(state: any) {
|
||||
try {
|
||||
// Before we start any sign-in flow, clear out any previous state so that it doesn't pile up.
|
||||
this.purgeState();
|
||||
|
||||
const request: Msal.AuthenticationParameters = {
|
||||
redirectUri: this._settings.auth.redirectUri as string,
|
||||
state: await this.saveState(state)
|
||||
};
|
||||
|
||||
if (this._settings.defaultAccessTokenScopes && this._settings.defaultAccessTokenScopes.length > 0) {
|
||||
request.scopes = this._settings.defaultAccessTokenScopes;
|
||||
}
|
||||
|
||||
if (this._settings.additionalScopesToConsent && this._settings.additionalScopesToConsent.length > 0) {
|
||||
request.extraScopesToConsent = this._settings.additionalScopesToConsent;
|
||||
}
|
||||
|
||||
const result = await this.signInCore(request);
|
||||
if (!result) {
|
||||
return this.redirect();
|
||||
} else if (this.isMsalError(result)) {
|
||||
return this.error(result.errorMessage);
|
||||
}
|
||||
|
||||
try {
|
||||
if (this._settings.defaultAccessTokenScopes?.length > 0) {
|
||||
// This provisions the token as part of the sign-in flow eagerly so that is already in the cache
|
||||
// when the app asks for it.
|
||||
await this._msalApplication.acquireTokenSilent(request);
|
||||
}
|
||||
} catch (e) {
|
||||
return this.error(e.errorMessage);
|
||||
}
|
||||
|
||||
return this.success(state);
|
||||
} catch (e) {
|
||||
return this.error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async signInCore(request: Msal.AuthenticationParameters): Promise<Msal.AuthResponse | Msal.AuthError | undefined> {
|
||||
try {
|
||||
return await this._msalApplication.loginPopup(request);
|
||||
} catch (e) {
|
||||
// If the user explicitly cancelled the pop-up, avoid performing a redirect.
|
||||
if (this.isMsalError(e) && e.errorCode !== ClientAuthErrorMessage.userCancelledError.code) {
|
||||
try {
|
||||
this._msalApplication.loginRedirect(request);
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completeSignIn() {
|
||||
return this._callbackPromise;
|
||||
}
|
||||
|
||||
async signOut(state: any) {
|
||||
// We are about to sign out, so clear any state before we do so and leave just the sign out state for
|
||||
// the current sign out flow.
|
||||
this.purgeState();
|
||||
|
||||
const logoutStateId = await this.saveState(state);
|
||||
|
||||
// msal.js doesn't support providing logout state, so we shim it by putting the identifier in session storage
|
||||
// and using that on the logout callback to workout the problems.
|
||||
sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.LogoutState`, logoutStateId);
|
||||
|
||||
this._msalApplication.logout();
|
||||
|
||||
// We are about to be redirected.
|
||||
return this.redirect();
|
||||
}
|
||||
|
||||
async completeSignOut(url: string) {
|
||||
const logoutStateId = sessionStorage.getItem(`${AuthenticationService._infrastructureKey}.LogoutState`);
|
||||
const updatedUrl = new URL(url);
|
||||
updatedUrl.search = `?state=${logoutStateId}`;
|
||||
const logoutState = await this.retrieveState(updatedUrl.href, /*isLogout*/ true);
|
||||
|
||||
sessionStorage.removeItem(`${AuthenticationService._infrastructureKey}.LogoutState`);
|
||||
|
||||
if (logoutState) {
|
||||
return this.success(logoutState);
|
||||
} else {
|
||||
return this.operationCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
// msal.js only allows a string as the account state and it simply attaches it to the sign-in request state.
|
||||
// Given that we don't want to serialize the entire state and put it in the query string, we need to serialize the
|
||||
// state ourselves and pass an identifier to retrieve it while in the callback flow.
|
||||
async saveState<T>(state: T): Promise<string> {
|
||||
const base64UrlIdentifier = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = evt => resolve((evt?.target?.result as string)
|
||||
// The result comes back as a base64 string inside a dataUrl.
|
||||
// We remove the prefix and convert it to base64url by replacing '+' with '-', '/' with '_' and removing '='.
|
||||
.split(',')[1].replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''));
|
||||
reader.onerror = evt => reject(evt.target?.error?.message);
|
||||
|
||||
// We generate a base 64 url encoded string of random data.
|
||||
const entropy = window.crypto.getRandomValues(new Uint8Array(32));
|
||||
reader.readAsDataURL(new Blob([entropy]));
|
||||
});
|
||||
|
||||
sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.AuthorizeService.${base64UrlIdentifier}`, JSON.stringify(state));
|
||||
return base64UrlIdentifier;
|
||||
}
|
||||
|
||||
async retrieveState<T>(url: string, isLogout: boolean = false): Promise<T | undefined> {
|
||||
const parsedUrl = new URL(url);
|
||||
const fromHash = parsedUrl.hash && parsedUrl.hash.length > 0 && new URLSearchParams(parsedUrl.hash.substring(1));
|
||||
let state = fromHash && fromHash.getAll('state');
|
||||
if (state && state.length > 1) {
|
||||
return undefined;
|
||||
} else if (!state || state.length == 0) {
|
||||
state = parsedUrl.searchParams && parsedUrl.searchParams.getAll('state');
|
||||
if (!state || state.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// We need to calculate the state key in two different ways. The reason for it is that
|
||||
// msal.js doesn't support the state parameter on logout flows, which forces us to shim our own logout state.
|
||||
// The format then is different, as msal follows the pattern state=<<guid>>|<<user_state>> and our format
|
||||
// simple uses <<base64urlIdentifier>>.
|
||||
const appState = !isLogout? this._msalApplication.getAccountState(state[0]): state[0];
|
||||
const stateKey = `${AuthenticationService._infrastructureKey}.AuthorizeService.${appState}`;
|
||||
const stateString = sessionStorage.getItem(stateKey);
|
||||
if (stateString) {
|
||||
sessionStorage.removeItem(stateKey);
|
||||
const savedState = JSON.parse(stateString);
|
||||
return savedState;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
purgeState() {
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (key?.startsWith(AuthenticationService._infrastructureKey)) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createCallbackResult(callbackUrl: string): Promise<AuthenticationResult> {
|
||||
// msal.js requires a callback to be registered during app initialization to handle redirect flows.
|
||||
// To map that behavior to our API we register a callback early and store the result of that callback
|
||||
// as a promise on an instance field to be able to serve the state back to the main app.
|
||||
const promiseFactory = (resolve: (result: Msal.AuthResponse) => void, reject: (error: Msal.AuthError) => void): void => {
|
||||
this._msalApplication.handleRedirectCallback(
|
||||
authenticationResponse => {
|
||||
resolve(authenticationResponse);
|
||||
},
|
||||
authenticationError => {
|
||||
reject(authenticationError);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Evaluate the promise to capture any authentication errors
|
||||
await new Promise<Msal.AuthResponse>(promiseFactory);
|
||||
// See https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/FAQs#q6-how-to-avoid-page-reloads-when-acquiring-and-renewing-tokens-silently
|
||||
if (window !== window.parent && !window.opener) {
|
||||
return this.operationCompleted();
|
||||
} else {
|
||||
const state = await this.retrieveState(callbackUrl);
|
||||
return this.success(state);
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.isMsalError(e)) {
|
||||
return this.error(e.errorMessage);
|
||||
} else {
|
||||
return this.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isMsalError(resultOrError: any): resultOrError is Msal.AuthError {
|
||||
return resultOrError?.errorCode;
|
||||
}
|
||||
|
||||
private error(message: string) {
|
||||
return { status: AuthenticationResultStatus.Failure, errorMessage: message };
|
||||
}
|
||||
|
||||
private success(state: any) {
|
||||
return { status: AuthenticationResultStatus.Success, state };
|
||||
}
|
||||
|
||||
private redirect() {
|
||||
return { status: AuthenticationResultStatus.Redirect };
|
||||
}
|
||||
|
||||
private operationCompleted() {
|
||||
return { status: AuthenticationResultStatus.OperationCompleted };
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthenticationService {
|
||||
|
||||
static _infrastructureKey = 'Microsoft.Authentication.WebAssembly.Msal';
|
||||
static _initialized = false;
|
||||
static instance: MsalAuthorizeService;
|
||||
|
||||
public static async init(settings: AuthorizeServiceConfiguration) {
|
||||
if (!AuthenticationService._initialized) {
|
||||
AuthenticationService._initialized = true;
|
||||
AuthenticationService.instance = new MsalAuthorizeService(settings);
|
||||
}
|
||||
}
|
||||
|
||||
public static getUser() {
|
||||
return AuthenticationService.instance.getUser();
|
||||
}
|
||||
|
||||
public static getAccessToken(request: AccessTokenRequestOptions) {
|
||||
return AuthenticationService.instance.getAccessToken(request);
|
||||
}
|
||||
|
||||
public static signIn(state: any) {
|
||||
return AuthenticationService.instance.signIn(state);
|
||||
}
|
||||
|
||||
// url is not used in the msal.js implementation but we keep it here
|
||||
// as it is part of the default RemoteAuthenticationService contract implementation.
|
||||
// The unused parameter here just reflects that.
|
||||
public static completeSignIn(url: string) {
|
||||
return AuthenticationService.instance.completeSignIn();
|
||||
}
|
||||
|
||||
public static signOut(state: any) {
|
||||
return AuthenticationService.instance.signOut(state);
|
||||
}
|
||||
|
||||
public static completeSignOut(url: string) {
|
||||
return AuthenticationService.instance.completeSignOut(url);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window { AuthenticationService: AuthenticationService; }
|
||||
}
|
||||
|
||||
window.AuthenticationService = AuthenticationService;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*.js
|
||||
*.js.map
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"scripts": {
|
||||
"build": "npm run build:release",
|
||||
"build:release": "webpack --mode production --env.production --env.configuration=Release",
|
||||
"build:debug": "webpack --mode development --env.configuration=Debug",
|
||||
"watch": "webpack --watch --mode development --env.configuration=Debug"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-loader": "^6.2.1",
|
||||
"typescript": "^3.7.5",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-cli": "^3.3.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"msal": "^1.2.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"module": "commonjs",
|
||||
"lib": [ "DOM", "ES2019" ],
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = env => {
|
||||
|
||||
return {
|
||||
entry: './AuthenticationService.ts',
|
||||
devtool: env && env.production ? 'none' : 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
},
|
||||
output: {
|
||||
filename: 'AuthenticationService.js',
|
||||
path: path.resolve(__dirname, 'dist', env.configuration),
|
||||
},
|
||||
};
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,82 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<Sdk Name="Yarn.MSBuild" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<Description>Authenticate your Blazor webassembly applications with Azure Active Directory and Azure Active Directory B2C</Description>
|
||||
<IsShippingPackage>true</IsShippingPackage>
|
||||
<HasReferenceAssembly>false</HasReferenceAssembly>
|
||||
<RazorLangVersion>3.0</RazorLangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Authentication.WebAssembly.Msal.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<YarnWorkingDir>$(MSBuildThisFileDirectory)Interop\</YarnWorkingDir>
|
||||
<ResolveCurrentProjectStaticWebAssetsInputsDependsOn>
|
||||
CompileInterop;
|
||||
$(ResolveCurrentProjectStaticWebAssetsInputsDependsOn)
|
||||
</ResolveCurrentProjectStaticWebAssetsInputsDependsOn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<YarnInputs Include="$(YarnWorkingDir)**" Exclude="$(YarnWorkingDir)node_modules\**;$(YarnWorkingDir)*.d.ts;$(YarnWorkingDir)dist\**" />
|
||||
<YarnOutputs Include="$(YarnWorkingDir)dist\$(Configuration)\AuthenticationService.js" />
|
||||
<YarnOutputs Include="$(YarnWorkingDir)dist\$(Configuration)\AuthenticationService.js.map" Condition="'$(Configuration)' == 'Debug'" />
|
||||
|
||||
<Content Remove="$(YarnWorkingDir)**" />
|
||||
<None Include="$(YarnWorkingDir)*" Exclude="$(YarnWorkingDir)node_modules\**" />
|
||||
|
||||
<UpToDateCheckInput Include="@(YarnInputs)" Set="StaticWebassets" />
|
||||
<UpToDateCheckOutput Include="@(YarnOutputs)" Set="StaticWebassets" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="_CreateInteropHash" BeforeTargets="CompileInterop" Condition="'$(DesignTimeBuild)' != 'true'">
|
||||
|
||||
<PropertyGroup>
|
||||
<InteropCompilationCacheFile>$(IntermediateOutputPath)interop.cache</InteropCompilationCacheFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<Hash ItemsToHash="@(YarnInputs)">
|
||||
<Output TaskParameter="HashResult" PropertyName="_YarnInputsHash" />
|
||||
</Hash>
|
||||
|
||||
<WriteLinesToFile Lines="$(_YarnInputsHash)" File="$(InteropCompilationCacheFile)" Overwrite="True" WriteOnlyWhenDifferent="True" />
|
||||
|
||||
<ItemGroup>
|
||||
<FileWrites Include="$(InteropCompilationCacheFile)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Target>
|
||||
|
||||
<Target Name="CompileInterop" Condition="'$(DesignTimeBuild)' != 'true'" Inputs="$(InteropCompilationCacheFile)" Outputs="@(YarnOutputs)">
|
||||
<Yarn Command="install --mutex network" WorkingDirectory="$(YarnWorkingDir)" />
|
||||
<Yarn Command="run build:release" WorkingDirectory="$(YarnWorkingDir)" Condition="'$(Configuration)' == 'Release'" />
|
||||
<Yarn Command="run build:debug" WorkingDirectory="$(YarnWorkingDir)" Condition="'$(Configuration)' == 'Debug'" />
|
||||
|
||||
<ItemGroup>
|
||||
<_InteropBuildOutput Include="$(YarnWorkingDir)dist\$(Configuration)\**" Exclude="$(YarnWorkingDir)dist\.gitignore" />
|
||||
|
||||
<StaticWebAsset Include="@(_InteropBuildOutput->'%(FullPath)')">
|
||||
<SourceType></SourceType>
|
||||
<SourceId>$(PackageId)</SourceId>
|
||||
<ContentRoot>$([MSBuild]::NormalizeDirectory('$(YarnWorkingDir)\dist\$(Configuration)'))</ContentRoot>
|
||||
<BasePath>_content/$(PackageId)</BasePath>
|
||||
<RelativePath>$([System.String]::Copy('%(RecursiveDir)%(FileName)%(Extension)').Replace('\','/'))</RelativePath>
|
||||
</StaticWebAsset>
|
||||
|
||||
<FileWrites Include="$(_InteropBuildOutput)" />
|
||||
</ItemGroup>
|
||||
|
||||
<Message Importance="high" Text="@(_InteropBuildOutput->'Emitted %(FullPath)')" />
|
||||
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.Authentication.WebAssembly.Msal
|
||||
{
|
||||
/// <summary>
|
||||
/// Authentication options for the underlying msal.js library handling the authentication.
|
||||
/// </summary>
|
||||
public class MsalAuthenticationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the client id for the application.
|
||||
/// </summary>
|
||||
public string ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authority for the Azure Active Directory or Azure Active Directory B2C instance.
|
||||
/// </summary>
|
||||
public string Authority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether or not to validate the authority.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value needs to be set to false when using Azure Active Directory B2C.
|
||||
/// </remarks>
|
||||
public bool ValidateAuthority { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the redirect uri for the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It can be an absolute or base relative <see cref="Uri"/> and defaults to <c>authentication/login-callback.</c>
|
||||
/// </remarks>
|
||||
public string RedirectUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the post logout redirect uri for the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It can be an absolute or base relative <see cref="Uri"/> and defaults to <c>authentication/logout-callback.</c>
|
||||
/// </remarks>
|
||||
public string PostLogoutRedirectUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether or not to navigate to the login request url after a successful login.
|
||||
/// </summary>
|
||||
public bool NavigateToLoginRequestUrl { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// 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.Authentication.WebAssembly.Msal.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Cache options for the msal.js cache.
|
||||
/// </summary>
|
||||
public class MsalCacheOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the cache location.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Valid values are <c>sessionStorage</c> and <c>localStorage</c>.
|
||||
/// </remarks>
|
||||
public string CacheLocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to store the authentication state in a cookie.
|
||||
/// </summary>
|
||||
public bool StoreAuthStateInCookie { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// 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.Authentication.WebAssembly.Msal.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Authentication provider options for the msal.js authentication provider.
|
||||
/// </summary>
|
||||
public class MsalProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="MsalAuthenticationOptions"/> to use for authentication operations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("auth")]
|
||||
public MsalAuthenticationOptions Authentication { get; set; } = new MsalAuthenticationOptions
|
||||
{
|
||||
RedirectUri = "authentication/login-callback",
|
||||
PostLogoutRedirectUri = "authentication/logout-callback"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the msal.js cache options.
|
||||
/// </summary>
|
||||
public MsalCacheOptions Cache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set the list of default access tokens scopes to provision during the sign-in flow.
|
||||
/// </summary>
|
||||
public IList<string> DefaultAccessTokenScopes { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of additional scopes to consent during the initial sign-in flow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this parameter to request consent for scopes for other resources.
|
||||
/// </remarks>
|
||||
public IList<string> AdditionalScopesToConsent { get; set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// 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.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||
using Microsoft.Authentication.WebAssembly.Msal.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Authentication.WebAssembly.Msal
|
||||
{
|
||||
internal class MsalDefaultOptionsConfiguration : IPostConfigureOptions<RemoteAuthenticationOptions<MsalProviderOptions>>
|
||||
{
|
||||
private readonly NavigationManager _navigationManager;
|
||||
|
||||
public MsalDefaultOptionsConfiguration(NavigationManager navigationManager)
|
||||
{
|
||||
_navigationManager = navigationManager;
|
||||
}
|
||||
|
||||
public void Configure(RemoteAuthenticationOptions<MsalProviderOptions> options)
|
||||
{
|
||||
options.UserOptions.ScopeClaim ??= "scp";
|
||||
options.UserOptions.AuthenticationType ??= options.ProviderOptions.Authentication.ClientId;
|
||||
|
||||
var redirectUri = options.ProviderOptions.Authentication.RedirectUri;
|
||||
if (redirectUri == null || !Uri.TryCreate(redirectUri, UriKind.Absolute, out _))
|
||||
{
|
||||
redirectUri ??= "authentication/login-callback";
|
||||
options.ProviderOptions.Authentication.RedirectUri = _navigationManager
|
||||
.ToAbsoluteUri(redirectUri).AbsoluteUri;
|
||||
}
|
||||
|
||||
var logoutUri = options.ProviderOptions.Authentication.PostLogoutRedirectUri;
|
||||
if (logoutUri == null || !Uri.TryCreate(logoutUri, UriKind.Absolute, out _))
|
||||
{
|
||||
logoutUri ??= "authentication/logout-callback";
|
||||
options.ProviderOptions.Authentication.PostLogoutRedirectUri = _navigationManager
|
||||
.ToAbsoluteUri(logoutUri).AbsoluteUri;
|
||||
}
|
||||
|
||||
options.ProviderOptions.Authentication.NavigateToLoginRequestUrl = false;
|
||||
options.ProviderOptions.Cache = new MsalCacheOptions
|
||||
{
|
||||
CacheLocation = "localStorage",
|
||||
StoreAuthStateInCookie = true
|
||||
};
|
||||
}
|
||||
|
||||
public void PostConfigure(string name, RemoteAuthenticationOptions<MsalProviderOptions> options)
|
||||
{
|
||||
if (string.Equals(name, Options.DefaultName))
|
||||
{
|
||||
Configure(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// 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.AspNetCore.Components.WebAssembly.Authentication;
|
||||
using Microsoft.Authentication.WebAssembly.Msal;
|
||||
using Microsoft.Authentication.WebAssembly.Msal.Models;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods to add authentication to Blazor WebAssembly applications using
|
||||
/// Azure Active Directory or Azure Active Directory B2C.
|
||||
/// </summary>
|
||||
public static class MsalWebAssemblyServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds authentication using msal.js to Blazor applications.
|
||||
/// </summary>
|
||||
/// <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(this IServiceCollection services, Action<RemoteAuthenticationOptions<MsalProviderOptions>> configure)
|
||||
{
|
||||
services.AddRemoteAuthentication<RemoteAuthenticationState, MsalProviderOptions>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<RemoteAuthenticationOptions<MsalProviderOptions>>, MsalDefaultOptionsConfiguration>());
|
||||
|
||||
if (configure != null)
|
||||
{
|
||||
services.Configure(configure);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
|||
/// <summary>
|
||||
/// Gets or sets the list of scopes to request for the token.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Scopes { get; set; }
|
||||
public string[] Scopes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific return url to use for returning the user back to the application if it needs to be
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="${MicrosoftAspNetCoreComponentsWebAssemblyDevServerPackageVersion}" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Blazor.HttpClient" Version="${MicrosoftAspNetCoreBlazorHttpClientPackageVersion}" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="${MicrosoftAspNetCoreComponentsWebAssemblyAuthenticationPackageVersion}" Condition="'$(IndividualLocalAuth)' == 'true'" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="${MicrosoftAspNetCoreComponentsWebAssemblyAuthenticationPackageVersion}" Condition="'$(OrganizationalAuth)' == 'true' OR '$(IndividualB2CAuth)' == 'true'" />
|
||||
<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="${MicrosoftAuthenticationWebAssemblyMsalPackageVersion}" Condition="'$(OrganizationalAuth)' == 'true' OR '$(IndividualB2CAuth)' == 'true'" />
|
||||
</ItemGroup>
|
||||
<!--#if Hosted -->
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
<PackageVersionVariableReference Include="$(ComponentsWebAssemblyProjectsRoot)Server\src\Microsoft.AspNetCore.Components.WebAssembly.Server.csproj" />
|
||||
<PackageVersionVariableReference Include="$(ComponentsWebAssemblyProjectsRoot)Server\src\Microsoft.AspNetCore.Components.WebAssembly.Server.csproj" />
|
||||
<PackageVersionVariableReference Include="$(ComponentsWebAssemblyProjectsRoot)WebAssembly.Authentication\src\Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj" />
|
||||
<PackageVersionVariableReference Include="$(ComponentsWebAssemblyProjectsRoot)Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -23,18 +23,15 @@
|
|||
},
|
||||
"AAdB2CInstance": {
|
||||
"longName": "aad-b2c-instance",
|
||||
"shortName": "",
|
||||
"isHidden": true
|
||||
"shortName": ""
|
||||
},
|
||||
"SignUpSignInPolicyId": {
|
||||
"longName": "susi-policy-id",
|
||||
"shortName": "ssp",
|
||||
"isHidden": true
|
||||
"shortName": "ssp"
|
||||
},
|
||||
"OrgReadAccess": {
|
||||
"longName": "org-read-access",
|
||||
"shortName": "r",
|
||||
"isHidden": true
|
||||
"shortName": "r"
|
||||
},
|
||||
"ClientId": {
|
||||
"longName": "client-id",
|
||||
|
|
@ -42,28 +39,23 @@
|
|||
},
|
||||
"AppIDUri": {
|
||||
"longName": "app-id-uri",
|
||||
"shortName": "",
|
||||
"isHidden": true
|
||||
"shortName": ""
|
||||
},
|
||||
"APIClientId": {
|
||||
"longName": "api-client-id",
|
||||
"shortName": "",
|
||||
"isHidden": true
|
||||
"shortName": ""
|
||||
},
|
||||
"Domain": {
|
||||
"longName": "domain",
|
||||
"shortName": "",
|
||||
"isHidden": true
|
||||
"shortName": ""
|
||||
},
|
||||
"TenantId": {
|
||||
"longName": "tenant-id",
|
||||
"shortName": "",
|
||||
"isHidden": true
|
||||
"shortName": ""
|
||||
},
|
||||
"DefaultScope": {
|
||||
"longName": "default-scope",
|
||||
"shortName": "s",
|
||||
"isHidden": true
|
||||
"shortName": "s"
|
||||
},
|
||||
"Authority": {
|
||||
"longName": "authority",
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ namespace ComponentsWebAssembly_CSharp
|
|||
#endif
|
||||
#endif
|
||||
#if (IndividualB2CAuth)
|
||||
builder.Services.AddMsalSpaAuthentication(options =>
|
||||
builder.Services.AddMsalAuthentication(options =>
|
||||
{
|
||||
var authentication = options.ProviderOptions.Authentication;
|
||||
authentication.Authority = "https:////aadB2CInstance.b2clogin.com/qualified.domain.name/MySignUpSignInPolicyId";
|
||||
|
|
@ -43,7 +43,7 @@ namespace ComponentsWebAssembly_CSharp
|
|||
});
|
||||
#endif
|
||||
#if(OrganizationalAuth)
|
||||
builder.Services.AddMsalSpaAuthentication(options =>
|
||||
builder.Services.AddMsalAuthentication(options =>
|
||||
{
|
||||
var authentication = options.ProviderOptions.Authentication;
|
||||
authentication.Authority = "https://login.microsoftonline.com/22222222-2222-2222-2222-222222222222";
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
|
||||
<!--#endif -->
|
||||
<!--#if (IndividualB2CAuth || OrganizationalAuth) -->
|
||||
<script src="_content/Blazor.Msal/AuthenticationService.js"></script>
|
||||
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
|
||||
<!--#endif -->
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<!--#if PWA -->
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
#if (OrganizationalAuth || IndividualB2CAuth || IndividualLocalAuth)
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
#endif
|
||||
#if (OrganizationalAuth)
|
||||
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
|
||||
#endif
|
||||
#if (IndividualB2CAuth)
|
||||
using Microsoft.AspNetCore.Authentication.AzureADB2C.UI;
|
||||
#endif
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
#if (IndividualLocalAuth)
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
// "Instance": "https:////login.microsoftonline.com/",
|
||||
// "Domain": "qualified.domain.name",
|
||||
// "TenantId": "22222222-2222-2222-2222-222222222222",
|
||||
// "ClientId": "11111111-1111-1111-11111111111111111",
|
||||
// },
|
||||
////#endif
|
||||
"Logging": {
|
||||
|
|
|
|||
|
|
@ -58,22 +58,6 @@ namespace Templates.Test
|
|||
using var serveProcess = RunPublishedStandaloneBlazorProject(project);
|
||||
}
|
||||
|
||||
private ProcessEx RunPublishedStandaloneBlazorProject(Project project)
|
||||
{
|
||||
var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
|
||||
AspNetProcess.EnsureDevelopmentCertificates();
|
||||
|
||||
Output.WriteLine("Running dotnet serve on published output...");
|
||||
var serveProcess = ProcessEx.Run(Output, publishDir, DotNetMuxer.MuxerPathOrDefault(), "serve -S");
|
||||
|
||||
// Todo: Use dynamic port assignment: https://github.com/natemcmaster/dotnet-serve/pull/40/files
|
||||
var listeningUri = "https://localhost:8080";
|
||||
Output.WriteLine($"Opening browser at {listeningUri}...");
|
||||
Browser.Navigate().GoToUrl(listeningUri);
|
||||
TestBasicNavigation(project.ProjectName);
|
||||
return serveProcess;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlazorWasmHostedTemplate_Works()
|
||||
{
|
||||
|
|
@ -259,6 +243,76 @@ namespace Templates.Test
|
|||
using var serveProcess = RunPublishedStandaloneBlazorProject(project);
|
||||
}
|
||||
|
||||
public static TheoryData<TemplateInstance> TemplateData => new TheoryData<TemplateInstance>
|
||||
{
|
||||
new TemplateInstance(
|
||||
"blazorwasmhostedaadb2c", "-ho",
|
||||
"-au", "IndividualB2C",
|
||||
"--aad-b2c-instance", "example.b2clogin.com",
|
||||
"-ssp", "b2c_1_siupin",
|
||||
"--client-id", "clientId",
|
||||
"--domain", "my-domain",
|
||||
"--default-scope", "full",
|
||||
"--app-id-uri", "ApiUri",
|
||||
"--api-client-id", "1234123413241324"),
|
||||
new TemplateInstance(
|
||||
"blazorwasmhostedaad", "-ho",
|
||||
"-au", "SingleOrg",
|
||||
"--domain", "my-domain",
|
||||
"--tenant-id", "tenantId",
|
||||
"--client-id", "clientId",
|
||||
"--default-scope", "full",
|
||||
"--app-id-uri", "ApiUri",
|
||||
"--api-client-id", "1234123413241324"),
|
||||
new TemplateInstance(
|
||||
"blazorwasmstandaloneaadb2c",
|
||||
"-au", "IndividualB2C",
|
||||
"--aad-b2c-instance", "example.b2clogin.com",
|
||||
"-ssp", "b2c_1_siupin",
|
||||
"--client-id", "clientId",
|
||||
"--domain", "my-domain"),
|
||||
new TemplateInstance(
|
||||
"blazorwasmstandaloneaad",
|
||||
"-au", "SingleOrg",
|
||||
"--domain", "my-domain",
|
||||
"--tenant-id", "tenantId",
|
||||
"--client-id", "clientId"),
|
||||
};
|
||||
|
||||
public class TemplateInstance
|
||||
{
|
||||
public TemplateInstance(string name, params string[] arguments)
|
||||
{
|
||||
Name = name;
|
||||
Arguments = arguments;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string[] Arguments { get; }
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TemplateData))]
|
||||
public async Task BlazorWasmHostedTemplate_AzureActiveDirectoryTemplate_Works(TemplateInstance instance)
|
||||
{
|
||||
var project = await ProjectFactory.GetOrCreateProject(instance.Name, Output);
|
||||
project.TargetFramework = "netstandard2.1";
|
||||
|
||||
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: instance.Arguments);
|
||||
|
||||
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult));
|
||||
|
||||
var publishResult = await project.RunDotNetPublishAsync();
|
||||
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", project, publishResult));
|
||||
|
||||
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
|
||||
// The output from publish will go into bin/Release/netcoreappX.Y/publish and won't be affected by calling build
|
||||
// later, while the opposite is not true.
|
||||
|
||||
var buildResult = await project.RunDotNetBuildAsync();
|
||||
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", project, buildResult));
|
||||
}
|
||||
|
||||
protected async Task BuildAndRunTest(string appName, Project project, bool usesAuth = false)
|
||||
{
|
||||
using var aspNetProcess = project.StartBuiltProjectAsync();
|
||||
|
|
@ -404,5 +458,22 @@ namespace Templates.Test
|
|||
var testAppSettings = appSettings.ToString();
|
||||
File.WriteAllText(Path.Combine(serverProject.TemplatePublishDir, "appsettings.json"), testAppSettings);
|
||||
}
|
||||
|
||||
|
||||
private ProcessEx RunPublishedStandaloneBlazorProject(Project project)
|
||||
{
|
||||
var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
|
||||
AspNetProcess.EnsureDevelopmentCertificates();
|
||||
|
||||
Output.WriteLine("Running dotnet serve on published output...");
|
||||
var serveProcess = ProcessEx.Run(Output, publishDir, DotNetMuxer.MuxerPathOrDefault(), "serve -S");
|
||||
|
||||
// Todo: Use dynamic port assignment: https://github.com/natemcmaster/dotnet-serve/pull/40/files
|
||||
var listeningUri = "https://localhost:8080";
|
||||
Output.WriteLine($"Opening browser at {listeningUri}...");
|
||||
Browser.Navigate().GoToUrl(listeningUri);
|
||||
TestBasicNavigation(project.ProjectName);
|
||||
return serveProcess;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue