angular-auth-oidc-client 在 Ionic Android 应用程序中

angular-auth-oidc-client in a Ionic Android application

我正在尝试在 Android Ionic-Angular 应用程序中使用 angular-auth-oidc-client 针对 MS Identity 服务器进行身份验证。

版本:

电容平台:Android

我在哪里:

接下来要做什么?身份验证仍然是错误的。我应该用回调 queryString 调用什么?

我发现这个 CallBackService 似乎符合我的需要,但不幸的是它不是库的一部分 public API :/

请注意此解决方案仅适用于刷新令牌(在 conf 中设置 useRefreshToken: true)。我无法使用 silentRenewUrl 使其正常工作(还没有?)

首先,AppComponent:

export class AppComponent implements OnInit, OnDestroy {
  currentUser: KeycloakUser;

  private deeplinksRouteSubscription: Subscription;

  constructor(
    private deeplinks: Deeplinks,
    private navController: NavController,
    private platform: Platform,
    private uaa: UaaService,
    private changedetector: ChangeDetectorRef
  ) {}

  async ngOnInit() {
    await this.platform.ready();
    console.log('PLATFORMS: ' + this.platform.platforms());

    if (this.platform.is('capacitor')) {
      this.setupDeeplinks();
      const { SplashScreen, StatusBar } = Plugins;
      StatusBar.setStyle({ style: StatusBarStyle.Light });
      SplashScreen.hide();
    }

    await this.initUaa();
  }

  ngOnDestroy() {
    this.deeplinksRouteSubscription.unsubscribe();
  }

  login() {
    this.uaa.login();
  }

  logout() {
    this.uaa.logout();
  }

  private setupDeeplinks() {
    this.deeplinks.routeWithNavController(this.navController, {}).subscribe(
      (match) =>
        this.navController
          .navigateForward(match.$link.path + '?' + match.$link.queryString)
          .then(async () => await this.initUaa()),
      (nomatch) =>
        console.error(
          "Got a deeplink that didn't match",
          JSON.stringify(nomatch)
        )
    );
  }

  private async initUaa(): Promise<void> {
    await this.uaa.init();

    this.uaa.currentUser$.subscribe((u) => {
      if (this.currentUser !== u) {
        this.currentUser = u;
        this.changedetector.detectChanges();
      }
    });
  }
}

现在,我使用 UAA 服务将 Keycloak ID 令牌转换为用户对象。实际初始化发生在 onBackOnline():

import { Injectable, OnDestroy } from '@angular/core';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import {
  BehaviorSubject,
  fromEvent,
  merge,
  Observable,
  Subscription,
} from 'rxjs';
import { map } from 'rxjs/operators';
import { KeycloakUser } from './domain/keycloak-user';

@Injectable({ providedIn: 'root' })
export class UaaService implements OnDestroy {
  private user$ = new BehaviorSubject<KeycloakUser>(KeycloakUser.ANONYMOUS);
  private userdataSubscription: Subscription;

  constructor(private oidcSecurityService: OidcSecurityService) {
    console.log(
      `Starting UaaService in ${navigator.onLine ? 'online' : 'offline'} mode`
    );
    merge<boolean>(
      fromEvent(window, 'offline').pipe(
        map((): boolean => {
          console.log('Switching UaaService to offline mode');
          return true;
        })
      ),
      fromEvent(window, 'online').pipe(
        map((): boolean => {
          console.log('Switching UaaService to online mode');
          return false;
        })
      )
    ).subscribe((isOffline: boolean) => {
      if (isOffline) {
        this.onOffline();
      } else {
        this.onBackOnline();
      }
    });
  }

  public ngOnDestroy() {
    this.userdataSubscription.unsubscribe();
  }

  public async init(): Promise<boolean> {
    if (!navigator.onLine) {
      this.user$.next(KeycloakUser.ANONYMOUS);
      return false;
    }

    const user = await this.onBackOnline();
    return !!user.sub;
  }

  private async onBackOnline(): Promise<KeycloakUser> {
    const isAlreadyAuthenticated = await this.oidcSecurityService
      .checkAuth()
      .toPromise()
      .catch(() => false);

    const user = UaaService.fromToken(
      this.oidcSecurityService.getPayloadFromIdToken()
    );
    console.log('UaaService::onBackOnline', isAlreadyAuthenticated, user);

    this.userdataSubscription?.unsubscribe();
    this.userdataSubscription = this.oidcSecurityService.isAuthenticated$.subscribe(
      () =>
        this.user$.next(
          UaaService.fromToken(this.oidcSecurityService.getPayloadFromIdToken())
        )
    );

    return user;
  }

  private static fromToken = (idToken: any) =>
    idToken?.sub
      ? new KeycloakUser({
          sub: idToken.sub,
          preferredUsername: idToken.preferred_username,
          roles: idToken?.resource_access?.['tahiti-devops']?.roles || [],
        })
      : KeycloakUser.ANONYMOUS;

  private onOffline() {
    this.userdataSubscription?.unsubscribe();
  }

  get currentUser$(): Observable<KeycloakUser> {
    return this.user$;
  }

  public login(): void {
    this.oidcSecurityService.authorize();
  }

  public logout(): boolean {
    this.oidcSecurityService.logoff();

    if (this.user$.value !== KeycloakUser.ANONYMOUS) {
      this.user$.next(KeycloakUser.ANONYMOUS);
      return true;
    }

    return false;
  }
}

这是我使用的 conf(注意 eagerLoadAuthWellKnownEndpointsuseRefreshToken):

import { LogLevel } from 'angular-auth-oidc-client';

export const environment = {
  production: false,
  openIdConfiguration: {
    // https://github.com/damienbod/angular-auth-oidc-client/blob/master/docs/configuration.md
    clientId: 'tahiti-devops',
    forbiddenRoute: '/settings',
    eagerLoadAuthWellKnownEndpoints: false,
    ignoreNonceAfterRefresh: true, // Keycloak sends refresh_token with nonce
    logLevel: LogLevel.Warn,
    postLogoutRedirectUri: 'com.c4-soft://device/cafe-skifo',
    redirectUrl: 'com.c4-soft://device/cafe-skifo',
    renewTimeBeforeTokenExpiresInSeconds: 10,
    responseType: 'code',
    scope: 'email openid offline_access roles',
    silentRenew: true,
    // silentRenewUrl: 'com.c4soft.mobileapp://cafe-skifo/silent-renew-pkce.html',
    useRefreshToken: true,
    stsServer: 'https://laptop-jerem:8443/auth/realms/master',
    unauthorizedRoute: '/settings',
  },
};