javascriptangularopenid-connectoktaoidc-client-js

oidc-client to configure discovery documentation from the local host or other URL


Is their any way to configure discovery document from local host before the login using OIDC-Client in angular 8 application.

I have this manager which is a helper call for the OIDC client

export class AuthenticationService {

  @Output() initialized: boolean = false;

  static USER_LOADED_EVENT = "USER_LOADED";
  static USER_UNLOADED_EVENT = "USER_UNLOADED";
  //static USER_SIGNED_OUT_EVENT = "USER_SIGNED_OUT";
  //static USER_EXPIRED_EVENT = "USER_EXPIRED";
  static USER_RESET_EVENT = "USER_RESET";

  private manager: UserManager;
  private user: User = null;
  private accessToken: Object = null;
  private signingOut: boolean = false;

  private listeners: Object;
  private eventsSubject: Subject<any>;
  private events: Observable<any>;

  public settings: UserManagerSettings;

  constructor(
    private $log: Logger,
    private tokenHelper: TokenHelperService,
    private ngZone: NgZone, private oauthService: OAuthService) {

    //Hook up some event notifications
    this.listeners = {};
    this.eventsSubject = new Subject<any>();

    this.events = from(this.eventsSubject);

    this.events.subscribe(
      ({ name, args }) => {
        if (this.listeners[name]) {
          for (let listener of this.listeners[name]) {
            listener(...args);
          }
        }
      });
  }

  async serviceIsReady(): Promise<void> {

    await new Promise((resolve, reject) => {
      const source = timer(0, 100).subscribe(t => {
        if (this.initialized) {
          source.unsubscribe();
          resolve(true);
        }
        else if (t > 5000) {
          source.unsubscribe();
          reject(false);
        }
      }, error => {
        reject(error);
      });
    });
  }

  /**
   * Initializes the OIDC Client ready for use by the application.
   */
  async initialize(openIdSettings: IOpenIdOptions): Promise<void> {

    if (this.initialized) return;
    this.ngZone.runOutsideAngular(() => {

      this.settings = this.getClientSettings(openIdSettings);
      this.manager = new UserManager(this.settings);
      //Persist settings for easy access by the silent-renew iframe
      window["oidc"] = {
        settings: this.settings
      };
    });

    var self = this;

    this.manager.events.addAccessTokenExpiring(() => {
      this.$log.info("IdSvr token expiring", new Date());
    });


    this.manager.events.addAccessTokenExpired(() => {
      this.$log.info("IdSvr token expired", new Date());
      this.logout(false);
      //this.broadcast(AuthenticationService.USER_EXPIRED_EVENT);
      this.broadcast(AuthenticationService.USER_RESET_EVENT);
    });

    this.manager.events.addSilentRenewError(e => {
      this.$log.warn("IdSvr silent renew error", e.message, new Date());
      this.logout(false);
    });

    this.manager.events.addUserLoaded(user => {
      this.$log.info("IdSvr user session is ready", new Date());
      this.accessToken = self.tokenHelper.getPayloadFromToken(user.access_token, false);
      this.user = user;
      this.broadcast(AuthenticationService.USER_LOADED_EVENT, user);
    });

    this.manager.events.addUserUnloaded(() => {
      this.$log.info("IdSvr user session has ended", new Date());
      this.broadcast(AuthenticationService.USER_UNLOADED_EVENT);

      if (!this.signingOut) {
        this.startAuthentication(window.location.pathname + window.location.search);
      }
    });

    this.manager.events.addUserSignedOut(() => {
      this.$log.info("IdSvr user signed out", new Date());
      this.logout(false);
      //this.broadcast(AuthenticationService.USER_SIGNED_OUT_EVENT);
      this.broadcast(AuthenticationService.USER_RESET_EVENT);
    });

    this.user = await this.manager.getUser();

    this.initialized = true;
  }

  /**
   * Gets the Authorization header, to be added to any outgoing requests, that needs to be authenticated.
   */
  getAuthorizationHeaders(): HttpHeaders {
    return new HttpHeaders({ 'Authorization': this.getAuthorizationHeaderValue() });
  }

  /**
   * Checks to see if a user is currently logged on.
   */
  isLoggedIn(): boolean {
    return this.user != null && !this.user.expired;
  }

  /**
   * Gets all the claims assigned to the current logged on user.
   */
  getProfile(): any {
    return this.user.profile;
  }

  /**
   * Gets all the claims assigned to the current logged on user.
   */
  getAccessToken(): any {
    return this.accessToken || this.tokenHelper.getPayloadFromToken(this.user.access_token, false);;
  }

