diff --git a/docs-src/silent-refresh.md b/docs-src/silent-refresh.md index cbc6b39d..0fba058f 100644 --- a/docs-src/silent-refresh.md +++ b/docs-src/silent-refresh.md @@ -70,7 +70,7 @@ this .catch(err => console.error('refresh error', err)); ``` -When there is an error in the iframe that prevents the communication with the main application, silentRefresh will give you a timeout. To configure the timespan for this, you can set the property ``siletRefreshTimeout`` (msec). The default value is 20.000 (20 seconds). +When there is an error in the iframe that prevents the communication with the main application, silentRefresh will give you a timeout. To configure the timespan for this, you can set the property ``silentRefreshTimeout`` (msec). The default value is 20.000 (20 seconds). ### Automatically refreshing a token when/ before it expires diff --git a/docs/additional-documentation/refreshing-a-token-(silent-refresh).html b/docs/additional-documentation/refreshing-a-token-(silent-refresh).html index 5d2c6f52..6f47bb43 100644 --- a/docs/additional-documentation/refreshing-a-token-(silent-refresh).html +++ b/docs/additional-documentation/refreshing-a-token-(silent-refresh).html @@ -655,7 +655,7 @@

Refreshing .oauthService .silentRefresh() .then(info => console.debug('refresh ok', info)) - .catch(err => console.error('refresh error', err));

When there is an error in the iframe that prevents the communication with the main application, silentRefresh will give you a timeout. To configure the timespan for this, you can set the property siletRefreshTimeout (msec). The default value is 20.000 (20 seconds).

+ .catch(err => console.error('refresh error', err));

When there is an error in the iframe that prevents the communication with the main application, silentRefresh will give you a timeout. To configure the timespan for this, you can set the property silentRefreshTimeout (msec). The default value is 20.000 (20 seconds).

Automatically refreshing a token when/ before it expires

To automatically refresh a token when/ some time before it expires, just call the following method after configuring the OAuthService:

this.oauthService.setupAutomaticSilentRefresh();

By default, this event is fired after 75% of the token's life time is over. You can adjust this factor by setting the property timeoutFactor to a value between 0 and 1. For instance, 0.5 means, that the event is fired after half of the life time is over and 0.33 triggers the event after a third.

diff --git a/projects/lib/src/angular-oauth-oidic.module.ts b/projects/lib/src/angular-oauth-oidic.module.ts index 54c273d0..3c539b5c 100644 --- a/projects/lib/src/angular-oauth-oidic.module.ts +++ b/projects/lib/src/angular-oauth-oidic.module.ts @@ -33,8 +33,6 @@ export class OAuthModule { config: OAuthModuleConfig = null, validationHandlerClass = NullValidationHandler ): ModuleWithProviders { - // const setupInterceptor = config && config.resourceServer && config.resourceServer.allowedUrls; - return { ngModule: OAuthModule, providers: [ diff --git a/projects/lib/src/auth.config.ts b/projects/lib/src/auth.config.ts index 39dd2839..5f578c0f 100644 --- a/projects/lib/src/auth.config.ts +++ b/projects/lib/src/auth.config.ts @@ -37,7 +37,7 @@ export class AuthConfig { public oidc? = true; /** - * Defines whether to request a access token during + * Defines whether to request an access token during * implicit flow. */ public requestAccessToken? = true; @@ -66,7 +66,6 @@ export class AuthConfig { /** * Url of the userinfo endpoint as defined by OpenId Connect. - * */ public userinfoEndpoint?: string = null; @@ -107,9 +106,9 @@ export class AuthConfig { /** * Some auth servers don't allow using password flow - * w/o a client secreat while the standards do not + * w/o a client secret while the standards do not * demand for it. In this case, you can set a password - * here. As this passwort is exposed to the public + * here. As this password is exposed to the public * it does not bring additional security and is therefore * as good as using no password. */ @@ -159,7 +158,7 @@ export class AuthConfig { public sessionChecksEnabled? = false; /** - * Intervall in msec for checking the session + * Interval in msec for checking the session * according to http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification */ public sessionCheckIntervall? = 3 * 1000; @@ -183,18 +182,18 @@ export class AuthConfig { */ public disableAtHashCheck? = false; - /* - * Defines wether to check the subject of a refreshed token after silent refresh. - * Normally, it should be the same as before. - */ + /** + * Defines wether to check the subject of a refreshed token after silent refresh. + * Normally, it should be the same as before. + */ public skipSubjectCheck? = false; public useIdTokenHintForSilentRefresh? = false; - /* - * Defined whether to skip the validation of the issuer in the discovery document. - * Normally, the discovey document's url starts with the url of the issuer. - */ + /** + * Defined whether to skip the validation of the issuer in the discovery document. + * Normally, the discovey document's url starts with the url of the issuer. + */ public skipIssuerCheck? = false; /** @@ -204,17 +203,17 @@ export class AuthConfig { */ public fallbackAccessTokenExpirationTimeInSec?: number; - /* - * final state sent to issuer is built as follows: - * state = nonce + nonceStateSeparator + additional state - * Default separator is ';' (encoded %3B). - * In rare cases, this character might be forbidden or inconvenient to use by the issuer so it can be customized. - */ + /** + * final state sent to issuer is built as follows: + * state = nonce + nonceStateSeparator + additional state + * Default separator is ';' (encoded %3B). + * In rare cases, this character might be forbidden or inconvenient to use by the issuer so it can be customized. + */ public nonceStateSeparator? = ';'; - /* - * set this to true to use HTTP BASIC auth for password flow - */ + /** + * Set this to true to use HTTP BASIC auth for password flow + */ public useHttpBasicAuthForPasswordFlow? = false; constructor(json?: Partial) { diff --git a/projects/lib/src/oauth-service.ts b/projects/lib/src/oauth-service.ts index 655620c8..efe38455 100644 --- a/projects/lib/src/oauth-service.ts +++ b/projects/lib/src/oauth-service.ts @@ -34,8 +34,8 @@ import { WebHttpUrlEncodingCodec } from './encoder'; */ @Injectable() export class OAuthService extends AuthConfig { - // extending AuthConfig ist just for LEGACY reasons - // to not break existing code + // Extending AuthConfig ist just for LEGACY reasons + // to not break existing code. /** * The ValidationHandler used to validate received @@ -57,7 +57,7 @@ export class OAuthService extends AuthConfig { /** * Informs about events, like token_received or token_expires. - * See the string enum EventType for a full list of events. + * See the string enum EventType for a full list of event types. */ public events: Observable; @@ -65,12 +65,10 @@ export class OAuthService extends AuthConfig { * The received (passed around) state, when logging * in with implicit flow. */ - public state?= ''; + public state? = ''; private eventsSubject: Subject = new Subject(); - private discoveryDocumentLoadedSubject: Subject = new Subject< - object - >(); + private discoveryDocumentLoadedSubject: Subject = new Subject(); private silentRefreshPostMessageEventListener: EventListener; private grantTypesSupported: Array = []; private _storage: OAuthStorage; @@ -111,11 +109,14 @@ export class OAuthService extends AuthConfig { this.setStorage(sessionStorage); } } catch (e) { - this.logger.error( - 'cannot access sessionStorage. Consider setting an own storage implementation using setStorage', + + console.error( + 'No OAuthStorage provided and cannot access default (sessionStorage).' + + 'Consider providing a custom OAuthStorage implementation in your module.', e ); } + this.setupRefreshTimer(); } @@ -156,25 +157,40 @@ export class OAuthService extends AuthConfig { } /** - * + * Will setup up silent refreshing for when the token is + * about to expire. * @param params Additional parameter to pass */ public setupAutomaticSilentRefresh(params: object = {}) { this.events.pipe(filter(e => e.type === 'token_expires')).subscribe(e => { this.silentRefresh(params).catch(_ => { - this.debug('automatic silent refresh did not work'); + this.debug('Automatic silent refresh did not work'); }); }); this.restartRefreshTimerIfStillLoggedIn(); } + /** + * Convenience method that first calls `loadDiscoveryDocument(...)` and + * directly chains using the `then(...)` part of the promise to call + * the `tryLogin(...)` method. + * + * @param options LoginOptions to pass through to `tryLogin(...)` + */ public loadDiscoveryDocumentAndTryLogin(options: LoginOptions = null) { return this.loadDiscoveryDocument().then(doc => { return this.tryLogin(options); }); } + /** + * Convenience method that first calls `loadDiscoveryDocumentAndTryLogin(...)` + * and if then chains to `initImplicitFlow()`, but only if there is no valid + * IdToken or no valid AccessToken. + * + * @param options LoginOptions to pass through to `tryLogin(...)` + */ public loadDiscoveryDocumentAndLogin(options: LoginOptions = null) { return this.loadDiscoveryDocumentAndTryLogin(options).then(_ => { if (!this.hasValidIdToken() || !this.hasValidAccessToken()) { @@ -368,7 +384,7 @@ export class OAuthService extends AuthConfig { } if (!this.validateUrlForHttps(fullUrl)) { - reject('issuer must use Https. Also check property requireHttps.'); + reject('issuer must use https, or config value for property requireHttps must allow http'); return; } @@ -517,8 +533,6 @@ export class OAuthService extends AuthConfig { ); } - // this.sessionChecksEnabled = !!doc.check_session_iframe; - return true; } @@ -529,7 +543,7 @@ export class OAuthService extends AuthConfig { * about the user in question. * * When using this, make sure that the property oidc is set to false. - * Otherwise stricter validations take happen that makes this operation + * Otherwise stricter validations take place that make this operation * fail. * * @param userName @@ -550,8 +564,7 @@ export class OAuthService extends AuthConfig { * Loads the user profile by accessing the user info endpoint defined by OpenId Connect. * * When using this with OAuth2 password flow, make sure that the property oidc is set to false. - * Otherwise stricter validations take happen that makes this operation - * fail. + * Otherwise stricter validations take place that make this operation fail. */ public loadUserProfile(): Promise { if (!this.hasValidAccessToken()) { @@ -559,7 +572,7 @@ export class OAuthService extends AuthConfig { } if (!this.validateUrlForHttps(this.userinfoEndpoint)) { throw new Error( - 'userinfoEndpoint must use Http. Also check property requireHttps.' + 'userinfoEndpoint must use http, or config value for property requireHttps must allow http' ); } @@ -620,7 +633,7 @@ export class OAuthService extends AuthConfig { ): Promise { if (!this.validateUrlForHttps(this.tokenEndpoint)) { throw new Error( - 'tokenEndpoint must use Http. Also check property requireHttps.' + 'tokenEndpoint must use http, or config value for property requireHttps must allow http' ); } @@ -697,7 +710,7 @@ export class OAuthService extends AuthConfig { public refreshToken(): Promise { if (!this.validateUrlForHttps(this.tokenEndpoint)) { throw new Error( - 'tokenEndpoint must use Http. Also check property requireHttps.' + 'tokenEndpoint must use http, or config value for property requireHttps must allow http' ); } @@ -804,8 +817,8 @@ export class OAuthService extends AuthConfig { /** * Performs a silent refresh for implicit flow. - * Use this method to get a new tokens when/ before - * the existing tokens expires. + * Use this method to get new tokens when/before + * the existing tokens expire. */ public silentRefresh(params: object = {}, noPrompt = true): Promise { const claims: object = this.getIdentityClaims() || {}; @@ -814,15 +827,9 @@ export class OAuthService extends AuthConfig { params['id_token_hint'] = this.getIdToken(); } - /* - if (!claims) { - throw new Error('cannot perform a silent refresh as the user is not logged in'); - } - */ - if (!this.validateUrlForHttps(this.loginUrl)) { throw new Error( - 'tokenEndpoint must use Https. Also check property requireHttps.' + 'tokenEndpoint must use https, or config value for property requireHttps must allow http' ); } @@ -833,6 +840,7 @@ export class OAuthService extends AuthConfig { const existingIframe = document.getElementById( this.silentRefreshIFrameName ); + if (existingIframe) { document.body.removeChild(existingIframe); } @@ -888,16 +896,15 @@ export class OAuthService extends AuthConfig { return false; } if (!this.sessionCheckIFrameUrl) { - this.logger.warn( - 'sessionChecksEnabled is activated but there ' + - 'is no sessionCheckIFrameUrl' + console.warn( + 'sessionChecksEnabled is activated but there is no sessionCheckIFrameUrl' ); return false; } const sessionState = this.getSessionState(); if (!sessionState) { - this.logger.warn( - 'sessionChecksEnabled is activated but there ' + 'is no session_state' + console.warn( + 'sessionChecksEnabled is activated but there is no session_state' ); return false; } @@ -1013,7 +1020,6 @@ export class OAuthService extends AuthConfig { const url = this.sessionCheckIFrameUrl; iframe.setAttribute('src', url); - // iframe.style.visibility = 'hidden'; iframe.style.display = 'none'; document.body.appendChild(iframe); @@ -1163,7 +1169,7 @@ export class OAuthService extends AuthConfig { if (!this.validateUrlForHttps(this.loginUrl)) { throw new Error( - 'loginUrl must use Http. Also check property requireHttps.' + 'loginUrl must use http, or config value for property requireHttps must allow http' ); } @@ -1181,18 +1187,17 @@ export class OAuthService extends AuthConfig { location.href = url; }) .catch(error => { - this.logger.error('Error in initImplicitFlow'); - this.logger.error(error); + console.error('Error in initImplicitFlow', error); this.inImplicitFlow = false; }); } /** * Starts the implicit flow and redirects to user to - * the auth servers login url. + * the auth servers' login url. * - * @param additionalState Optinal state that is passes around. - * You find this state in the property ``state`` after ``tryLogin`` logged in the user. + * @param additionalState Optional state that is passed around. + * You'll find this state in the property `state` after `tryLogin` logged in the user. * @param params Hash with additional parameter. If it is a string, it is used for the * parameter loginHint (for the sake of compatibility with former versions) */ @@ -1251,7 +1256,7 @@ export class OAuthService extends AuthConfig { * parsed, validated and used to sign the user in to the * current client. * - * @param options Optinal options. + * @param options Optional options. */ public tryLogin(options: LoginOptions = null): Promise { options = options || {}; @@ -1293,7 +1298,7 @@ export class OAuthService extends AuthConfig { if (!this.requestAccessToken && !this.oidc) { return Promise.reject( - 'Either requestAccessToken or oidc or both must be true.' + 'Either requestAccessToken or oidc (or both) must be true.' ); } @@ -1310,7 +1315,7 @@ export class OAuthService extends AuthConfig { if (this.sessionChecksEnabled && !sessionState) { this.logger.warn( 'session checks (Session Status Change Notification) ' + - 'is activated in the configuration but the id_token ' + + 'were activated in the configuration but the id_token ' + 'does not contain a session_state claim' ); } @@ -1384,8 +1389,8 @@ export class OAuthService extends AuthConfig { ): boolean { const savedNonce = this._storage.getItem('nonce'); if (savedNonce !== nonceInState) { - const err = 'validating access_token failed. wrong state/nonce.'; - this.logger.error(err, savedNonce, nonceInState); + const err = 'Validating access_token failed, wrong state/nonce.'; + console.error(err, savedNonce, nonceInState); return false; } return true; @@ -1445,14 +1450,6 @@ export class OAuthService extends AuthConfig { } } - /* - if (this.getKeyCount() > 1 && !header.kid) { - let err = 'There needs to be a kid property in the id_token header when multiple keys are defined via the property jwks'; - this.logger.warn(err); - return Promise.reject(err); - } - */ - if (!claims.sub) { const err = 'No sub claim in id_token'; this.logger.warn(err); @@ -1460,10 +1457,10 @@ export class OAuthService extends AuthConfig { } /* For now, we only check whether the sub against - * silentRefreshSubject when sessionChecksEnabled is on - * We will reconsider in a later version to do this - * in every other case too. - */ + * silentRefreshSubject when sessionChecksEnabled is on + * We will reconsider in a later version to do this + * in every other case too. + */ if ( this.sessionChecksEnabled && this.silentRefreshSubject && @@ -1516,9 +1513,9 @@ export class OAuthService extends AuthConfig { issuedAtMSec - tenMinutesInMsec >= now || expiresAtMSec + tenMinutesInMsec <= now ) { - const err = 'Token has been expired'; - this.logger.error(err); - this.logger.error({ + const err = 'Token has expired'; + console.error(err); + console.error({ now: now, issuedAtMSec: issuedAtMSec, expiresAtMSec: expiresAtMSec @@ -1656,7 +1653,7 @@ export class OAuthService extends AuthConfig { } /** - * Checkes, whether there is a valid id_token. + * Checks whether there is a valid id_token. */ public hasValidIdToken(): boolean { if (this.getIdToken()) { @@ -1719,7 +1716,7 @@ export class OAuthService extends AuthConfig { if (!this.validateUrlForHttps(this.logoutUrl)) { throw new Error( - 'logoutUrl must use Http. Also check property requireHttps.' + 'logoutUrl must use http, or config value for property requireHttps must allow http' ); } diff --git a/projects/lib/src/token-validation/jwks-validation-handler.ts b/projects/lib/src/token-validation/jwks-validation-handler.ts index 3f15f2fa..da7afa46 100644 --- a/projects/lib/src/token-validation/jwks-validation-handler.ts +++ b/projects/lib/src/token-validation/jwks-validation-handler.ts @@ -3,9 +3,6 @@ import { ValidationParams } from './validation-handler'; -// declare var require: any; -// let rs = require('jsrsasign'); - import * as rs from 'jsrsasign'; /** @@ -52,8 +49,6 @@ export class JwksValidationHandler extends AbstractValidationHandler { throw new Error('Array keys in jwks missing!'); } - // console.debug('validateSignature: retry', retry); - let kid: string = params.idTokenHeader['kid']; let keys: object[] = params.jwks['keys']; let key: object; @@ -68,12 +63,6 @@ export class JwksValidationHandler extends AbstractValidationHandler { k => k['kty'] === kty && k['use'] === 'sig' ); - /* - if (matchingKeys.length == 0) { - let error = 'No matching key found.'; - console.error(error); - return Promise.reject(error); - }*/ if (matchingKeys.length > 1) { let error = 'More than one matching key found. Please specify a kid in the id_token header.'; diff --git a/projects/lib/src/token-validation/null-validation-handler.ts b/projects/lib/src/token-validation/null-validation-handler.ts index 181cc2a6..07dbb2f7 100644 --- a/projects/lib/src/token-validation/null-validation-handler.ts +++ b/projects/lib/src/token-validation/null-validation-handler.ts @@ -2,7 +2,7 @@ import { ValidationHandler, ValidationParams } from './validation-handler'; /** * A validation handler that isn't validating nothing. - * Can be used to skip validation (on your own risk). + * Can be used to skip validation (at your own risk). */ export class NullValidationHandler implements ValidationHandler { validateSignature(validationParams: ValidationParams): Promise { diff --git a/projects/lib/src/types.ts b/projects/lib/src/types.ts index 3ee2a098..a8d1c13e 100644 --- a/projects/lib/src/types.ts +++ b/projects/lib/src/types.ts @@ -12,6 +12,7 @@ export class LoginOptions { /** * Hook, to validate the received tokens. + * * Deprecated: Use property ``tokenValidationHandler`` on OAuthService instead. */ validationHandler?: (receivedTokens: ReceivedTokens) => Promise;