[英]Blazor WebAssembly + Amazon Cognito
我想設置一個Blazor客戶端應用程序,通過AWS Cognito進行身份驗證。
當我運行應用程序時,我沒有重定向到登錄頁面,而是頁面顯示“正在授權...”幾秒鍾,而我在控制台中收到此錯誤:
The loading of “https://blazorapp.auth.eu-central-1.amazoncognito.com/login?…Q&code_challenge_method=S256&prompt=none&response_mode=query” in a frame is denied by “X-Frame-Options“ directive set to “DENY“.
This error page has no error code in its security info
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
然后,默認的“你好,世界!” 顯示索引頁面(盡管據我了解,根據 App.razor 定義,未經身份驗證的用戶不應該看到它?) 。 如果我單擊“登錄”,我會在控制台中收到相同的錯誤,但幾秒鍾后,Cognito 托管的登錄頁面打開,我可以登錄,我被重定向回我的應用程序,並且應用程序顯示經過身份驗證的用戶信息在右上角,但控制台又有點奇怪:
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[1]
Authorization was successful.
問題 1
我怎樣才能擺脫這些錯誤並讓我的應用程序重定向到 Cognito 登錄頁面而不會有約 10 秒的延遲?
問題2
為什么無論我是否經過身份驗證,我的應用程序中的所有內容始終可見? 就好像 App.razor 中AuthorizeRouteView
下的NotAuthorized
節點App.razor
沒有效果,除非我在這里混淆了一些東西
代碼:
程序.cs
builder.Services.AddOidcAuthentication(options =>
{
options.ProviderOptions.Authority = "https://cognito-idp.{aws-region}.amazonaws.com/{cognito-userpoolid}";
options.ProviderOptions.ClientId = "{cognito-clientid}";
options.ProviderOptions.ResponseType = "code";
options.ProviderOptions.RedirectUri = "https://localhost:44306/authentication/login-callback";
options.ProviderOptions.PostLogoutRedirectUri = "https://localhost:44306/authentication/logout-callback";
});
App.razor (從模板創建,無修改)
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (!context.User.Identity.IsAuthenticated)
{
<RedirectToLogin />
}
else
{
<p>You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
我自己只修改了Program.cs
中對AddOidcAuthentication
的調用,在使用個人用戶帳戶創建 Blazor WebAssembly 應用程序時,Visual Studio 填充了所有其他文件。
我正在努力讓它發揮作用,並非常感謝有關此主題的任何幫助
編輯:
根據@aguafrommars 的回答,我已使用 static 網站托管與 Amazon CloudFront 作為 CDN 將網站發布到 Amazon S3,但是,已發布應用程序的行為與描述的本地行為完全相同
擴展問題:
問題1擴展:
當頁面顯示“正在授權...”時,我只在控制台中收到描述的錯誤,Cognito 托管的 UI 不會呈現,只有當我點擊“登錄”時,我才會被重定向(有很大延遲)到 Cognito 托管的 UI ,或者在沒有重定向的情況下進行身份驗證(如果我之前登錄過),也許這個 GIF 會清除一些東西:
我可能錯了,但是Cognito 托管的 UI 拒絕在 iframe 中呈現的問題不是嗎? 我的應用程序能否像最終那樣重定向到托管的 UI? 現在我必須等待拋出X-Frame-Options
錯誤,點擊“登錄”,等待另一個X-Frame-Options
錯誤拋出,最后我被重定向並且流程成功(在 gif UI 沒有顯示,因為我之前在會話中進行了身份驗證)
問題2擴展:
我想要實現的行為是,如果用戶未經過身份驗證,他們將看不到應用程序的任何部分,而是將它們重定向到 Cognito 托管的 UI,並且只有在經過身份驗證后才能看到任何內容。 我嘗試在MainLayout.razor
中使用Authorize
屬性,但結果始終是空白屏幕,我想提供一些代碼和詳細信息,但我相信該行為受到問題 1中描述的錯誤的影響,這就是為什么我會喜歡先整理一下
我最終從 Cognito 切換到 Auth0 並從 Api 網關的 RestApi 升級到 HttpApi,其中包括內置的 JWT 授權器,我對這個變化非常滿意。 Cognito 最后只是遇到了太多問題,但如果有人決心讓它工作,請檢查@aguafrommars 在接受的答案下的評論。
回應1:
在顯示授權消息時,應用程序會檢查有效身份驗證並設置自動更新令牌 iframe。 如果您查看瀏覽器上的網絡日志,您將看到此時發出的請求。
當應用程序在發行版中運行時,它會更快。
回應 2:
您需要通過添加Authorize
屬性在要保護的頁面上添加授權。
@page "/"
@attribute [Authorize]
有同樣的問題並切換到 Azure B2C 再次解決了問題。 鏈接到 AWS Cognito 作為身份驗證提供程序時,身份驗證庫似乎存在問題。
MS 提出的問題 - https://github.com/dotnet/aspnetcore/issues/22651
我正在回答這個在此處標記為重復的問題...
延遲的原因是等待靜默登錄過程的超時(我相信它有 10 秒的超時),如此處和此處所述
根本原因是 AWS Cognito 不符合 OIDC 標准。 這會導致瀏覽器控制台中出現“'X-Frame-Options' to 'DENY'”錯誤。
直到 Blazor 團隊允許我們從代碼中關閉靜默登錄,解決方案是禁用靜默登錄,如下所示:
將位於 asp.net 存儲庫中的 Blazor 互操作文件下載到本地文件夾。
使用 vs 代碼打開本地文件夾並安裝 typescript、webpack、yarn 等(如果尚未安裝)
npm install -g yarn
npm install -g typescript
npm install -g webpack
然后按如下方式編輯 AuthenticationService.ts 文件(注釋掉靜默登錄功能)。 對不起,長代碼打印。
import { UserManager, UserManagerSettings, User } from 'oidc-client'
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
type ExtendedUserManagerSettings = Writeable<UserManagerSettings & AuthorizeServiceSettings>
type OidcAuthorizeServiceSettings = ExtendedUserManagerSettings | ApiAuthorizationSettings;
function isApiAuthorizationSettings(settings: OidcAuthorizeServiceSettings): settings is ApiAuthorizationSettings {
return settings.hasOwnProperty('configurationEndpoint');
}
interface AuthorizeServiceSettings {
defaultScopes: string[];
}
interface ApiAuthorizationSettings {
configurationEndpoint: string;
}
export interface AccessTokenRequestOptions {
scopes: string[];
returnUrl: string;
}
export interface AccessTokenResult {
status: AccessTokenResultStatus;
token?: AccessToken;
}
export interface AccessToken {
value: string;
expires: Date;
grantedScopes: string[];
}
export enum AccessTokenResultStatus {
Success = 'success',
RequiresRedirect = 'requiresRedirect'
}
export enum AuthenticationResultStatus {
Redirect = 'redirect',
Success = 'success',
Failure = 'failure',
OperationCompleted = 'operationCompleted'
};
export interface AuthenticationResult {
status: AuthenticationResultStatus;
state?: unknown;
message?: string;
}
export interface AuthorizeService {
getUser(): Promise<unknown>;
getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult>;
signIn(state: unknown): Promise<AuthenticationResult>;
completeSignIn(state: unknown): Promise<AuthenticationResult>;
signOut(state: unknown): Promise<AuthenticationResult>;
completeSignOut(url: string): Promise<AuthenticationResult>;
}
class OidcAuthorizeService implements AuthorizeService {
private _userManager: UserManager;
private _intialSilentSignIn: Promise<void> | undefined;
constructor(userManager: UserManager) {
this._userManager = userManager;
}
async trySilentSignIn() {
if (!this._intialSilentSignIn) {
this._intialSilentSignIn = (async () => {
try {
await this._userManager.signinSilent();
} catch (e) {
// It is ok to swallow the exception here.
// The user might not be logged in and in that case it
// is expected for signinSilent to fail and throw
}
})();
}
return this._intialSilentSignIn;
}
async getUser() {
// if (window.parent === window && !window.opener && !window.frameElement && this._userManager.settings.redirect_uri &&
// !location.href.startsWith(this._userManager.settings.redirect_uri)) {
// // If we are not inside a hidden iframe, try authenticating silently.
// await AuthenticationService.instance.trySilentSignIn();
// }
const user = await this._userManager.getUser();
return user && user.profile;
}
async getAccessToken(request?: AccessTokenRequestOptions): Promise<AccessTokenResult> {
const user = await this._userManager.getUser();
if (hasValidAccessToken(user) && hasAllScopes(request, user.scopes)) {
return {
status: AccessTokenResultStatus.Success,
token: {
grantedScopes: user.scopes,
expires: getExpiration(user.expires_in),
value: user.access_token
}
};
} else {
return {
status: AccessTokenResultStatus.RequiresRedirect
};
// try {
// const parameters = request && request.scopes ?
// { scope: request.scopes.join(' ') } : undefined;
// const newUser = await this._userManager.signinSilent(parameters);
// return {
// status: AccessTokenResultStatus.Success,
// token: {
// grantedScopes: newUser.scopes,
// expires: getExpiration(newUser.expires_in),
// value: newUser.access_token
// }
// };
// } catch (e) {
// return {
// status: AccessTokenResultStatus.RequiresRedirect
// };
// }
}
function hasValidAccessToken(user: User | null): user is User {
return !!(user && user.access_token && !user.expired && user.scopes);
}
function getExpiration(expiresIn: number) {
const now = new Date();
now.setTime(now.getTime() + expiresIn * 1000);
return now;
}
function hasAllScopes(request: AccessTokenRequestOptions | undefined, currentScopes: string[]) {
const set = new Set(currentScopes);
if (request && request.scopes) {
for (const current of request.scopes) {
if (!set.has(current)) {
return false;
}
}
}
return true;
}
}
async signIn(state: unknown) {
try {
await this._userManager.clearStaleState();
await this._userManager.signinRedirect(this.createArguments(state));
return this.redirect();
} catch (redirectError) {
return this.error(this.getExceptionMessage(redirectError));
}
// try {
// await this._userManager.clearStaleState();
// await this._userManager.signinSilent(this.createArguments());
// return this.success(state);
// } catch (silentError) {
// try {
// await this._userManager.clearStaleState();
// await this._userManager.signinRedirect(this.createArguments(state));
// return this.redirect();
// } catch (redirectError) {
// return this.error(this.getExceptionMessage(redirectError));
// }
// }
}
async completeSignIn(url: string) {
const requiresLogin = await this.loginRequired(url);
const stateExists = await this.stateExists(url);
try {
const user = await this._userManager.signinCallback(url);
if (window.self !== window.top) {
return this.operationCompleted();
} else {
return this.success(user && user.state);
}
} catch (error) {
if (requiresLogin || window.self !== window.top || !stateExists) {
return this.operationCompleted();
}
return this.error('There was an error signing in.');
}
}
async signOut(state: unknown) {
try {
if (!(await this._userManager.metadataService.getEndSessionEndpoint())) {
await this._userManager.removeUser();
return this.success(state);
}
await this._userManager.signoutRedirect(this.createArguments(state));
return this.redirect();
} catch (redirectSignOutError) {
return this.error(this.getExceptionMessage(redirectSignOutError));
}
}
async completeSignOut(url: string) {
try {
if (await this.stateExists(url)) {
const response = await this._userManager.signoutCallback(url);
return this.success(response && response.state);
} else {
return this.operationCompleted();
}
} catch (error) {
return this.error(this.getExceptionMessage(error));
}
}
private getExceptionMessage(error: any) {
if (isOidcError(error)) {
return error.error_description;
} else if (isRegularError(error)) {
return error.message;
} else {
return error.toString();
}
function isOidcError(error: any): error is (Oidc.SigninResponse & Oidc.SignoutResponse) {
return error && error.error_description;
}
function isRegularError(error: any): error is Error {
return error && error.message;
}
}
private async stateExists(url: string) {
const stateParam = new URLSearchParams(new URL(url).search).get('state');
if (stateParam && this._userManager.settings.stateStore) {
return await this._userManager.settings.stateStore.get(stateParam);
} else {
return undefined;
}
}
private async loginRequired(url: string) {
const errorParameter = new URLSearchParams(new URL(url).search).get('error');
if (errorParameter && this._userManager.settings.stateStore) {
const error = await this._userManager.settings.stateStore.get(errorParameter);
return error === 'login_required';
} else {
return false;
}
}
private createArguments(state?: unknown) {
return { useReplaceToNavigate: true, data: state };
}
private error(message: string) {
return { status: AuthenticationResultStatus.Failure, errorMessage: message };
}
private success(state: unknown) {
return { status: AuthenticationResultStatus.Success, state };
}
private redirect() {
return { status: AuthenticationResultStatus.Redirect };
}
private operationCompleted() {
return { status: AuthenticationResultStatus.OperationCompleted };
}
}
export class AuthenticationService {
static _infrastructureKey = 'Microsoft.AspNetCore.Components.WebAssembly.Authentication';
static _initialized: Promise<void>;
static instance: OidcAuthorizeService;
static _pendingOperations: { [key: string]: Promise<AuthenticationResult> | undefined } = {}
public static init(settings: UserManagerSettings & AuthorizeServiceSettings) {
// Multiple initializations can start concurrently and we want to avoid that.
// In order to do so, we create an initialization promise and the first call to init
// tries to initialize the app and sets up a promise other calls can await on.
if (!AuthenticationService._initialized) {
AuthenticationService._initialized = AuthenticationService.initializeCore(settings);
}
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() {
return AuthenticationService.instance.getUser();
}
public static getAccessToken(options: AccessTokenRequestOptions) {
return AuthenticationService.instance.getAccessToken(options);
}
public static signIn(state: unknown) {
return AuthenticationService.instance.signIn(state);
}
public static async completeSignIn(url: string) {
let operation = this._pendingOperations[url];
if (!operation) {
operation = AuthenticationService.instance.completeSignIn(url);
await operation;
delete this._pendingOperations[url];
}
return operation;
}
public static signOut(state: unknown) {
return AuthenticationService.instance.signOut(state);
}
public static async completeSignOut(url: string) {
let operation = this._pendingOperations[url];
if (!operation) {
operation = AuthenticationService.instance.completeSignOut(url);
await operation;
delete this._pendingOperations[url];
}
return operation;
}
private static async createUserManager(settings: OidcAuthorizeServiceSettings): Promise<UserManager> {
let finalSettings: UserManagerSettings;
if (isApiAuthorizationSettings(settings)) {
const response = await fetch(settings.configurationEndpoint);
if (!response.ok) {
throw new Error(`Could not load settings from '${settings.configurationEndpoint}'`);
}
const downloadedSettings = await response.json();
finalSettings = downloadedSettings;
} else {
if (!settings.scope) {
settings.scope = settings.defaultScopes.join(' ');
}
if (settings.response_type === null) {
// If the response type is not set, it gets serialized as null. OIDC-client behaves differently than when the value is undefined, so we explicitly check for a null value and remove the property instead.
delete settings.response_type;
}
finalSettings = settings;
}
window.sessionStorage.setItem(`${AuthenticationService._infrastructureKey}.CachedAuthSettings`, JSON.stringify(finalSettings));
return AuthenticationService.createUserManagerCore(finalSettings);
}
private static createUserManagerCore(finalSettings: UserManagerSettings) {
const userManager = new UserManager(finalSettings);
userManager.events.addUserSignedOut(async () => {
userManager.removeUser();
});
return userManager;
}
}
declare global {
interface Window { AuthenticationService: AuthenticationService }
}
AuthenticationService.handleCallback();
window.AuthenticationService = AuthenticationService;
然后構建js
yarn build:release
編譯 js 文件后,將AuthenticationService.js
文件復制到 Blazor WASM 應用程序的/wwwroot
目錄中。
然后在 index.html 文件中,注釋掉 MS 腳本並替換為自己的:
<!-- <script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>-->
<script src="AuthenticationService.js"></script>
運行您的應用程序,Cognito 現在將(相對)即時
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.