akveo / nebular

:boom: Customizable Angular UI Library based on Eva Design System :new_moon_with_face::sparkles:Dark Mode
https://akveo.github.io/nebular
MIT License
8.06k stars 1.51k forks source link

NbOAuth2AuthStrategy with Code Flow + PKCE #2071

Open sadeqhatami opened 5 years ago

sadeqhatami commented 5 years ago

hi i want implement NbOAuth2AuthStrategy with Code Flow + PKCE

my code is

 strategies: [
      NbOAuth2AuthStrategy.setup({
        name: 'identity',
        baseEndpoint: "http://localhost:5000/connect/",
        clientId: "clientid",
        clientAuthMethod: NbOAuth2ClientAuthMethod.NONE,
        redirect: {
          success: "/",
          //failure: "string"
        },
        authorize: {
          scope: 'openid profile email',
          redirectUri: 'http://localhost:4200/#/pages/callback',
          responseType: NbOAuth2ResponseType.CODE
        },
        token: {
          endpoint: 'token',
          grantType: NbOAuth2GrantType.AUTHORIZATION_CODE,
          class: NbAuthJWTToken
        }
      }),
    ],

but this code dont generate

code_challenge code_challenge_method nonce

i want do it like this linke how i can do it ?

ramax495 commented 4 years ago

Any updates on this topic ?

herve-brun commented 4 years ago

@ramax495 Did you find any solution for this problem ?

sadeqhatami commented 4 years ago

@herve-brun Unfortunately, I could not find a solution to this problem,so I used this library

dizco commented 4 years ago

I came upon this issue while trying to integrate Okta Authorization Code + PKCE into ngx-admin... Figured out a way with https://github.com/okta/okta-angular If anybody is interested here is my solution:

import { Inject, Injectable } from '@angular/core';
import { forkJoin, from, Observable, of as observableOf } from 'rxjs';
import { map, switchMap, catchError, mapTo } from 'rxjs/operators';
import {
  NbOAuth2AuthStrategy,
  NbOAuth2ResponseType,
  NbAuthOAuth2JWTToken,
  NbOAuth2AuthStrategyOptions,
  NbAuthStrategyClass,
  NbAuthResult,
  auth2StrategyOptions,
} from '@nebular/auth';
import { HttpClient } from '@angular/common/http';
import { NB_WINDOW } from '@nebular/theme';
import { ActivatedRoute } from '@angular/router';
import { OktaAuthService, UserClaims } from '@okta/okta-angular';

export interface OktaToken {
  user: UserClaims;
  idToken: string;
  accessToken: string;
}

export class OktaToken extends NbAuthOAuth2JWTToken {
  // let's rename it to exclude name clashes
  static NAME = 'nb:auth:okta:token';

  protected readonly token: OktaToken;

  getValue(): string {
    return this.token.accessToken;
  }
}

@Injectable()
export class OktaAuthStrategy extends NbOAuth2AuthStrategy {
  static setup(options: NbOAuth2AuthStrategyOptions): [NbAuthStrategyClass, NbOAuth2AuthStrategyOptions] {
    return [OktaAuthStrategy, options];
  }

  protected redirectResultHandlers: { [key: string]: Function } = {
    [NbOAuth2ResponseType.CODE]: () => {
      return from(this.oktaAuth.handleAuthentication())
        .pipe(
          switchMap(() => {
            return forkJoin({
              user: this.oktaAuth.getUser(),
              idToken: this.oktaAuth.getIdToken(),
              accessToken: this.oktaAuth.getAccessToken(),
            }) as Observable<OktaToken>;
          }),
          map((res) => {
            return new NbAuthResult(
              true,
              {},
              this.getOption('redirect.success'),
              [],
              this.getOption('defaultMessages'),
              this.createToken(res, this.getOption(`${module}.requireValidToken`)));
          }),
          catchError((e) => {
            console.error('Caught authentication error', e);
            return observableOf(
              new NbAuthResult(
                false,
                this.route.snapshot.queryParams,
                this.getOption('redirect.failure'),
                this.getOption('defaultErrors'),
                [],
              ));
          }),

        );
    },
  };

  protected defaultOptions: NbOAuth2AuthStrategyOptions = auth2StrategyOptions;

