Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication Token #51

Open
isc30 opened this issue Mar 3, 2021 · 2 comments
Open

Authentication Token #51

isc30 opened this issue Mar 3, 2021 · 2 comments

Comments

@isc30
Copy link

isc30 commented Mar 3, 2021

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
Copy link
Author

isc30 commented Mar 3, 2021

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

@smnbbrv
Copy link
Owner

smnbbrv commented Mar 10, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants