angular-auth-oidc-client
angular-auth-oidc-client copied to clipboard
[Question]: Cannot log migrating from 13 to 14, with StsConfigLoader and AuthInterceptor
What Version of the library are you using? Library 14.0.0 (also tried with 14.1.5), Angular 14.1.1, rxjs 7.5.0, typescript 4.6.3
Everything was working perfecly fine with this magnific library on Angular 14, with angular-auth-oidc-client v11, v12 and v13. When I updated the library to v14, loggin stopped working. I only had to make the getAccessToken() as observable migration change.
At first I was getting a big, but not infinite loop, in AuthInterceptor's getAccessToken() , which can be avoided with
if (req.url.includes(environment.apiUrl))
But neither way allows me to loggin after, it just redirects to unauthorized after I signIn with this.oidcSecurityService.authorize().
My code, which worked in v11, v12 and v13:
core.module.ts
export const httpLoaderFactory = (httpClient: HttpClient) => {
const envInjector = inject(EnvironmentInjector);
const config$ = httpClient
.get<any>(`${environment.authServerUrl}/tenant/config`)
.pipe(
map((customConfig: any) => {
environment.logo = customConfig.logo;
environment.backgroundImage = customConfig.backgroundImage;
const lang = envInjector.runInContext(() => getLanguageToSet());
const origin = lang === LanguageCodes.English
? customConfig.origin
: `${customConfig.origin}/${lang}`;
const config: OpenIdConfiguration = {
authority: customConfig.stsServer,
redirectUrl: origin,
responseType: "code",
clientId: customConfig.clientId,
scope: "openid profile email inspections",
postLogoutRedirectUri: origin,
silentRenew: true,
silentRenewUrl: `${origin}/silent-renew.html`,
postLoginRoute: getPostLoginRoute(),
forbiddenRoute: "/forbidden",
unauthorizedRoute: "/unauthorized",
maxIdTokenIatOffsetAllowedInSeconds: 20,
logLevel: 1,
historyCleanupOff: true,
disableIatOffsetValidation: true,
};
return config;
})
)
return new StsConfigHttpLoader(config$);
};
@NgModule({
declarations: [
NavComponent,
LoginComponent,
],
imports: [
AuthModule.forRoot({
loader: {
provide: StsConfigLoader,
useFactory: httpLoaderFactory,
deps: [HttpClient],
},
}),
HttpClientModule,
RouterModule,
],
providers: [
AuthService,
AuthGuard,
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true,
},
],
exports: [AuthModule, NavComponent],
})
export class CoreModule {}
auth-interceptor.ts
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private oidcSecurityService: OidcSecurityService;
constructor(private injector: Injector, private router: Router) {}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
let requestToForward = req;
if (this.oidcSecurityService === undefined) {
this.oidcSecurityService = this.injector.get(OidcSecurityService);
}
if (this.oidcSecurityService !== undefined) {
if (req.url.includes(environment.apiUrl)) { // Added in v14 to avoid the big getAccessToken loop
try { // I had to add this try catch in v13, because it stopped the app otherwise
this.oidcSecurityService.getAccessToken().subscribe({ // Before v14, it was not an observable, it was a constant
next: token => {
if (token !== "" && !req.headers.has(InterceptorSkipHeader)) {
const tokenValue = `Bearer ${token}`;
requestToForward = req.clone({
setHeaders: { Authorization: tokenValue },
});
}
},
error: error => console.error(error)
});
} catch { /* */ }
}
}
return next.handle(requestToForward).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) {
this.router.navigateByUrl("/login");
}
return throwError(() => new Error(err.message));
})
);
}
}
app.component.ts
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.sass"],
})
export class AppComponent implements OnInit {
isAuthenticated: boolean;
userData = userData;
constructor(
public oidcSecurityService: OidcSecurityService,
private authService: AuthService
) { }
ngOnInit(): void {
this.oidcSecurityService
.checkAuth()
.subscribe(auth => {
this.isAuthenticated = auth.isAuthenticated;
if (auth.isAuthenticated) {
this.authService.userData.subscribe(data => {
setUserData(data.userData);
this.userData = data.userData;
});
}
});
}
app.component.html
<app-nav *ngIf='!isAuthenticated || userData'>
<router-outlet></router-outlet>
</app-nav>
auth-guard.ts
@Injectable({ providedIn: "root" })
export class AuthGuard implements CanActivate, CanActivateChild, CanLoad {
canActivateChild(
_childRoute: ActivatedRouteSnapshot,
_state: RouterStateSnapshot
): Observable<boolean> {
return this.checkUser();
}
constructor(
private router: Router,
private oidcSecurityService: OidcSecurityService
) {}
canActivate(
_route: ActivatedRouteSnapshot,
_state: RouterStateSnapshot
): Observable<boolean> {
return this.checkUser();
}
canLoad(_state: Route): Observable<boolean> {
return this.checkUser();
}
private checkUser(): Observable<boolean> {
return this.oidcSecurityService.isAuthenticated$.pipe(
map(isAuthorized => {
if (!isAuthorized.isAuthenticated) {
const url = window.location.pathname + window.location.search;
if (!url?.includes("?code"))
localStorage.setItem("page-before-unauthorized", url);
// If it starts with "?code", it means it is logging in, so avoid navigation
if (!localStorage.getItem("page-before-unauthorized")?.includes("?code")
|| !url?.includes("?code")
)
this.router.navigate(["unauthorized"]);
return false;
}
return true;
})
);
}
}
I have the same problem аfter updating to 14 or 15 version. Auth interceptor starts inifinite loop, when query is sending. But if I just emulate observable for StsConfigHttpLoader it works perfect. And it not depends how I get token in interceptor. Did you find decision?
function httpLoaderFactory(http: HttpClient): StsConfigHttpLoader {
// const query = http.get<{ identityServerUrl: string }>( //infinite intercept
// '/api/authorization/getidentityserverconfiguration',
// );
const query = of({ // it works good
identityServerUrl: 'http://localhost:9505',
});
const config$ = query.pipe(
map((config: { identityServerUrl: string }) =>
getOidcConfiguration(appUrl, config.identityServerUrl),
),
);
return new StsConfigHttpLoader(config$);
}
Now I fix problem with fetch() instead httpClient.
I fixed it replacing isAuthenticated$
with awaiting this
async isAuthenticated() {
const state$ = this.oidcSecurityService.isAuthenticated();
return lastValueFrom(state$);
}