ngx-grpc
ngx-grpc copied to clipboard
Authentication Token
Hi, I'm trying to automatically send a token with every GRPC call after the client has logged in. Is there any built-in way to do this? If not, would a custom Interceptor to set the metadata be enough for the job? Maybe it's worth mentioning this usage in the docs?
Thanks
In case anyone is interested, this works well with ASP.NET Authentication (and with technically any Bearer token):
import { Injectable } from '@angular/core';
import { GrpcEvent, GrpcMessage, GrpcRequest } from '@ngx-grpc/common';
import { GrpcHandler, GrpcInterceptor } from '@ngx-grpc/core';
import { Observable } from 'rxjs';
@Injectable()
export class GrpcAuthenticationInjector implements GrpcInterceptor
{
intercept<Q extends GrpcMessage, S extends GrpcMessage>(
request: GrpcRequest<Q, S>,
next: GrpcHandler
): Observable<GrpcEvent<S>>
{
const token = 12345; // take it from the store
request.requestMetadata.set('Authorization', `Bearer ${token}`);
return next.handle(request);
}
}

Hi @isc30
answering multiple questions, sorry for delay :)
- there is no "standard" solution, that is offered by ngx-grpc and I doubt there would be one. The OAuth implementations often differ, there could also be a refresh token logic required, etc. In our projects we use the adapted version of interceptor from ngx-auth
- yes, the interceptor is a good place for implementing this logic. Your solution looks good as well
Here is the example implementation that I mentioned above (inspired by https://github.com/serhiisol/ngx-auth/blob/master/src/auth.interceptor.ts)
import { Injectable, Injector } from '@angular/core';
import { GrpcEvent, GrpcMessage, GrpcRequest, GrpcStatusEvent } from '@ngx-grpc/common';
import { GrpcHandler, GrpcInterceptor } from '@ngx-grpc/core';
import { StatusCode } from 'grpc-web';
import { Observable, of, Subject, throwError } from 'rxjs';
import { first, map, switchMap } from 'rxjs/operators';
import { AuthError } from '../../../proto/.../auth.pb';
import { AuthService } from '../auth/auth.service';
import { Oauth2Service } from '../auth/oauth2.service';
@Injectable({
providedIn: 'root',
})
export class GrpcAuthInterceptor implements GrpcInterceptor {
private refreshInProgress = false;
private refreshSubject = new Subject<boolean>();
constructor(
private authService: AuthService,
private injector: Injector,
) {
}
intercept<Q extends GrpcMessage, S extends GrpcMessage>(request: GrpcRequest<Q, S>, next: GrpcHandler): Observable<GrpcEvent<S>> {
// skip for token operations
if (this.injector.get(Oauth2Service).isTokenResponse(request.responseClass)) {
return next.handle(request);
}
const doRequest = this.refreshInProgress ? this.waitUntilRefresh(request) : this.addToken(request);
return doRequest.pipe(
switchMap(req => next.handle(req)),
switchMap(event => {
if (event instanceof GrpcStatusEvent) {
if (event.statusCode === StatusCode.UNAUTHENTICATED) {
return this.loginRequired(event);
} else if (event.statusCode === StatusCode.PERMISSION_DENIED) {
switch (Number(event.metadata.get('reason'))) {
case AuthError.aeTokenExpired: return this.refresh(request, event);
case AuthError.aeMissingRequiredScope: return of(event); // pass through to the error handler
case AuthError.aeTokenInvalid: return this.loginRequired(event);
default: return this.loginRequired(event);
}
}
}
return of(event);
}),
);
}
private loginRequired(event: GrpcStatusEvent) {
this.authService.logout();
return throwError(event);
}
private refresh<Q extends GrpcMessage, S extends GrpcMessage>(request: GrpcRequest<Q, S>, event: GrpcStatusEvent) {
if (!this.refreshInProgress) {
this.refreshInProgress = true;
this.injector.get(Oauth2Service).refreshTokens().subscribe(
() => {
this.refreshInProgress = false;
this.refreshSubject.next(true);
},
() => {
this.refreshInProgress = false;
this.refreshSubject.next(false);
},
);
}
return this.retryAfterRefresh(request, event);
}
private addToken<Q extends GrpcMessage, S extends GrpcMessage>(request: GrpcRequest<Q, S>) {
return this.authService.getAccessToken().pipe(
first(),
map((token: string) => {
if (token) {
request.requestMetadata = request.requestMetadata.clone();
request.requestMetadata.set('Authorization', `Bearer ${token}`);
}
return request;
}),
);
}
private waitUntilRefresh<Q extends GrpcMessage, S extends GrpcMessage>(request: GrpcRequest<Q, S>) {
return this.refreshSubject.pipe(first(), switchMap(ok => ok ? this.addToken(request) : throwError(request)));
}
private retryAfterRefresh<Q extends GrpcMessage, S extends GrpcMessage>(request: GrpcRequest<Q, S>, event: GrpcEvent<S>) {
return this.refreshSubject.pipe(
first(),
switchMap(ok => ok ? this.injector.get<GrpcHandler>(GrpcHandler).handle(request) : of(event)),
);
}
}
This interceptor after some adaptions could potentially be used with the rest of ngx-auth as well