  constructor(protected http: HttpClient,
              protected route: ActivatedRoute,
              protected oktaAuth: OktaAuthService,
              @Inject(NB_WINDOW) protected window: any) {
    super(http, route, window);
  }

  authenticate(data?: any): Observable<NbAuthResult> {
    return this.isRedirectResult()
      .pipe(
        switchMap((result: boolean) => {
          if (!result) {
            this.oktaAuth.login();
            return observableOf(new NbAuthResult(true));
          }
          return this.getAuthorizationResult();
        }),
      );
  }

  logout(): Observable<NbAuthResult> {
    return from(this.oktaAuth.logout()).pipe(
      mapTo(new NbAuthResult(true)),
    );
  }
}

In core.module.ts

export const NB_CORE_PROVIDERS = [
  // Other providers..

  OktaAuthModule,
  {
    provide: OKTA_CONFIG, useValue: config,
  },
  ...NbAuthModule.forRoot({
    strategies: [
      OktaAuthStrategy.setup({ // Uses Okta's Auth service under the hood
        name: 'okta',
        clientId: '',
        authorize: {
          responseType: NbOAuth2ResponseType.CODE,
        },
        token: {
          class: OktaToken,
        },
      }),
    ],
    forms: {
      login: {
        strategy: 'okta',
      },
      logout: {
        strategy: 'okta',
      },
    },
  }).providers,
  OktaAuthStrategy,

  // Other providers...
]
ColinM9991 commented 3 years ago

@herve-brun Unfortunately, I could not find a solution to this problem,so I used this library

Do you mind sharing your code for this implementation?

dizco commented 3 years ago

@ColinM9991 here is my implementation of Auth0 with angular-auth-oidc-client v11.2.1. Hope this helps

import { Inject, Injectable } from '@angular/core';
import { Observable, of, of as observableOf, throwError } from 'rxjs';
import { map, switchMap, catchError } from 'rxjs/operators';
import {
  NbOAuth2AuthStrategy,
  NbOAuth2ResponseType,
  NbAuthOAuth2JWTToken,
  NbOAuth2AuthStrategyOptions,
  NbAuthStrategyClass,
  NbAuthResult,
  auth2StrategyOptions,
} from '@nebular/auth';
import { HttpClient, HttpParams } from '@angular/common/http';
import { NB_WINDOW } from '@nebular/theme';
import { ActivatedRoute } from '@angular/router';
import { OidcSecurityService, PublicConfiguration } from 'angular-auth-oidc-client';

export interface Auth0Claims {
  email: string;
  email_verified: boolean;
  family_name?: string;
  given_name?: string;
  locale?: string;
  name: string;
  nickname: string;
  picture: string;
  sub: string;
  updated_at: string;
}

export interface Auth0Token {
  user: Auth0Claims;
  idToken: string;
  accessToken: string;
}

export class Auth0JWTToken extends NbAuthOAuth2JWTToken {
  // let's rename it to exclude name clashes
  static NAME = 'nb:auth:auth0:token';

  protected readonly token: Auth0Token;

  getValue(): string {
    return this.token.accessToken;
  }
}

@Injectable()
export class Auth0AuthStrategy extends NbOAuth2AuthStrategy {
  static setup(options: NbOAuth2AuthStrategyOptions): [NbAuthStrategyClass, NbOAuth2AuthStrategyOptions] {
    return [Auth0AuthStrategy, options];
  }

  protected redirectResultHandlers: { [key: string]: Function } = {
    [NbOAuth2ResponseType.CODE]: () => {
      return this.oidcService.checkAuth()
        .pipe(
          switchMap((isAuthenticated) => {
            if (isAuthenticated) {
              return this.oidcService.userData$;
            }
            return throwError('Authentication error');
          }),
          map((user) => ({
            user,
            idToken: this.oidcService.getIdToken(),
            accessToken: this.oidcService.getToken(),
          } as Auth0Token)),
          map((res) => {
            return new NbAuthResult(
              true,
              {},
              this.getOption('redirect.success'),
              [],
              this.getOption('defaultMessages'),
              this.createToken(res, this.getOption(`${module}.requireValidToken`)));
          }),
          catchError((e) => {
            console.error('Caught authentication error', e);
            return observableOf(
              new NbAuthResult(
                false,
                this.route.snapshot.queryParams,
                this.getOption('redirect.failure'),
                this.getOption('defaultErrors'),
                [],
              ));
          }),

        );
    },
  };

