auth0 总是在浏览器刷新时显示登录对话框
Posted
技术标签:
【中文标题】auth0 总是在浏览器刷新时显示登录对话框【英文标题】:auth0 always shows login dialog on browser refresh 【发布时间】:2020-02-23 20:13:30 【问题描述】:我正在使用具有通用登录功能的新 auth0-spa-js 库。我完全按照https://auth0.com/docs/quickstart/spa/angular2/01-login 上的指南进行操作,但仍然 - 在浏览器重新加载时client.isAuthenticated()
将始终返回 false 并重定向到登录页面。
这很令人沮丧。
编辑:删除了 github 的链接,并按要求直接在帖子中添加了我的代码
EDIT2:解决方案发布在这篇文章的底部
auth0 配置
应用程序
Allowed Callback URLs: http://localhost:3000/callback
Allowed Web Origins: http://localhost:3000
Allowed Logout URLs: http://localhost:3000
Allowed Origins (CORS): http://localhost:3000
JWT Expiration 36000
API
Token expiration: 86400
Token Expiration For Browser Flows: 7200
不知道这两个部分(应用程序/Api 配置)之间有什么区别,也不知道我在通过正常的通用登录流程时实际使用的是哪一个,但无论如何我都会发布它们。
app.module.ts
import BrowserModule from '@angular/platform-browser';
import NgModule from '@angular/core';
import HttpClientModule from '@angular/common/http';
import AppRoutes from './app.routing';
import AppComponent from './app.component';
import DashboardComponent from './views/dashboard/dashboard.component';
import CallbackComponent from './shared/auth/callback/callback.component';
@NgModule(
declarations: [
AppComponent,
DashboardComponent,
CallbackComponent
],
imports: [
BrowserModule,
AppRoutes,
HttpClientModule,
],
providers: [],
bootstrap: [AppComponent]
)
export class AppModule
app.routing.ts
import Routes, RouterModule from '@angular/router';
import CallbackComponent from './shared/auth/callback/callback.component';
import DashboardComponent from './views/dashboard/dashboard.component';
import AuthGuard from './shared/auth/auth.guard';
const routes: Routes = [
path: '', pathMatch: 'full', component: DashboardComponent, canActivate: [AuthGuard] ,
path: 'callback', component: CallbackComponent ,
path: '**', redirectTo: ''
];
export const AppRoutes = RouterModule.forRoot(routes);
app.component.ts
import Component, OnInit from '@angular/core';
import AuthService from './shared/auth/auth.service';
@Component(
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
)
export class AppComponent implements OnInit
title = 'logic-energy';
constructor(private auth: AuthService)
ngOnInit()
// On initial load, check authentication state with authorization server
// Set up local auth streams if user is already authenticated
this.auth.localAuthSetup();
auth.service.ts
import Injectable from '@angular/core';
import createAuth0Client from '@auth0/auth0-spa-js';
import Auth0Client from '@auth0/auth0-spa-js/dist/typings/Auth0Client';
import environment from 'src/environments/environment';
import from, of, Observable, BehaviorSubject, combineLatest, throwError from 'rxjs';
import tap, catchError, concatMap, shareReplay, take from 'rxjs/operators';
import Router from '@angular/router';
@Injectable(
providedIn: 'root'
)
export class AuthService
// Create an observable of Auth0 instance of client
auth0Client$ = (from(
createAuth0Client(
domain: environment.auth.domain,
client_id: environment.auth.clientId,
redirect_uri: `$window.location.origin/callback`
)
) as Observable<Auth0Client>).pipe(
shareReplay(1), // Every subscription receives the same shared value
catchError(err => throwError(err))
);
// Define observables for SDK methods that return promises by default
// For each Auth0 SDK method, first ensure the client instance is ready
// concatMap: Using the client instance, call SDK method; SDK returns a promise
// from: Convert that resulting promise into an observable
isAuthenticated$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.isAuthenticated())),
tap(res => this.loggedIn = res)
);
handleRedirectCallback$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.handleRedirectCallback()))
);
// Create subject and public observable of user profile data
private userProfileSubject$ = new BehaviorSubject<any>(null);
userProfile$ = this.userProfileSubject$.asObservable();
// Create a local property for login status
loggedIn: boolean = null;
constructor(private router: Router)
// When calling, options can be passed if desired
// https://auth0.github.io/auth0-spa-js/classes/auth0client.html#getuser
getUser$(options?): Observable<any>
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getUser(options))),
tap(user => this.userProfileSubject$.next(user))
);
localAuthSetup()
// This should only be called on app initialization
// Set up local authentication streams
const checkAuth$ = this.isAuthenticated$.pipe(
concatMap((loggedIn: boolean) =>
if (loggedIn)
// If authenticated, get user and set in app
// NOTE: you could pass options here if needed
return this.getUser$();
// If not authenticated, return stream that emits 'false'
return of(loggedIn);
)
);
checkAuth$.subscribe((response: [key: string]: any | boolean) =>
// If authenticated, response will be user object
// If not authenticated, response will be 'false'
this.loggedIn = !!response;
);
login(redirectPath: string = '/')
// A desired redirect path can be passed to login method
// (e.g., from a route guard)
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) =>
// Call method to log in
client.loginWithRedirect(
redirect_uri: `$window.location.origin/callback`,
appState: target: redirectPath
);
);
handleAuthCallback()
// Only the callback component should call this method
// Call when app reloads after user logs in with Auth0
let targetRoute: string; // Path to redirect to after login processsed
const authComplete$ = this.handleRedirectCallback$.pipe(
// Have client, now call method to handle auth callback redirect
tap(cbRes =>
// Get and set target redirect route from callback results
targetRoute = cbRes.appState && cbRes.appState.target ? cbRes.appState.target : '/';
),
concatMap(() =>
// Redirect callback complete; get user and login status
return combineLatest(
this.getUser$(),
this.isAuthenticated$
);
)
);
// Subscribe to authentication completion observable
// Response will be an array of user and login status
// authComplete$.subscribe(([user, loggedIn]) =>
authComplete$.subscribe(([user, loggedIn]) =>
// Redirect to target route after callback processing
this.router.navigate([targetRoute]);
);
logout()
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) =>
// Call method to log out
client.logout(
client_id: environment.auth.clientId,
returnTo: `$window.location.origin`
);
);
getTokenSilently$(options?): Observable<string>
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getTokenSilently(options)))
);
auth.guard.ts
import Injectable from '@angular/core';
import ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, CanActivate from '@angular/router';
import Observable from 'rxjs';
import AuthService from './auth.service';
import tap from 'rxjs/operators';
@Injectable( providedIn: 'root' )
export class AuthGuard implements CanActivate
constructor(private auth: AuthService)
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean|UrlTree> | boolean
return this.auth.isAuthenticated$.pipe(
tap(loggedIn =>
if (!loggedIn)
this.auth.login(state.url);
)
);
callback.component.ts
import Component, OnInit from '@angular/core';
import AuthService from '../auth.service';
@Component(
selector: 'app-callback',
templateUrl: './callback.component.html',
styleUrls: ['./callback.component.scss']
)
export class CallbackComponent implements OnInit
constructor(private auth: AuthService)
ngOnInit()
this.auth.handleAuthCallback();
通过检查 devtools 中的网络选项卡,我可以看到进行了以下调用:
登录前:
authorize
使用多个查询参数=>返回 HTTP 200,正文为空。
login
=> 返回登录页面
登录后:
authorize
=> 返回 HTTP 302 空正文
authorize
再次(使用一组不同的参数)=> 返回 HTTP 302 空正文
再次authorize
(使用另一组不同的参数)=>返回 HTTP 302 空正文
callback
=> 返回 HTTP 302 空正文
callback
=> 使用我的回调 html 返回 HTTP 200
注意:每隔一段时间它就会停在这里并且不会重定向到 root,这有点奇怪。
回调重定向后:
authorize
=> 返回 HTTP 200 空正文
token
=> 接收有效令牌。我可以对其进行Base64解码,看起来还可以(除了nonce
属性中的一些垃圾)
在浏览器中点击刷新,会重复这个过程
我已经仔细检查了 auth0 配置。这在使用旧 auth0-js 的反应应用程序上按预期工作,我使用相同的 client_id 并配置了相同的 url。
我做错了什么?是否有我必须执行的手动步骤,但文档中没有描述?我是否必须迁移到较旧的 auth0-js 库才能使其正常工作?
更新
我在 auth0-spa-js 中设置了一些断点,并且我看到当应用程序启动时,它会尝试运行 getTokenSilently()
,但它总是以 "login_required"
拒绝承诺。
即使在登录后,它也会先调用 url 并拒绝(即使 http 请求返回 HTTP 200,因为响应的正文为空?),然后它会尝试内部缓存并通过。
只要我不刷新,auth0 就会使用缓存中的令牌,但如果它尝试从 http 进行验证,它会立即抛出。
我看到一件事,每次getTokenSilently()
未从缓存中获取时都会运行以下代码:
stateIn = encodeState(createRandomString());
nonceIn = createRandomString();
code_verifier = createRandomString();
return [4 /*yield*/, sha256(code_verifier)];
换句话说,它总是询问 auth0 后端是否基于完全随机的字符串进行了身份验证。如果这是允许它识别我和我的会话的原因,它不应该将其中一些存储在浏览器中吗?
更新 2/解决方案
嗯... Chrome 插件“Privacy Badger”似乎可以防止存储 cookie,如果您通过其他浏览器浏览该站点(当 chrome 打开时),它实际上也会对该站点产生影响。它实际上在处理它的那一刻就清除了会话。上面的代码有效,我只需要调整插件。呃……
如果我不是唯一一个忘记安装了哪些扩展的人,我将把这个问题留在这里,所以其他人可能不会浪费一整天的时间来调试不需要调试的东西。
【问题讨论】:
请在 app.component.ts 和 auth.service.ts 中分享您的代码。请务必删除这些文件中可能包含的任何敏感内容。 完成,尽管这与来自 auth0 的 github 存储库中的代码完全相同:github.com/auth0-samples/auth0-angular-samples/tree/master/… 您可以查看的一件事是您的 Auth0 配置的应用程序和 Api 部分中的令牌过期超时。确保它们没有过期,否则客户端会将您退回到 Auth0 以重新进行身份验证。 您可以发布您的应用程序路由文件吗?您是否有可能守卫了一条不需要守卫的路线?在我的应用程序中,我将 AuthService 添加为 APP_INITIALIZER 与 app.component 相比,因此我知道 localAuthSetup 是在我的应用程序启动之前运行的第一件事之一。我不知道您的用例是否需要此功能,但我发现它很有帮助。 完成,但目前只有一页,该页应该在身份验证之后。 【参考方案1】:我刚刚注意到您没有为您的回叫注册路由:
const routes: Routes = [
path: '', pathMatch: 'full', component: DashboardComponent, canActivate: [AuthGuard] ,
path: 'callback', component: CallbackComponent ,
path: '**', redirectTo: ''
];
https://auth0.com/docs/quickstart/spa/angular2/01-login#handle-login-redirects
【讨论】:
我唯一拥有的 cookie 是一个布尔值auth0.is.authenticated
,但是当我重新加载浏览器并且 client.isAuthenticated()
返回 false 时,它会被重置。这是你的意思吗?
auth0.is.authenticated 在 client.loginWithRedirect() 或 client.getTokenSilently() 成功时设置为 true。但是 client.isAuthenticated() 只是检查是否有用户。您的应用程序中是否有任何东西可以清除 auth0 cookie。此外,cookie 的有效期应为 1 天。这是你看到的吗?
我的应用程序或多或少是空的,除了 auth0 设置。但是,我确实在不同的浏览器中看到了一些差异。 Chrome 让我登录一次,并在重新加载时要求重新登录。边缘也是如此。 Firefox 让我登录并在需要再次登录之前持续登录一次重新加载。 Opera 从不让我传递我的回调 url。也许是因为上面原始帖子中发布的错误?
奇怪的问题。您计算机上的日期和时间是否正确?
奇怪,我确定我已将系统设置为自动接收互联网时间。这消除了错误消息,但我仍然需要在每次重新加载时登录。 :-( 我将从帖子中删除错误消息,以免引起人们的困惑。以上是关于auth0 总是在浏览器刷新时显示登录对话框的主要内容,如果未能解决你的问题,请参考以下文章