  /**
   * Checks to see if the current logged on user has the specified claim
   * @param claimType The type of the claim the user must be assigned
   * @param value The value of the claim, uses the wildcard "*", if no value provided.
   */
  hasClaim(claimType: string, value?: string): boolean {

    var upperValue = value === undefined || value === null
      ? "*"
      : value.toUpperCase();

    if (this.isLoggedIn()) {
      const claims = this.getAccessToken()[claimType];
      if (!claims)
        return false;
      if (typeof claims === "string")
        return claims.toUpperCase() === upperValue;
      else if (Object.prototype.toString.call(claims) === "[object Array]")
        if (claims.filter((c) => {
          return c.toUpperCase() === upperValue;
        })
          .length >
          0)
          return true;
    }
    return false;
  }

  /**
   * Checks to see if the current logged on user has any of the specified claims
   * @param claimTypes The type of the claim
   * @param value The value of the claim, uses the wildcard "*", if no value provided.
   */
  hasAnyClaim(claimTypes: string[], value?: string) {
    if (this.isLoggedIn())
      return false;
    for (let i = 0; i < claimTypes.length; i++) {
      if (this.hasClaim(claimTypes[i], value))
        return true;
    }
    return false;
  }

  /**
   * Gets the access token of the current logged on user.
   */
  getAuthorizationHeaderValue(): string {
    return `${this.user.token_type} ${this.user.access_token}`;
  }

  /**
   * Initiates the logon process, to authenticate the user using Identity Server.
   * @param returnUrl The route to load, post authentication.
   */
  async startAuthentication(returnUrl: string): Promise<void> {

    await this.manager.clearStaleState();
    await this.manager.signinRedirect({
      data: {
        returnUrl: returnUrl
      }
    }).catch(err => {
      this.$log.debug("IdSvr sign in failed", err);
      return err;
    });
  }

  /**
   * Processes the callback from Identity Server, post authentication.
   */
  async completeAuthentication(): Promise<Oidc.User> {
    let user = await new Promise<Oidc.User>((resolve, reject) => {
      this.ngZone.runOutsideAngular(() => {
        this.manager.signinRedirectCallback().then(user => {
          resolve(user);
        }).catch(error => {
          reject(error);
        });
      });
    });

    this.$log.debug("IdSvr user signed in");
    this.user = user;
    return user;
  }

  // private delay(ms: number): Promise<void> {
  //   return new Promise<void>(resolve =>
  //     setTimeout(resolve, ms));
  // }

  /**
   * Logs out the current logged in user.
   */
  logout(signoutRedirect?: boolean) {
    if (signoutRedirect === undefined || signoutRedirect !== false) {
      this.signingOut = true;
      signoutRedirect = true;
    }

    this.manager.stopSilentRenew();

    this.manager.removeUser().then(() => {
      this.manager.clearStaleState();
      this.$log.debug("user removed");

      if (signoutRedirect) {
        this.manager.signoutRedirect();
      }
    }).catch(err => {
      this.$log.error(err);
    });
  }

  /**
   * Gets the current logged in user.
   */
  async getUser(): Promise<Oidc.User> {
    return await this.manager.getUser();
  }

  /**
   * Gets the Identity Server settings for this client application.
   */
  getClientSettings(configuration: IOpenIdOptions): UserManagerSettings {
    return {
      authority: configuration.authority + '/',
      client_id: configuration.clientId,
      redirect_uri: configuration.redirectUri,
      post_logout_redirect_uri: configuration.redirectUri,
      response_type: configuration.responseType, // "id_token token",
      scope: "openid profile email " + configuration.apiResourceId,
      filterProtocolClaims: true,
      loadUserInfo: true,
      automaticSilentRenew: true,
      monitorSession: true,
      silent_redirect_uri: configuration.silentRedirectUri,
      accessTokenExpiringNotificationTime: 20, //default 60
      checkSessionInterval: 5000, //default 2000
      silentRequestTimeout: 20000//default: 10000 
    };
  }

  on(name, listener) {
    if (!this.listeners[name]) {
      this.listeners[name] = [];
    }

    this.listeners[name].push(listener);
  }

  broadcast(name, ...args) {
    this.eventsSubject.next({
      name,
      args
    });
  }
}

export function authenticationServiceFactory(authService: AuthenticationService, appSettings: AppSettingsService) {
  return async () => {
    await appSettings.serviceIsReady();
    await authService.initialize(appSettings.getOpenIdOptions());
  }
};

All the configuration settings are inside the getClientSettings method.

Due to some security issue, I am not able to read the discovery document from the okta

Access to XMLHttpRequest at 'https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/.well-known/openid-configuration' from origin 'https://localhost:44307' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Related problem link

Access to XMLHttpRequest at 'xxx/.well-known/openid-configuration' from origin 'xxxx' has been blocked by CORS

I am looking for a way to configure the discovery document from other location. So that CORS issue won't appear. Is there any way to configure the discovery document in the OIDC-Client library