  protected defaultOptions: NbOAuth2AuthStrategyOptions = auth2StrategyOptions;

  constructor(protected http: HttpClient,
              protected route: ActivatedRoute,
              protected oidcService: OidcSecurityService,
              @Inject(NB_WINDOW) protected window: any) {
    super(http, route, window);
  }

  authenticate(data?: any): Observable<NbAuthResult> {
    return this.isRedirectResult()
      .pipe(
        switchMap((result: boolean) => {
          if (!result) {
            this.oidcService.authorize({
              customParams: {
                // Audience is required if we want to receive JWT tokens.
                // If not sent, we receive opaque access tokens.
                // See https://auth0.com/docs/api/authentication#authorization-code-flow-with-pkce
                audience: '<audience here>',
                // prompt: 'login',
              },
            });
            return observableOf(new NbAuthResult(true));
          }
          return this.getAuthorizationResult();
        }),
      );
  }

  logout(): Observable<NbAuthResult> {
    // 1st logout from OidcSecurityService level
    // 2nd logout from Auth0
    // 3rd, on callback, logout from NbAuthService

    return this.isLogoutRedirect().pipe(
      switchMap((isRedirect) => {
        if (!isRedirect) {
          this.performLogout();
        }
        return of(new NbAuthResult(true));
      }),
    );
  }

  private isLogoutRedirect(): Observable<boolean> {
    return of(this.window.location.href === this.oidcService.configuration.configuration.postLogoutRedirectUri);
  }

  private performLogout(): void {
    // Auth0 does not expose end_session_endpoint in the discovery document (sadly they're not spec compliant), so we must do it manually.
    // First we will logoff locally, then we will navigate to auth0 to finish the logout

    this.oidcService.logoff();

    // Leave this check for future proofing if ever auth0 exposes end_session
    if (!this.oidcService.configuration.wellknown.endSessionEndpoint) {
      // No end session was set, craft our own logout url
      this.window.location.href = Auth0AuthStrategy.buildLogoutUrl(this.oidcService.configuration);
    }
  }

  /**
   * @see https://auth0.com/docs/api/authentication#logout
   */
  private static buildLogoutUrl(config: PublicConfiguration): string {
    let logoutUrl = Auth0AuthStrategy.ensureTrailingSlash(config.configuration.stsServer) + 'v2/logout';

    let params = new HttpParams();
    params = params.set('client_id', config.configuration.clientId);

    if (config.configuration.postLogoutRedirectUri) {
      params = params.append('returnTo', config.configuration.postLogoutRedirectUri);
    }

    logoutUrl += `?${params}`;

    return logoutUrl;
  }

  private static ensureTrailingSlash(str: string): string {
    return str.endsWith('/') ? str : str + '/';
  }
}
ColinM9991 commented 3 years ago

Thank you @dizco.

Looks like I was along the right lines, I just need to factor in OIDC and login redirects now which shouldn't be too bad.

xmlking commented 3 years ago

@dizco I followed your instructions, invalid accessToken and IDToken as shown below: image my code: https://github.com/xmlking/yeti/blob/develop/libs/core/src/lib/services/okta-auth.strategy.ts https://github.com/xmlking/yeti/blob/develop/libs/core/src/lib/core.module.ts wonder if someone can help where I am doing wrong...

dizco commented 3 years ago

@xmlking can you log the OktaToken before you feed it to the map()? The error you're getting comes from https://github.com/akveo/nebular/blob/656ed0fa463aa35c62926de5ec5f17ffeea1494e/src/framework/auth/services/token/token.ts#L78

This tells me that this.oktaAuth.getAccessToken() probably doesn't return what you expect

xmlking commented 3 years ago

Yes , I tried. this.oktaAuth.getAccessToken() is printed as ‘A’ and IDToken as ‘g’ , only single character, which is random every time I SSO. The this.oktaAuth.getUser() call works fine