Reputation: 6283
I am new to both NestJS & RxJS. I'm working very hard to write things in an idiomatic RxJS, since one of the purposes of this project is to learn both of them much better, rather than trying to bypass or hack around their APIs.
Anyway, so I have a piece of logic where I am looking up a JWKSet from an OAuth2 server. I use the NestJS HttpService for that, which returns an Observable. I then use that observable to set the result to a ReplaySubject. Then, in my JwtStrategy, I use the secretOrKeyProvider function to subscribe to the ReplaySubject in order to get the value each time an HTTP request comes in.
Now, I'm sure there are tons of things wrong with this approach, since I just barely understand how to work with RxJS. My biggest issue is the last part. When I subscribe to the ReplaySubject in the secretOrKeyProvider function, I immediately unsubscribe. This is because I want to cleanup leftover subscriptions.
Overall, this feels very wrong to me, to subscribe and immediately unsubscribe. I feel like I am doing something wrong. I am seeking a review of this code to learn what improvements I can make.
While all the code below works, my goal is to be guided into making better, more proper use of RxJS.
@Injectable()
export class JwkService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(JwkService.name);
readonly key = new ReplaySubject<string>();
private subscription: Subscription;
constructor(
private httpService: HttpService,
private configService: ConfigService
) {}
onModuleInit(): void {
this.subscription = this.httpService
.get(`${this.configService.get<string>(AUTH_SERVER_HOST)}${jwkUri}`)
.pipe(
map((res: AxiosResponse<JwkSet>) => jwkToPem(res.data.keys[0]))
)
.subscribe({
next: (key) => this.key.next(key),
error: (error: Error) => {
this.logger.error(
'CRITICAL ERROR: Unable to load JWKSet',
ajaxErrorHandler(error)
);
}
});
}
onModuleDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
private readonly logger = new Logger(JwtStrategy.name);
constructor(
private readonly jwkService: JwkService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKeyProvider: (
req: Request,
rawJwt: string,
done: doneFn
) => {
jwkService.key
.subscribe({
next: (value: string) => done(null, value),
error: (error: Error) => {
this.logger.error(
'Error getting JWK key',
ajaxErrorHandler(error)
);
done(error);
}
})
.unsubscribe();
}
});
}
}
Upvotes: 1
Views: 975
Reputation: 3002
If you don't need unsubscribe
, RxJS gives you a few pipes for that:
.get(`${this.configService.get<string>(AUTH_SERVER_HOST)}${jwkUri}`)
.pipe(
take(1),
map((res: AxiosResponse<JwkSet>) => jwkToPem(res.data.keys[0]))
)
...
or
.get(`${this.configService.get<string>(AUTH_SERVER_HOST)}${jwkUri}`)
.pipe(
first(),
map((res: AxiosResponse<JwkSet>) => jwkToPem(res.data.keys[0]))
)
...
first()
, take(1)
pipes do unsubscribe for you if takes first value. If value in any cases don't come, then you can unsubscribe your subscription manually, or just complete the subject.
Upvotes: 2