Did some research on https://github.com/IdentityModel/oidc-client-js and haven't found the configure setting

Tried this configuration but seems not be working

getClientSettings(configuration: IOpenIdOptions): UserManagerSettings {
    return {
      authority: configuration.authority + '/',
      client_id: configuration.clientId,
      redirect_uri: configuration.redirectUri,
      post_logout_redirect_uri: configuration.redirectUri,
      response_type: configuration.responseType, // "id_token token",
      scope: "openid profile email " + configuration.apiResourceId,
      filterProtocolClaims: true,
      loadUserInfo: true,
      automaticSilentRenew: true,
      monitorSession: true,
      silent_redirect_uri: configuration.silentRedirectUri,
      accessTokenExpiringNotificationTime: 20, //default 60
      checkSessionInterval: 5000, //default 2000
      silentRequestTimeout: 20000,//default: 10000 
      metadata: {
        issuer: 'https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357',
        jwks_uri: 'https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/v1/keys',
        end_session_endpoint: 'https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/v1/logout',
        authorization_endpoint: 'https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/v1/authorize'
      }, signingKeys: ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"]
    };
  }

Reference

https://github.com/IdentityModel/oidc-client-js/issues/275

https://github.com/OHIF/Viewers/issues/616

Here is the discovery documentation that I get from the issuer

https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/.well-known/openid-configuration

{
    "issuer": "https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357",
    "authorization_endpoint": "https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/v1/authorize",
    "token_endpoint": "https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/v1/token",
    "userinfo_endpoint": "https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/v1/userinfo",
    "registration_endpoint": "https://dev-166545.okta.com/oauth2/v1/clients",
    "jwks_uri": "https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/v1/keys",
    "response_types_supported": ["code", "id_token", "code id_token", "code token", "id_token token", "code id_token token"],
    "response_modes_supported": ["query", "fragment", "form_post", "okta_post_message"],
    "grant_types_supported": ["authorization_code", "implicit", "refresh_token", "password"],
    "subject_types_supported": ["public"],
    "id_token_signing_alg_values_supported": ["RS256"],
    "scopes_supported": ["monash-identity-api", "openid", "profile", "email", "address", "phone", "offline_access"],
    "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
    "claims_supported": ["iss", "ver", "sub", "aud", "iat", "exp", "jti", "auth_time", "amr", "idp", "nonce", "name", "nickname", "preferred_username", "given_name", "middle_name", "family_name", "email", "email_verified", "profile", "zoneinfo", "locale", "address", "phone_number", "picture", "website", "gender", "birthdate", "updated_at", "at_hash", "c_hash"],
    "code_challenge_methods_supported": ["S256"],
    "introspection_endpoint": "https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/v1/introspect",
    "introspection_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
    "revocation_endpoint": "https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/v1/revoke",
    "revocation_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
    "end_session_endpoint": "https://dev-166545.okta.com/oauth2/aus1igd7yewoAs4xa357/v1/logout",
    "request_parameter_supported": true,
    "request_object_signing_alg_values_supported": ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"]
}

Solution

  • If CORS is blocked then you'll need to run the following steps.

    It is not the correct solution though - you should get your bosses + IT team to instead agree to configure Okta in the standard way for an SPA.

    Here is a configuration that I used back when Azure AD did not allow CORS requests from SPAs:

             // OIDC Settings that work when there is no CORS support
             const settings = {
    
                // OIDC client seems to require at least a dummy value for this
                authority: 'x',
                
                // Supply these details explicitly
                metadata: {
                    issuer: 'https://sts.windows.net/7f071fbc-8bf2-4e61-bb48-dabd8e2f5b5a/',
                    authorization_endpoint: 'https://login.microsoftonline.com/7f071fbc-8bf2-4e61-bb48-dabd8e2f5b5a/oauth2/authorize',
                },
    
                // When CORS is disabled, token signing keys cannot be retrieved
                // The keys must be retrieved first by double hopping from the UI to API to Auth Server
                signingKeys: tokenSigningKeys,
    
                // Turn off calls to user info since CORS will block it
                loadUserInfo: false,
                
                // The URL where the Web UI receives the login result
                redirect_uri: 'https://web.mycompany.com/spa/',
                
                // The no longer recommended implicit flow must be used if CORS is disabled
                response_type: 'token id_token',
    
                // Other OAuth settings
                client_id: '0ed1c9d0-68e7-4acc-abd1-a0efab2643c8',
                scope: 'openid email profile',
                
            } as UserManagerSettings;
            this._userManager = new UserManager(settings);
    

    To get the token signing keys the UI will need to double hop via your API to the JWKS endpoint. Note that JWKS keys are public information and getting them does not need securing - this is the JWKS Endpoint for my developer Azure account.