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 总是在浏览器刷新时显示登录对话框的主要内容,如果未能解决你的问题,请参考以下文章

Android在等待位置时显示进度对话框

文件选择器对话框只能在用户激活问题时显示

如何在从 jquery ajax 调用成功返回时显示 JQuery 对话框

Flutter:在启​​动时显示一个对话框

在关闭建议名称时显示 xlworkbook 保存对话框

Android - 在 AsyncTask 执行时显示进度对话框