angular-auth-oidc-client 在 Ionic Android 应用程序中
angular-auth-oidc-client in a Ionic Android application
我正在尝试在 Android Ionic-Angular 应用程序中使用 angular-auth-oidc-client
针对 MS Identity 服务器进行身份验证。
版本:
angular-auth-oidc-client
11.1.4
@angular
10.0.2
@ionic/angular
5.2.3
电容平台:Android
我在哪里:
- 当 运行 纯网络应用程序(来自桌面浏览器)时身份验证成功
- 在 android 清单中声明了意图过滤器,当授权服务器重定向到 my-app://login-callback(真实 Android 设备)时,应用程序正确打开。
- 使用 Deeplinks 插件,我可以拦截对登录回调的调用,并可以读取包含代码、范围、状态和 session_state 参数的查询字符串。
接下来要做什么?身份验证仍然是错误的。我应该用回调 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(注意 eagerLoadAuthWellKnownEndpoints
和 useRefreshToken
):
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',
},
};
我正在尝试在 Android Ionic-Angular 应用程序中使用 angular-auth-oidc-client
针对 MS Identity 服务器进行身份验证。
版本:
angular-auth-oidc-client
11.1.4
@angular
10.0.2
@ionic/angular
5.2.3
电容平台:Android
我在哪里:
- 当 运行 纯网络应用程序(来自桌面浏览器)时身份验证成功
- 在 android 清单中声明了意图过滤器,当授权服务器重定向到 my-app://login-callback(真实 Android 设备)时,应用程序正确打开。
- 使用 Deeplinks 插件,我可以拦截对登录回调的调用,并可以读取包含代码、范围、状态和 session_state 参数的查询字符串。
接下来要做什么?身份验证仍然是错误的。我应该用回调 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(注意 eagerLoadAuthWellKnownEndpoints
和 useRefreshToken
):
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',
},
};