I do not fully understand how to provide OAuth2 access token from a promise (oidc-client-js) to API code generated with Swagger-CodeGen.
It is easy to provide constant values, but how do I change below to get the user's access token from oidc-client-js? I would like to know the "correct" way. It would be easy enough to stick this token somewhere in a global variable.
@NgModule({
imports: [
CommonModule,
ApiModule.forConfig(() => new Configuration({
accessToken: 'my-access-token' //this can also be a () => string function
}))
],
In normal components with OnInit, I can get the token in a promise from an instance of oidc-client's UserManager. Making these two pieces fit together is what confuses me. One seems like static configuration and the other needs to subscribe to a singleton's promise.
this.userSubscription = this.authService.getUser().subscribe((user) => {
if (user) {
this.access_token = user.access_token;
}
});
Any corrections to things I am doing wrong would also be appreciated. This is my first prototype using Angular.
Update
After applying Ben's suggestion and taking time to understand APP_INITIALIZER (which is marked experimental and very sparsely documented imo), it felt like overkill. I ended with the following custom provider for the Configuration class which gets injected into TypeScript-Angular2 service code generated with Swagger-CodeGen:
providers: [
AuthService,
AuthGuardService,
{
provide: Configuration,
useFactory: (authSvc: AuthService) => new Configuration({accessToken: authSvc.getAccessToken.bind(authSvc)}),
deps: [AuthService],
multi: false
}
]
I changed my AuthService to store the user's latest access_token on the service. The getAccessToken()
method is called from the Swagger-CodeGen generated code and returns the latest jwt for use in HTTP headers. It feels clean and it works. Please let me know if (and why) this is the wrong way to solve my problem.
You need to use the APP_INITIALIZER to bootstrap your API token, take a look at my answer Pass web application context to Angular2 Service to see an example of how to do that.
I think this is a swagger-codegen bug, the property signature should be
accessToken?: string | (() => Promise<string>);
Or simply
accessToken?: (() => Promise<string>);
The reason is that access tokens expire, so every time a call will be made the
client should check if the token has expired and request a new one if so (token refresh), that implies an HTTP query so a promise is best option to handle access tokens. If you check Firebase's Javascript API you'll notice User.getIdToken()
returns a promise because it first checks if current is expired and requests a new one if so.
So the solution I'm using meanwhile is Angular's HTTP Interceptors:
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { AngularFireAuth } from '@angular/fire/auth';
import * as firebase from 'firebase/app';
import { from } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { environment } from '../environments/environment';
@Injectable({
providedIn: 'root'
})
export class UsersApiAuthInterceptorService implements HttpInterceptor {
constructor(private afAuth: AngularFireAuth) { }
intercept(req: HttpRequest<any>, next: HttpHandler) {
if (req.url.startsWith(environment.usersAPIBasePath) && this.afAuth.auth.currentUser) {
return from(this.afAuth.auth.currentUser.getIdToken()).pipe(mergeMap(token => {
console.log('UsersApiAuthInterceptorService got token', token);
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(authReq);
}));
}
else {
return next.handle(req);
}
}
}
What I don't like about this solution is that it will intercept all HTTPClient calls and that's why I had to add if (req.url.startsWith(environment.usersAPIBasePath) ...
but if all your HTTPClient calls are going to be to your API you can remove that part of the conditional.
This is how that app's providers goes in app.module.ts
:
providers: [
...
{ provide: BASE_PATH, useValue: environment.usersAPIBasePath },
{ provide: HTTP_INTERCEPTORS, useClass: UsersApiAuthInterceptorService, multi: true },
],