Okta Angular OpenID - auth guard 防止令牌在回调期间被消耗

Okta Angular OpenID - auth guard preventing tokens from being consumed during callback

我正在学习本教程,它很棒!它展示了如何使用 Okta 进行身份验证。

https://developer.okta.com/blog/2017/04/17/angular-authentication-with-oidc

本教程有一个 HomeComponent 分配给根路由,并根据用户是否登录显示或不显示元素。因此,在 app.component.ts 中,您可以在构造函数中使用以下内容从 url 的部分捕获令牌以存储在存储中:

this.oauthService.loadDiscoveryDocument().then(() => {
    this.oauthService.tryLogin({});
}

并且在 auth.guard.ts 中,您可以:

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (this.oauthService.hasValidIdToken()) {
        return true;
    }

    this.router.navigate(['/home']);
    return false;
}

使用此路由配置:

const appRoutes: Routes = [
    { path: 'search', component: SearchComponent, canActivate: [AuthGuard] },
    { path: 'edit/:id', component: EditComponent, canActivate: [AuthGuard]},
    { path: 'home', component: HomeComponent},
    { path: '', redirectTo: 'home', pathMatch: 'full' },
    { path: '**', redirectTo: 'home' }
];

我的问题

我的设置略有不同,一切的时机都不对。 - 我有一个 LoginComponent,如果您未通过身份验证,您将被重定向到。 - 我将根路由重定向到 AuthGuarded 路由。 - 当我使用 Okta 登录时,this.oauthService.tryLogin({}) 没有及时 运行 来阻止 AuthGuard 将我重定向到 LoginComponent。这会导致包含令牌的 url 部分在我尝试使用它们之前在存储中持续存在。

这是我的:

app.component.ts

constructor(
    ...
    private oauthService: OAuthService) {
    ...
    this.oauthService.loadDiscoveryDocument().then(() => {
      this.oauthService.tryLogin({});
    });
}

auth.guard.ts

canActivate(
  next: ActivatedRouteSnapshot,
  state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
  if (!this.oauthService.hasValidIdToken()) {
    this.router.navigate(['/login']);
  }
  return true;
}

app-routing.module.ts

const routes: Routes = [
  { path: '', redirectTo: '/projects', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  { path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] },
  { path: 'help', component: HelpComponent, canActivate: [AuthGuard] },
  { path: 'settings', component: SettingsComponent, canActivate: [AuthGuard] },
  { path: 'contact', component: ContactComponent },
  { path: '**', component: NotFoundComponent }
];

projects-routing.module.ts

const routes: Routes = [
{ 
  path: 'projects', 
  component: ProjectsComponent, 
  canActivate: [AuthGuard],
  children: [
    ...
  ]
}

如您所见,当我去我的 Okta 网站输入我的用户名和密码时,我被重定向到我的应用程序的根目录,从 loadDiscoveryDocument() 返回的 Promise 被订阅到,但是 auth.guard 将我重定向回登录页面,在我可以让 OAuthService 为我收集并存储它之前从 url 丢失了 id_token 等在存储中。

我的问题

有没有办法在不改变我的应用程序的路由结构的情况下让它工作?根据登录状态,我不需要充当“LoginComponent”和“HomeComponent”的“HomeComponent”。

不幸的是,我无法获得带有重定向的登录按钮以配合我的应用程序路由的组织方式。但是使用 JavaScript SDK,我有了更多的灵活性,并且由于 @MattRaible 在 Okta 开发人员页面上频繁发布博客文章,我能够想出一些对我来说效果很好的东西。我花了很多时间调试 sdk 才弄清楚可以得到我想要的东西的操作顺序,所以希望这对那里的人有所帮助:

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

import { OAuthService } from 'angular-oauth2-oidc/dist';

declare let OktaAuth: any;

@Injectable()
export class AuthenticationService {

  discoveryDocumentLoaded: boolean;

  constructor(
    private oauthService: OAuthService,
    private router: Router) { }

  init() {
    this.oauthService.redirectUri = window.location.origin;
    this.oauthService.clientId = '<client-id>';
    this.oauthService.scope = 'openid profile email';
    this.oauthService.oidc = true;
    this.oauthService.issuer = '<preview-path>';
    this.oauthService.loadDiscoveryDocument()
      .then(() => {
        this.discoveryDocumentLoaded = true;
        this.oauthService.tryLogin({});
      });
  }

  logOut() {
    this.oauthService.logOut();
    this.router.navigate(['/login']);
  }

  loginWithPassword(username: string, password: string) {
    this.oauthService.createAndSaveNonce().then(nonce => {
      const authClient = new OktaAuth({
        url: '<preview-path>'
      });
      authClient.signIn({
        username: username,
        password: password
      })
        .then(response => {
          if (response.status === 'SUCCESS') {
            authClient.token.getWithoutPrompt({
              clientId: '<client-id>',
              responseType: ['id_token', 'token'],
              scopes: ['openid', 'profile', 'email'],
              sessionToken: response.sessionToken,
              nonce: nonce,
              redirectUri: window.location.origin
            })
              .then(tokens => {
                localStorage.setItem('access_token', tokens[1].accessToken);
                this.oauthService.processIdToken(tokens[0].idToken, tokens[1].accessToken);
                this.router.navigate(['/']);
              })
              .catch(console.error);
          } else {
            throw new Error('We cannot handle the ' + response.status + ' status');
          }
        })
        .fail(console.error);
    });
  }

  loadUserProfile() {
    const returnFunc = () => this.oauthService.loadUserProfile()
      .catch(console.log);

    if (this.discoveryDocumentLoaded) {
      return returnFunc();
    } else {
      return this.oauthService.loadDiscoveryDocument()
        .then(returnFunc);
    }
  }

  isLoggedIn() {
    return this.oauthService.hasValidIdToken() && this.oauthService.getIdentityClaims()
  }

}

以下是我在我的应用程序中使用该服务的方式:

app.component.ts

export class AppComponent implements OnInit {
    ...
    constructor(
        ...
        private _auth: AuthenticationService) {
        ...
    }

    ngOnInit() {
        this._auth.init();
    }
    ...
}

profile.component.ts

import { Component, OnInit } from '@angular/core';

import { OktaProfile } from 'app/okta-profile';
import { AuthenticationService } from 'app/authentication.service';

@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.scss']
})
export class ProfileComponent implements OnInit {

  profile: OktaProfile;

  constructor(private _auth: AuthenticationService) { }

  ngOnInit() {
    this._auth.loadUserProfile()
      .then(oktaProfile => this.profile = <OktaProfile>oktaProfile);
  }

}