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