ngx-grpc icon indicating copy to clipboard operation
ngx-grpc copied to clipboard

Authentication Token

Open isc30 opened this issue 4 years ago • 2 comments

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

isc30 avatar Mar 03 '21 15:03 isc30

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);
    }
}

image

isc30 avatar Mar 03 '21 16:03 isc30

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

smnbbrv avatar Mar 10 '21 15:03 smnbbrv