Issue
I'm trying to use angular-auth-oidc-client
in an Android Ionic-Angular app authenticating against MS Identity server.
Versions:
angular-auth-oidc-client
11.1.4
@angular
10.0.2
@ionic/angular
5.2.3
Capacitor platform: Android
Where I am:
- Authentication is successful when running plain web app (from desktop browser)
- An intentent filter is declared in android manifest and the app correctly opens when authorization-server redirects to my-app://login-callback (real Android device).
- Using Deeplinks plugin, I can intercept calls to the login callback and can read the query-string containing code, scope, state and session_state params.
What to do next? The authentication remains false. What should I call with the callback queryString?
I found this CallBackService
which seems to match my need but is unfortunately not part of the lib public API :/
Solution
Please note this solution works with refresh-token only (set useRefreshToken: true
in conf). I couldn't get it work properly using silentRenewUrl
(yet?)
First, the 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();
}
});
}
}
Now, the UAA service I use to turn Keycloak ID tokens into user objects. Actual initialisation occurs in 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;
}
}
And this is the conf I use (note eagerLoadAuthWellKnownEndpoints
and 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',
},
};
Answered By - ch4mp
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.