oidc-client从本地主机或其他URL配置发现文档

时间:2019-12-19 23:13:38

标签: javascript angular openid-connect okta oidc-client-js

他们是在angular 8应用程序中使用OIDC-Client登录之前从本地主机配置发现文档的任何方法。

我有这个经理,是OIDC客户的帮助电话

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

所有配置设置都在getClientSettings方法内部。

由于某些安全问题,我无法从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.

相关问题链接

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

我正在寻找一种从其他位置配置发现文档的方法。这样就不会出现CORS问题。有什么方法可以在OIDC客户端库中配置发现文档

https://github.com/IdentityModel/oidc-client-js进行了一些研究,但未找到配置设置

尝试了此配置,但似乎不起作用

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"]
    };
  }

参考

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

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

这是我从发行人那里获得的发现文档

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"]
}

1 个答案:

答案 0 :(得分:1)

如果CORS被阻止,则需要执行以下步骤。

但这不是正确的解决方案-您应该让老板+ IT团队同意以标准方式为SPA配置Okta。

  • 配置没有授权网址的OIDC客户端
  • 使用明确的发行者+授权端点配置OIDC客户端
  • 配置OIDC客户端以不获取用户信息
  • 为OIDC客户端提供令牌签名密钥作为数据
  • 使用隐式流,并使用response_type = token id_token

这是我在Azure AD上测试过的有效配置,该配置具有CORS限制,需要这种类型的黑客-而Okta则不应:

stripes = 7

def stripes_pot():
    global stripes
    stripes -= 1
    another_go()

要获取令牌签名密钥,UI将需要通过您的API两次跳至JWKS端点。有关如何执行此操作的示例,请参见以下代码:

请注意,JWKS密钥是公共信息,获取它们不需要保护-this is the JWKS Endpoint对于我的开发人员Azure帐户。