projects/lib/src/oauth-service.ts
Service for logging in and logging out with OIDC and OAuth2. Supports implicit flow and password flow.
constructor(ngZone: NgZone, http: HttpClient, storage: OAuthStorage, tokenValidationHandler: ValidationHandler, config: AuthConfig, urlHelper: UrlHelperService, logger: OAuthLogger, crypto: HashHandler, document: Document, dateTimeService: DateTimeProvider)
|
|||||||||||||||||||||||||||||||||
Defined in projects/lib/src/oauth-service.ts:112
|
|||||||||||||||||||||||||||||||||
Parameters :
|
Protected assertUrlNotNullAndCorrectProtocol | |||||||||
assertUrlNotNullAndCorrectProtocol(url: string | undefined, description: string)
|
|||||||||
Defined in projects/lib/src/oauth-service.ts:371
|
|||||||||
Parameters :
Returns :
void
|
Public authorizationHeader |
authorizationHeader()
|
Defined in projects/lib/src/oauth-service.ts:2475
|
Returns the auth-header that can be used to transmit the access_token to a service
Returns :
string
|
Protected calcTimeout |
calcTimeout(storedAt: number, expiration: number)
|
Defined in projects/lib/src/oauth-service.ts:493
|
Returns :
number
|
Protected calculatePopupFeatures | ||||||
calculatePopupFeatures(options: literal type)
|
||||||
Defined in projects/lib/src/oauth-service.ts:1247
|
||||||
Parameters :
Returns :
string
|
Protected callOnTokenReceivedIfExists | ||||||
callOnTokenReceivedIfExists(options: LoginOptions)
|
||||||
Defined in projects/lib/src/oauth-service.ts:1659
|
||||||
Parameters :
Returns :
void
|
Protected canPerformSessionCheck |
canPerformSessionCheck()
|
Defined in projects/lib/src/oauth-service.ts:1280
|
Returns :
boolean
|
Protected Async checkAtHash | ||||||
checkAtHash(params: ValidationParams)
|
||||||
Defined in projects/lib/src/oauth-service.ts:2671
|
||||||
Parameters :
Returns :
Promise<boolean>
|
Public checkSession |
checkSession()
|
Defined in projects/lib/src/oauth-service.ts:1459
|
Returns :
void
|
Protected checkSignature | ||||||
checkSignature(params: ValidationParams)
|
||||||
Defined in projects/lib/src/oauth-service.ts:2681
|
||||||
Parameters :
Returns :
Promise<any>
|
Protected clearAccessTokenTimer |
clearAccessTokenTimer()
|
Defined in projects/lib/src/oauth-service.ts:475
|
Returns :
void
|
Protected clearAutomaticRefreshTimer |
clearAutomaticRefreshTimer()
|
Defined in projects/lib/src/oauth-service.ts:487
|
Returns :
void
|
Protected clearIdTokenTimer |
clearIdTokenTimer()
|
Defined in projects/lib/src/oauth-service.ts:481
|
Returns :
void
|
Protected configChanged |
configChanged()
|
Defined in projects/lib/src/oauth-service.ts:209
|
Returns :
void
|
Public configure | ||||||||
configure(config: AuthConfig)
|
||||||||
Defined in projects/lib/src/oauth-service.ts:195
|
||||||||
Use this method to configure the service
Parameters :
Returns :
void
|
Protected Async createChallangeVerifierPairForPKCE |
createChallangeVerifierPairForPKCE()
|
Defined in projects/lib/src/oauth-service.ts:2740
|
Returns :
Promise<>
|
Protected Async createLoginUrl | ||||||||||||||||||||||||
createLoginUrl(state: string, loginHint: string, customRedirectUri: string, noPrompt, params: object)
|
||||||||||||||||||||||||
Defined in projects/lib/src/oauth-service.ts:1481
|
||||||||||||||||||||||||
Parameters :
Returns :
Promise<string>
|
Protected createNonce |
createNonce()
|
Defined in projects/lib/src/oauth-service.ts:2629
|
Returns :
Promise<string>
|
Protected debug | ||||||
debug(...args: any[])
|
||||||
Defined in projects/lib/src/oauth-service.ts:322
|
||||||
Parameters :
Returns :
void
|
Public fetchTokenUsingGrant | ||||||||||||||||||||
fetchTokenUsingGrant(grantType: string, parameters: object, headers: HttpHeaders)
|
||||||||||||||||||||
Defined in projects/lib/src/oauth-service.ts:841
|
||||||||||||||||||||
Uses a custom grant type to retrieve tokens.
Parameters :
Returns :
Promise<TokenResponse>
|
Public fetchTokenUsingPasswordFlow | ||||||||||||||||||||
fetchTokenUsingPasswordFlow(userName: string, password: string, headers: HttpHeaders)
|
||||||||||||||||||||
Defined in projects/lib/src/oauth-service.ts:823
|
||||||||||||||||||||
Uses password flow to exchange userName and password for an access_token.
Parameters :
Returns :
Promise<TokenResponse>
|
Public fetchTokenUsingPasswordFlowAndLoadUserProfile | ||||||||||||||||||||
fetchTokenUsingPasswordFlowAndLoadUserProfile(userName: string, password: string, headers: HttpHeaders)
|
||||||||||||||||||||
Defined in projects/lib/src/oauth-service.ts:724
|
||||||||||||||||||||
Uses password flow to exchange userName and password for an access_token. After receiving the access_token, this method uses it to query the userinfo endpoint in order to get information about the user in question. When using this, make sure that the property oidc is set to false. Otherwise stricter validations take place that make this operation fail.
Parameters :
Returns :
Promise<object>
|
Public getAccessToken |
getAccessToken()
|
Defined in projects/lib/src/oauth-service.ts:2378
|
Returns the current access_token.
Returns :
string
|
Public getAccessTokenExpiration |
getAccessTokenExpiration()
|
Defined in projects/lib/src/oauth-service.ts:2390
|
Returns the expiration date of the access_token as milliseconds since 1970.
Returns :
number
|
Protected getAccessTokenStoredAt |
getAccessTokenStoredAt()
|
Defined in projects/lib/src/oauth-service.ts:2397
|
Returns :
number
|
Public getCustomTokenResponseProperty | ||||||
getCustomTokenResponseProperty(requestedProperty: string)
|
||||||
Defined in projects/lib/src/oauth-service.ts:2462
|
||||||
Retrieve a saved custom property of the TokenReponse object. Only if predefined in authconfig.
Parameters :
Returns :
any
|
Public getGrantedScopes |
getGrantedScopes()
|
Defined in projects/lib/src/oauth-service.ts:2353
|
Returns the granted scopes from the server.
Returns :
object
|
Public getIdentityClaims |
getIdentityClaims()
|
Defined in projects/lib/src/oauth-service.ts:2342
|
Returns the received claims about the user.
Returns :
Record<string, any>
|
Public getIdToken |
getIdToken()
|
Defined in projects/lib/src/oauth-service.ts:2364
|
Returns the current id_token.
Returns :
string
|
Public getIdTokenExpiration |
getIdTokenExpiration()
|
Defined in projects/lib/src/oauth-service.ts:2409
|
Returns the expiration date of the id_token as milliseconds since 1970.
Returns :
number
|
Protected getIdTokenStoredAt |
getIdTokenStoredAt()
|
Defined in projects/lib/src/oauth-service.ts:2401
|
Returns :
number
|
Public getRefreshToken |
getRefreshToken()
|
Defined in projects/lib/src/oauth-service.ts:2382
|
Returns :
string
|
Protected getSessionState |
getSessionState()
|
Defined in projects/lib/src/oauth-service.ts:2142
|
Returns :
string
|
Protected handleLoginError | |||||||||
handleLoginError(options: LoginOptions, parts: object)
|
|||||||||
Defined in projects/lib/src/oauth-service.ts:2146
|
|||||||||
Parameters :
Returns :
void
|
Protected handleSessionChange |
handleSessionChange()
|
Defined in projects/lib/src/oauth-service.ts:1360
|
Returns :
void
|
Protected handleSessionError |
handleSessionError()
|
Defined in projects/lib/src/oauth-service.ts:1405
|
Returns :
void
|
Protected handleSessionUnchanged |
handleSessionUnchanged()
|
Defined in projects/lib/src/oauth-service.ts:1355
|
Returns :
void
|
Public hasValidAccessToken |
hasValidAccessToken()
|
Defined in projects/lib/src/oauth-service.ts:2420
|
Checkes, whether there is a valid access_token.
Returns :
boolean
|
Public hasValidIdToken |
hasValidIdToken()
|
Defined in projects/lib/src/oauth-service.ts:2441
|
Checks whether there is a valid id_token.
Returns :
boolean
|
Public initCodeFlow |
initCodeFlow(additionalState: string, params: object)
|
Defined in projects/lib/src/oauth-service.ts:2707
|
Starts the authorization code flow and redirects to user to the auth servers login url.
Returns :
void
|
Public initImplicitFlow | |||||||||||||||
initImplicitFlow(additionalState: string, params: string | object)
|
|||||||||||||||
Defined in projects/lib/src/oauth-service.ts:1635
|
|||||||||||||||
Starts the implicit flow and redirects to user to the auth servers' login url. You'll find this state in the property
Parameters :
Returns :
void
|
Public initImplicitFlowInPopup | ||||||
initImplicitFlowInPopup(options?: literal type)
|
||||||
Defined in projects/lib/src/oauth-service.ts:1136
|
||||||
This method exists for backwards compatibility. OAuthService handles both code and implicit flows.
Parameters :
Returns :
any
|
initImplicitFlowInternal | ||||||||||||
initImplicitFlowInternal(additionalState: string, params: string | object)
|
||||||||||||
Defined in projects/lib/src/oauth-service.ts:1593
|
||||||||||||
Parameters :
Returns :
void
|
Public initLoginFlow |
initLoginFlow(additionalState: string, params: object)
|
Defined in projects/lib/src/oauth-service.ts:2695
|
Start the implicit flow or the code flow, depending on your configuration.
Returns :
void
|
Public initLoginFlowInPopup | ||||||
initLoginFlowInPopup(options?: literal type)
|
||||||
Defined in projects/lib/src/oauth-service.ts:1144
|
||||||
Parameters :
Returns :
any
|
Protected initSessionCheck |
initSessionCheck()
|
Defined in projects/lib/src/oauth-service.ts:1417
|
Returns :
void
|
Public loadDiscoveryDocument | ||||||||
loadDiscoveryDocument(fullUrl: string)
|
||||||||
Defined in projects/lib/src/oauth-service.ts:528
|
||||||||
Loads the discovery document to configure most properties of this service. The url of the discovery document is infered from the issuer's url according to the OpenId Connect spec. To use another url you can pass it to to optional parameter fullUrl.
Parameters :
Returns :
Promise<OAuthSuccessEvent>
|
Public loadDiscoveryDocumentAndLogin | ||||||||
loadDiscoveryDocumentAndLogin(options)
|
||||||||
Defined in projects/lib/src/oauth-service.ts:307
|
||||||||
Convenience method that first calls
Parameters :
Returns :
Promise<boolean>
|
Public loadDiscoveryDocumentAndTryLogin | ||||||||||
loadDiscoveryDocumentAndTryLogin(options: LoginOptions)
|
||||||||||
Defined in projects/lib/src/oauth-service.ts:292
|
||||||||||
Convenience method that first calls
Parameters :
Returns :
Promise<boolean>
|
Protected loadJwks |
loadJwks()
|
Defined in projects/lib/src/oauth-service.ts:611
|
Returns :
Promise<object>
|
Public loadUserProfile |
loadUserProfile()
|
Defined in projects/lib/src/oauth-service.ts:740
|
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 place that make this operation fail.
Returns :
Promise<object>
|
Public logOut |
logOut()
|
Defined in projects/lib/src/oauth-service.ts:2486
|
Removes all tokens and logs the user out. If a logout url is configured, the user is redirected to it with optional state parameter.
Returns :
void
|
Public logOut | ||||||
logOut(customParameters: boolean | object)
|
||||||
Defined in projects/lib/src/oauth-service.ts:2487
|
||||||
Parameters :
Returns :
void
|
Public logOut | ||||||
logOut(noRedirectToLogoutUrl: boolean)
|
||||||
Defined in projects/lib/src/oauth-service.ts:2488
|
||||||
Parameters :
Returns :
void
|
Public logOut |
logOut(noRedirectToLogoutUrl: boolean, state: string)
|
Defined in projects/lib/src/oauth-service.ts:2489
|
Returns :
void
|
Public logOut | ||||||||||||
logOut(customParameters: boolean | object, state: string)
|
||||||||||||
Defined in projects/lib/src/oauth-service.ts:2490
|
||||||||||||
Parameters :
Returns :
void
|
Protected padBase64 | ||||
padBase64(base64data)
|
||||
Defined in projects/lib/src/oauth-service.ts:2368
|
||||
Parameters :
Returns :
string
|
Protected processMessageEventMessage | ||||||
processMessageEventMessage(e: MessageEvent)
|
||||||
Defined in projects/lib/src/oauth-service.ts:1260
|
||||||
Parameters :
Returns :
string
|
Protected refreshInternal | ||||||
refreshInternal(params, noPrompt)
|
||||||
Defined in projects/lib/src/oauth-service.ts:274
|
||||||
Parameters :
Returns :
Promise<TokenResponse | OAuthEvent>
|
Public refreshToken |
refreshToken()
|
Defined in projects/lib/src/oauth-service.ts:929
|
Refreshes the token using a refresh_token. This does not work for implicit flow, b/c there is no refresh_token in this flow. A solution for this is provided by the method silentRefresh.
Returns :
Promise<TokenResponse>
|
Protected removeSessionCheckEventListener |
removeSessionCheckEventListener()
|
Defined in projects/lib/src/oauth-service.ts:1410
|
Returns :
void
|
Protected removeSilentRefreshEventListener |
removeSilentRefreshEventListener()
|
Defined in projects/lib/src/oauth-service.ts:1011
|
Returns :
void
|
Public resetImplicitFlow |
resetImplicitFlow()
|
Defined in projects/lib/src/oauth-service.ts:1655
|
Reset current implicit flow
Returns :
void
|
Protected restartRefreshTimerIfStillLoggedIn |
restartRefreshTimerIfStillLoggedIn()
|
Defined in projects/lib/src/oauth-service.ts:219
|
Returns :
void
|
Public restartSessionChecksIfStillLoggedIn |
restartSessionChecksIfStillLoggedIn()
|
Defined in projects/lib/src/oauth-service.ts:213
|
Returns :
void
|
Public revokeTokenAndLogout | ||||||||||||
revokeTokenAndLogout(customParameters: boolean | object, ignoreCorsIssues)
|
||||||||||||
Defined in projects/lib/src/oauth-service.ts:2779
|
||||||||||||
Revokes the auth token to secure the vulnarability of the token issued allowing the authorization server to clean up any security credentials associated with the authorization
Parameters :
Returns :
Promise<any>
|
Protected setupAccessTokenTimer |
setupAccessTokenTimer()
|
Defined in projects/lib/src/oauth-service.ts:429
|
Returns :
void
|
Public setupAutomaticSilentRefresh | ||||||||||||||||||||
setupAutomaticSilentRefresh(params: object, listenTo?: "access_token" | "id_token" | "any", noPrompt)
|
||||||||||||||||||||
Defined in projects/lib/src/oauth-service.ts:239
|
||||||||||||||||||||
Will setup up silent refreshing for when the token is about to expire. When the user is logged out via this.logOut method, the silent refreshing will pause and not refresh the tokens until the user is logged back in via receiving a new token.
Parameters :
Returns :
void
|
Protected setupExpirationTimers |
setupExpirationTimers()
|
Defined in projects/lib/src/oauth-service.ts:419
|
Returns :
void
|
Protected setupIdTokenTimer |
setupIdTokenTimer()
|
Defined in projects/lib/src/oauth-service.ts:447
|
Returns :
void
|
Protected setupRefreshTimer |
setupRefreshTimer()
|
Defined in projects/lib/src/oauth-service.ts:395
|
Returns :
void
|
Protected setupSessionCheck |
setupSessionCheck()
|
Defined in projects/lib/src/oauth-service.ts:223
|
Returns :
void
|
Protected setupSessionCheckEventListener |
setupSessionCheckEventListener()
|
Defined in projects/lib/src/oauth-service.ts:1304
|
Returns :
void
|
Protected setupSilentRefreshEventListener |
setupSilentRefreshEventListener()
|
Defined in projects/lib/src/oauth-service.ts:1021
|
Returns :
void
|
Public silentRefresh | ||||||||||||
silentRefresh(params: object, noPrompt)
|
||||||||||||
Defined in projects/lib/src/oauth-service.ts:1051
|
||||||||||||
Performs a silent refresh for implicit flow. Use this method to get new tokens when/before the existing tokens expire.
Parameters :
Returns :
Promise<OAuthEvent>
|
Protected startSessionCheckTimer |
startSessionCheckTimer()
|
Defined in projects/lib/src/oauth-service.ts:1442
|
Returns :
void
|
Public stopAutomaticRefresh |
stopAutomaticRefresh()
|
Defined in projects/lib/src/oauth-service.ts:469
|
Stops timers for automatic refresh. To restart it, call setupAutomaticSilentRefresh again.
Returns :
void
|
Protected stopSessionCheckTimer |
stopSessionCheckTimer()
|
Defined in projects/lib/src/oauth-service.ts:1452
|
Returns :
void
|
Protected storeAccessTokenResponse | ||||||||||||||||||
storeAccessTokenResponse(accessToken: string, refreshToken: string, expiresIn: number, grantedScopes: string, customParameters?: Map
|
||||||||||||||||||
Defined in projects/lib/src/oauth-service.ts:1672
|
||||||||||||||||||
Parameters :
Returns :
void
|
Protected storeIdToken | ||||||
storeIdToken(idToken: ParsedIdToken)
|
||||||
Defined in projects/lib/src/oauth-service.ts:2128
|
||||||
Parameters :
Returns :
void
|
Protected storeSessionState | ||||||
storeSessionState(sessionState: string)
|
||||||
Defined in projects/lib/src/oauth-service.ts:2138
|
||||||
Parameters :
Returns :
void
|
Public tryLogin | ||||||||||
tryLogin(options: LoginOptions)
|
||||||||||
Defined in projects/lib/src/oauth-service.ts:1714
|
||||||||||
Delegates to tryLoginImplicitFlow for the sake of competability
Parameters :
Returns :
Promise<boolean>
|
Public Async tryLoginCodeFlow | ||||||||
tryLoginCodeFlow(options: LoginOptions)
|
||||||||
Defined in projects/lib/src/oauth-service.ts:1734
|
||||||||
Parameters :
Returns :
Promise<void>
|
Public tryLoginImplicitFlow | ||||||||||
tryLoginImplicitFlow(options: LoginOptions)
|
||||||||||
Defined in projects/lib/src/oauth-service.ts:1975
|
||||||||||
Checks whether there are tokens in the hash fragment as a result of the implicit flow. These tokens are parsed, validated and used to sign the user in to the current client.
Parameters :
Returns :
Promise<boolean>
|
Protected validateDiscoveryDocument | ||||||
validateDiscoveryDocument(doc: OidcDiscoveryDoc)
|
||||||
Defined in projects/lib/src/oauth-service.ts:636
|
||||||
Parameters :
Returns :
boolean
|
Protected validateNonce | ||||||
validateNonce(nonceInState: string)
|
||||||
Defined in projects/lib/src/oauth-service.ts:2108
|
||||||
Parameters :
Returns :
boolean
|
Protected validateUrlAgainstIssuer | ||||||
validateUrlAgainstIssuer(url: string)
|
||||||
Defined in projects/lib/src/oauth-service.ts:385
|
||||||
Parameters :
Returns :
any
|
Protected validateUrlForHttps | ||||||
validateUrlForHttps(url: string)
|
||||||
Defined in projects/lib/src/oauth-service.ts:349
|
||||||
Parameters :
Returns :
boolean
|
Protected validateUrlFromDiscoveryDocument | ||||||
validateUrlFromDiscoveryDocument(url: string)
|
||||||
Defined in projects/lib/src/oauth-service.ts:328
|
||||||
Parameters :
Returns :
string[]
|
Protected waitForSilentRefreshAfterSessionChange |
waitForSilentRefreshAfterSessionChange()
|
Defined in projects/lib/src/oauth-service.ts:1385
|
Returns :
void
|
Protected _storage |
Type : OAuthStorage
|
Defined in projects/lib/src/oauth-service.ts:100
|
Protected accessTokenTimeoutSubscription |
Type : Subscription
|
Defined in projects/lib/src/oauth-service.ts:101
|
Protected automaticRefreshSubscription |
Type : Subscription
|
Defined in projects/lib/src/oauth-service.ts:104
|
Protected discoveryDocumentLoadedSubject |
Type : Subject<OidcDiscoveryDoc>
|
Default value : new Subject<OidcDiscoveryDoc>()
|
Defined in projects/lib/src/oauth-service.ts:96
|
Public events |
Type : Observable<OAuthEvent>
|
Defined in projects/lib/src/oauth-service.ts:87
|
Informs about events, like token_received or token_expires. See the string enum EventType for a full list of event types. |
Protected eventsSubject |
Type : Subject<OAuthEvent>
|
Default value : new Subject<OAuthEvent>()
|
Defined in projects/lib/src/oauth-service.ts:95
|
Protected grantTypesSupported |
Type : Array<string>
|
Default value : []
|
Defined in projects/lib/src/oauth-service.ts:99
|
Protected idTokenTimeoutSubscription |
Type : Subscription
|
Defined in projects/lib/src/oauth-service.ts:102
|
Protected inImplicitFlow |
Default value : false
|
Defined in projects/lib/src/oauth-service.ts:109
|
Protected jwksUri |
Type : string
|
Defined in projects/lib/src/oauth-service.ts:106
|
Protected saveNoncesInLocalStorage |
Default value : false
|
Defined in projects/lib/src/oauth-service.ts:111
|
Protected sessionCheckEventListener |
Type : EventListener
|
Defined in projects/lib/src/oauth-service.ts:105
|
Protected sessionCheckTimer |
Type : any
|
Defined in projects/lib/src/oauth-service.ts:107
|
Protected silentRefreshPostMessageEventListener |
Type : EventListener
|
Defined in projects/lib/src/oauth-service.ts:98
|
Protected silentRefreshSubject |
Type : string
|
Defined in projects/lib/src/oauth-service.ts:108
|
Public Optional state |
Type : string
|
Default value : ''
|
Defined in projects/lib/src/oauth-service.ts:93
|
The received (passed around) state, when logging in with implicit flow. |
Protected tokenReceivedSubscription |
Type : Subscription
|
Defined in projects/lib/src/oauth-service.ts:103
|
Public tokenValidationHandler |
Type : ValidationHandler
|
Defined in projects/lib/src/oauth-service.ts:69
|
The ValidationHandler used to validate received id_tokens. |
Public Optional checkOrigin |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:280
|
Blocks other origins requesting a silent refresh |
Public Optional clearHashAfterLogin |
Default value : true
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:66
|
Defines whether to clear the hash fragment after logging in. |
Public Optional clientId |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:5
|
The client's id as registered with the auth server |
Public Optional clockSkewInSec |
Type : number
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:238
|
The window of time (in seconds) to allow the current time to deviate when validating id_token's iat and exp values. |
Public Optional customQueryParams |
Type : object
|
Default value : null
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:158
|
Map with additional query parameter that are appended to the request when initializing implicit flow. |
Public Optional customTokenParameters |
Type : string[]
|
Default value : []
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:81
|
Names of known parameters sent out in the TokenResponse. https://tools.ietf.org/html/rfc6749#section-5.1 |
Public Optional decreaseExpirationBySec |
Type : number
|
Default value : 0
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:243
|
Decreases the Expiration time of tokens by this number of seconds |
Public Optional disableAtHashCheck |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:199
|
This property has been introduced to disable at_hash checks and is indented for Identity Provider that does not deliver an at_hash EVEN THOUGH its recommended by the OIDC specs. Of course, when disabling these checks then we are bypassing a security check which means we are more vulnerable. |
Public Optional disableIdTokenTimer |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:275
|
Allows to disable the timer for the id_token used for token refresh |
Public Optional disablePKCE |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:263
|
Code Flow is by defauld used together with PKCI which is also higly recommented. You can disbale it here by setting this flag to true. https://tools.ietf.org/html/rfc7636#section-1.1 |
Public Optional dummyClientSecret |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:131
|
Some auth servers don't allow using password flow w/o a client secret while the standards do not demand for it. In this case, you can set a password here. As this password is exposed to the public it does not bring additional security and is therefore as good as using no password. |
Public Optional fallbackAccessTokenExpirationTimeInSec |
Type : number
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:220
|
According to rfc6749 it is recommended (but not required) that the auth server exposes the access_token's life time in seconds. This is a fallback value for the case this value is not exposed. |
Public Optional issuer |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:56
|
The issuer's uri. |
Public Optional jwks |
Type : object
|
Default value : null
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:152
|
JSON Web Key Set (https://tools.ietf.org/html/rfc7517) with keys used to validate received id_tokens. This is taken out of the disovery document. Can be set manually too. |
Public Optional loginUrl |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:28
|
The auth server's endpoint that allows to log the user in when using implicit flow. |
Public Optional logoutUrl |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:61
|
The logout url. |
Public Optional nonceStateSeparator |
Type : string
|
Default value : ';'
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:228
|
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 Optional oidc |
Default value : true
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:43
|
Defines whether to use OpenId Connect during implicit flow. |
Public Optional openUri |
Type : function
|
Default value : () => {...}
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:293
|
This property allows you to override the method that is used to open the login url, allowing a way for implementations to specify their own method of routing to new urls. |
Public Optional options |
Type : any
|
Default value : null
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:51
|
Public Optional postLogoutRedirectUri |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:16
|
An optional second redirectUri where the auth server redirects the user to after logging out. |
Public Optional preserveRequestedRoute |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:269
|
Set this to true to preserve the requested route including query parameters after code flow login. This setting enables deep linking for the code flow. |
Public Optional redirectUri |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:10
|
The client's redirectUri as registered with the auth server |
Public Optional redirectUriAsPostLogoutRedirectUriFallback |
Default value : true
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:22
|
Defines whether to use 'redirectUri' as a replacement of 'postLogoutRedirectUri' if the latter is not set. |
Public Optional requestAccessToken |
Default value : true
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:49
|
Defines whether to request an access token during implicit flow. |
Public Optional requireHttps |
Type : boolean | "remoteOnly"
|
Default value : 'remoteOnly'
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:139
|
Defines whether https is required. The default value is remoteOnly which only allows http for localhost, while every other domains need to be used with https. |
Public Optional resource |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:35
|
Public Optional responseType |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:88
|
Public Optional revocationEndpoint |
Type : string
|
Default value : null
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:76
|
Url of the revocation endpoint as defined by OpenId Connect and OAuth 2. |
Public Optional rngUrl |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:37
|
Public Optional scope |
Type : string
|
Default value : 'openid profile'
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:33
|
The requested scopes |
Public Optional sessionCheckIFrameName |
Type : string
|
Default value : 'angular-oauth-oidc-check-session-iframe'
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:190
|
Name of the iframe to use for session checks |
Public Optional sessionCheckIFrameUrl |
Type : string
|
Default value : null
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:185
|
Url for the iframe used for session checks |
Public Optional sessionCheckIntervall |
Default value : 3 * 1000
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:180
|
Interval in msec for checking the session according to http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification |
Public Optional sessionChecksEnabled |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:174
|
If true, the lib will try to check whether the user is still logged in on a regular basis as described in http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification |
Public Optional showDebugInformation |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:96
|
Defines whether additional debug information should be shown at the console. Note that in certain browsers the verbosity of the console needs to be explicitly set to include Debug level messages. |
Public Optional silentRefreshIFrameName |
Type : string
|
Default value : 'angular-oauth-oidc-silent-refresh-iframe'
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:160
|
Public Optional silentRefreshMessagePrefix |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:103
|
Public Optional silentRefreshRedirectUri |
Type : string
|
Default value : ''
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:101
|
The redirect uri used when doing silent refresh. |
Public Optional silentRefreshShowIFrame |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:109
|
Set this to true to display the iframe used for silent refresh for debugging. |
Public Optional silentRefreshTimeout |
Type : number
|
Default value : 1000 * 20
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:121
|
Timeout for silent refresh. |
Public Optional skipIssuerCheck |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:213
|
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 Optional skipSubjectCheck |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:205
|
Defines wether to check the subject of a refreshed token after silent refresh. Normally, it should be the same as before. |
Public Optional strictDiscoveryDocumentValidation |
Default value : true
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:145
|
Defines whether every url provided by the discovery document has to start with the issuer's url. |
Public Optional timeoutFactor |
Type : number
|
Default value : 0.75
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:167
|
Defines when the token_timeout event should be raised. If you set this to the default value 0.75, the event is triggered after 75% of the token's life time. |
Public Optional tokenEndpoint |
Type : string
|
Default value : null
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:71
|
Url of the token endpoint as defined by OpenId Connect and OAuth 2. |
Public Optional useHttpBasicAuth |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:233
|
Set this to true to use HTTP BASIC auth for AJAX calls |
Public Optional useIdTokenHintForSilentRefresh |
Default value : false
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:207
|
Public Optional userinfoEndpoint |
Type : string
|
Default value : null
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:86
|
Url of the userinfo endpoint as defined by OpenId Connect. |
Public Optional useSilentRefresh |
Inherited from
AuthConfig
|
Defined in
AuthConfig:256
|
Set this to true if you want to use silent refresh together with code flow. As silent refresh is the only option for refreshing with implicit flow, you don't need to explicitly turn it on in this case. |
Public Optional waitForTokenInMsec |
Type : number
|
Default value : 0
|
Inherited from
AuthConfig
|
Defined in
AuthConfig:248
|
The interceptors waits this time span if there is no token |
import { Injectable, NgZone, Optional, OnDestroy, Inject } from '@angular/core';
import {
HttpClient,
HttpHeaders,
HttpParams,
HttpErrorResponse,
} from '@angular/common/http';
import {
Observable,
Subject,
Subscription,
of,
race,
from,
combineLatest,
throwError,
} from 'rxjs';
import {
filter,
delay,
first,
tap,
map,
switchMap,
debounceTime,
catchError,
} from 'rxjs/operators';
import { DOCUMENT } from '@angular/common';
import { DateTimeProvider } from './date-time-provider';
import {
ValidationHandler,
ValidationParams,
} from './token-validation/validation-handler';
import { UrlHelperService } from './url-helper.service';
import {
OAuthEvent,
OAuthInfoEvent,
OAuthErrorEvent,
OAuthSuccessEvent,
} from './events';
import {
OAuthLogger,
OAuthStorage,
LoginOptions,
ParsedIdToken,
OidcDiscoveryDoc,
TokenResponse,
} from './types';
import { b64DecodeUnicode, base64UrlEncode } from './base64-helper';
import { AuthConfig } from './auth.config';
import { WebHttpUrlEncodingCodec } from './encoder';
import { HashHandler } from './token-validation/hash-handler';
/**
* Service for logging in and logging out with
* OIDC and OAuth2. Supports implicit flow and
* password flow.
*/
@Injectable()
export class OAuthService extends AuthConfig implements OnDestroy {
// Extending AuthConfig ist just for LEGACY reasons
// to not break existing code.
/**
* The ValidationHandler used to validate received
* id_tokens.
*/
public tokenValidationHandler: ValidationHandler;
/**
* @internal
* Deprecated: use property events instead
*/
public discoveryDocumentLoaded = false;
/**
* @internal
* Deprecated: use property events instead
*/
public discoveryDocumentLoaded$: Observable<OidcDiscoveryDoc>;
/**
* Informs about events, like token_received or token_expires.
* See the string enum EventType for a full list of event types.
*/
public events: Observable<OAuthEvent>;
/**
* The received (passed around) state, when logging
* in with implicit flow.
*/
public state? = '';
protected eventsSubject: Subject<OAuthEvent> = new Subject<OAuthEvent>();
protected discoveryDocumentLoadedSubject: Subject<OidcDiscoveryDoc> =
new Subject<OidcDiscoveryDoc>();
protected silentRefreshPostMessageEventListener: EventListener;
protected grantTypesSupported: Array<string> = [];
protected _storage: OAuthStorage;
protected accessTokenTimeoutSubscription: Subscription;
protected idTokenTimeoutSubscription: Subscription;
protected tokenReceivedSubscription: Subscription;
protected automaticRefreshSubscription: Subscription;
protected sessionCheckEventListener: EventListener;
protected jwksUri: string;
protected sessionCheckTimer: any;
protected silentRefreshSubject: string;
protected inImplicitFlow = false;
protected saveNoncesInLocalStorage = false;
private document: Document;
constructor(
protected ngZone: NgZone,
protected http: HttpClient,
@Optional() storage: OAuthStorage,
@Optional() tokenValidationHandler: ValidationHandler,
@Optional() protected config: AuthConfig,
protected urlHelper: UrlHelperService,
protected logger: OAuthLogger,
@Optional() protected crypto: HashHandler,
@Inject(DOCUMENT) document: Document,
protected dateTimeService: DateTimeProvider
) {
super();
this.debug('angular-oauth2-oidc v10');
// See https://github.com/manfredsteyer/angular-oauth2-oidc/issues/773 for why this is needed
this.document = document;
if (!config) {
config = {};
}
this.discoveryDocumentLoaded$ =
this.discoveryDocumentLoadedSubject.asObservable();
this.events = this.eventsSubject.asObservable();
if (tokenValidationHandler) {
this.tokenValidationHandler = tokenValidationHandler;
}
if (config) {
this.configure(config);
}
try {
if (storage) {
this.setStorage(storage);
} else if (typeof sessionStorage !== 'undefined') {
this.setStorage(sessionStorage);
}
} catch (e) {
console.error(
'No OAuthStorage provided and cannot access default (sessionStorage).' +
'Consider providing a custom OAuthStorage implementation in your module.',
e
);
}
// in IE, sessionStorage does not always survive a redirect
if (this.checkLocalStorageAccessable()) {
const ua = window?.navigator?.userAgent;
const msie = ua?.includes('MSIE ') || ua?.includes('Trident');
if (msie) {
this.saveNoncesInLocalStorage = true;
}
}
this.setupRefreshTimer();
}
private checkLocalStorageAccessable() {
if (typeof window === 'undefined') return false;
const test = 'test';
try {
if (typeof window['localStorage'] === 'undefined') return false;
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
/**
* Use this method to configure the service
* @param config the configuration
*/
public configure(config: AuthConfig): void {
// For the sake of downward compatibility with
// original configuration API
Object.assign(this, new AuthConfig(), config);
this.config = Object.assign({} as AuthConfig, new AuthConfig(), config);
if (this.sessionChecksEnabled) {
this.setupSessionCheck();
}
this.configChanged();
}
protected configChanged(): void {
this.setupRefreshTimer();
}
public restartSessionChecksIfStillLoggedIn(): void {
if (this.hasValidIdToken()) {
this.initSessionCheck();
}
}
protected restartRefreshTimerIfStillLoggedIn(): void {
this.setupExpirationTimers();
}
protected setupSessionCheck(): void {
this.events
.pipe(filter((e) => e.type === 'token_received'))
.subscribe(() => {
this.initSessionCheck();
});
}
/**
* Will setup up silent refreshing for when the token is
* about to expire. When the user is logged out via this.logOut method, the
* silent refreshing will pause and not refresh the tokens until the user is
* logged back in via receiving a new token.
* @param params Additional parameter to pass
* @param listenTo Setup automatic refresh of a specific token type
*/
public setupAutomaticSilentRefresh(
params: object = {},
listenTo?: 'access_token' | 'id_token' | 'any',
noPrompt = true
): void {
let shouldRunSilentRefresh = true;
this.clearAutomaticRefreshTimer();
this.automaticRefreshSubscription = this.events
.pipe(
tap((e) => {
if (e.type === 'token_received') {
shouldRunSilentRefresh = true;
} else if (e.type === 'logout') {
shouldRunSilentRefresh = false;
}
}),
filter(
(e: OAuthInfoEvent) =>
e.type === 'token_expires' &&
(listenTo == null || listenTo === 'any' || e.info === listenTo)
),
debounceTime(1000)
)
.subscribe(() => {
if (shouldRunSilentRefresh) {
// this.silentRefresh(params, noPrompt).catch(_ => {
this.refreshInternal(params, noPrompt).catch(() => {
this.debug('Automatic silent refresh did not work');
});
}
});
this.restartRefreshTimerIfStillLoggedIn();
}
protected refreshInternal(
params,
noPrompt
): Promise<TokenResponse | OAuthEvent> {
if (!this.useSilentRefresh && this.responseType === 'code') {
return this.refreshToken();
} else {
return this.silentRefresh(params, noPrompt);
}
}
/**
* 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
): Promise<boolean> {
return this.loadDiscoveryDocument().then(() => {
return this.tryLogin(options);
});
}
/**
* Convenience method that first calls `loadDiscoveryDocumentAndTryLogin(...)`
* and if then chains to `initLoginFlow()`, but only if there is no valid
* IdToken or no valid AccessToken.
*
* @param options LoginOptions to pass through to `tryLogin(...)`
*/
public loadDiscoveryDocumentAndLogin(
options: LoginOptions & { state?: string } = null
): Promise<boolean> {
options = options || {};
return this.loadDiscoveryDocumentAndTryLogin(options).then(() => {
if (!this.hasValidIdToken() || !this.hasValidAccessToken()) {
const state = typeof options.state === 'string' ? options.state : '';
this.initLoginFlow(state);
return false;
} else {
return true;
}
});
}
protected debug(...args): void {
if (this.showDebugInformation) {
this.logger.debug(...args);
}
}
protected validateUrlFromDiscoveryDocument(url: string): string[] {
const errors: string[] = [];
const httpsCheck = this.validateUrlForHttps(url);
const issuerCheck = this.validateUrlAgainstIssuer(url);
if (!httpsCheck) {
errors.push(
'https for all urls required. Also for urls received by discovery.'
);
}
if (!issuerCheck) {
errors.push(
'Every url in discovery document has to start with the issuer url.' +
'Also see property strictDiscoveryDocumentValidation.'
);
}
return errors;
}
protected validateUrlForHttps(url: string): boolean {
if (!url) {
return true;
}
const lcUrl = url.toLowerCase();
if (this.requireHttps === false) {
return true;
}
if (
(lcUrl.match(/^http:\/\/localhost($|[:/])/) ||
lcUrl.match(/^http:\/\/localhost($|[:/])/)) &&
this.requireHttps === 'remoteOnly'
) {
return true;
}
return lcUrl.startsWith('https://');
}
protected assertUrlNotNullAndCorrectProtocol(
url: string | undefined,
description: string
) {
if (!url) {
throw new Error(`'${description}' should not be null`);
}
if (!this.validateUrlForHttps(url)) {
throw new Error(
`'${description}' must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).`
);
}
}
protected validateUrlAgainstIssuer(url: string) {
if (!this.strictDiscoveryDocumentValidation) {
return true;
}
if (!url) {
return true;
}
return url.toLowerCase().startsWith(this.issuer.toLowerCase());
}
protected setupRefreshTimer(): void {
if (typeof window === 'undefined') {
this.debug('timer not supported on this plattform');
return;
}
if (this.hasValidIdToken() || this.hasValidAccessToken()) {
this.clearAccessTokenTimer();
this.clearIdTokenTimer();
this.setupExpirationTimers();
}
if (this.tokenReceivedSubscription)
this.tokenReceivedSubscription.unsubscribe();
this.tokenReceivedSubscription = this.events
.pipe(filter((e) => e.type === 'token_received'))
.subscribe(() => {
this.clearAccessTokenTimer();
this.clearIdTokenTimer();
this.setupExpirationTimers();
});
}
protected setupExpirationTimers(): void {
if (this.hasValidAccessToken()) {
this.setupAccessTokenTimer();
}
if (!this.disableIdTokenTimer && this.hasValidIdToken()) {
this.setupIdTokenTimer();
}
}
protected setupAccessTokenTimer(): void {
const expiration = this.getAccessTokenExpiration();
const storedAt = this.getAccessTokenStoredAt();
const timeout = this.calcTimeout(storedAt, expiration);
this.ngZone.runOutsideAngular(() => {
this.accessTokenTimeoutSubscription = of(
new OAuthInfoEvent('token_expires', 'access_token')
)
.pipe(delay(timeout))
.subscribe((e) => {
this.ngZone.run(() => {
this.eventsSubject.next(e);
});
});
});
}
protected setupIdTokenTimer(): void {
const expiration = this.getIdTokenExpiration();
const storedAt = this.getIdTokenStoredAt();
const timeout = this.calcTimeout(storedAt, expiration);
this.ngZone.runOutsideAngular(() => {
this.idTokenTimeoutSubscription = of(
new OAuthInfoEvent('token_expires', 'id_token')
)
.pipe(delay(timeout))
.subscribe((e) => {
this.ngZone.run(() => {
this.eventsSubject.next(e);
});
});
});
}
/**
* Stops timers for automatic refresh.
* To restart it, call setupAutomaticSilentRefresh again.
*/
public stopAutomaticRefresh() {
this.clearAccessTokenTimer();
this.clearIdTokenTimer();
this.clearAutomaticRefreshTimer();
}
protected clearAccessTokenTimer(): void {
if (this.accessTokenTimeoutSubscription) {
this.accessTokenTimeoutSubscription.unsubscribe();
}
}
protected clearIdTokenTimer(): void {
if (this.idTokenTimeoutSubscription) {
this.idTokenTimeoutSubscription.unsubscribe();
}
}
protected clearAutomaticRefreshTimer(): void {
if (this.automaticRefreshSubscription) {
this.automaticRefreshSubscription.unsubscribe();
}
}
protected calcTimeout(storedAt: number, expiration: number): number {
const now = this.dateTimeService.now();
const delta =
(expiration - storedAt) * this.timeoutFactor - (now - storedAt);
const duration = Math.max(0, delta);
const maxTimeoutValue = 2_147_483_647;
return duration > maxTimeoutValue ? maxTimeoutValue : duration;
}
/**
* DEPRECATED. Use a provider for OAuthStorage instead:
*
* { provide: OAuthStorage, useFactory: oAuthStorageFactory }
* export function oAuthStorageFactory(): OAuthStorage { return localStorage; }
* Sets a custom storage used to store the received
* tokens on client side. By default, the browser's
* sessionStorage is used.
* @ignore
*
* @param storage
*/
public setStorage(storage: OAuthStorage): void {
this._storage = storage;
this.configChanged();
}
/**
* Loads the discovery document to configure most
* properties of this service. The url of the discovery
* document is infered from the issuer's url according
* to the OpenId Connect spec. To use another url you
* can pass it to to optional parameter fullUrl.
*
* @param fullUrl
*/
public loadDiscoveryDocument(
fullUrl: string = null
): Promise<OAuthSuccessEvent> {
return new Promise((resolve, reject) => {
if (!fullUrl) {
fullUrl = this.issuer || '';
if (!fullUrl.endsWith('/')) {
fullUrl += '/';
}
fullUrl += '.well-known/openid-configuration';
}
if (!this.validateUrlForHttps(fullUrl)) {
reject(
"issuer must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)."
);
return;
}
this.http.get<OidcDiscoveryDoc>(fullUrl).subscribe(
(doc) => {
if (!this.validateDiscoveryDocument(doc)) {
this.eventsSubject.next(
new OAuthErrorEvent('discovery_document_validation_error', null)
);
reject('discovery_document_validation_error');
return;
}
this.loginUrl = doc.authorization_endpoint;
this.logoutUrl = doc.end_session_endpoint || this.logoutUrl;
this.grantTypesSupported = doc.grant_types_supported;
this.issuer = doc.issuer;
this.tokenEndpoint = doc.token_endpoint;
this.userinfoEndpoint =
doc.userinfo_endpoint || this.userinfoEndpoint;
this.jwksUri = doc.jwks_uri;
this.sessionCheckIFrameUrl =
doc.check_session_iframe || this.sessionCheckIFrameUrl;
this.discoveryDocumentLoaded = true;
this.discoveryDocumentLoadedSubject.next(doc);
this.revocationEndpoint =
doc.revocation_endpoint || this.revocationEndpoint;
if (this.sessionChecksEnabled) {
this.restartSessionChecksIfStillLoggedIn();
}
this.loadJwks()
.then((jwks) => {
const result: object = {
discoveryDocument: doc,
jwks: jwks,
};
const event = new OAuthSuccessEvent(
'discovery_document_loaded',
result
);
this.eventsSubject.next(event);
resolve(event);
return;
})
.catch((err) => {
this.eventsSubject.next(
new OAuthErrorEvent('discovery_document_load_error', err)
);
reject(err);
return;
});
},
(err) => {
this.logger.error('error loading discovery document', err);
this.eventsSubject.next(
new OAuthErrorEvent('discovery_document_load_error', err)
);
reject(err);
}
);
});
}
protected loadJwks(): Promise<object> {
return new Promise<object>((resolve, reject) => {
if (this.jwksUri) {
this.http.get(this.jwksUri).subscribe(
(jwks) => {
this.jwks = jwks;
// this.eventsSubject.next(
// new OAuthSuccessEvent('discovery_document_loaded')
// );
resolve(jwks);
},
(err) => {
this.logger.error('error loading jwks', err);
this.eventsSubject.next(
new OAuthErrorEvent('jwks_load_error', err)
);
reject(err);
}
);
} else {
resolve(null);
}
});
}
protected validateDiscoveryDocument(doc: OidcDiscoveryDoc): boolean {
let errors: string[];
if (!this.skipIssuerCheck && doc.issuer !== this.issuer) {
this.logger.error(
'invalid issuer in discovery document',
'expected: ' + this.issuer,
'current: ' + doc.issuer
);
return false;
}
errors = this.validateUrlFromDiscoveryDocument(doc.authorization_endpoint);
if (errors.length > 0) {
this.logger.error(
'error validating authorization_endpoint in discovery document',
errors
);
return false;
}
errors = this.validateUrlFromDiscoveryDocument(doc.end_session_endpoint);
if (errors.length > 0) {
this.logger.error(
'error validating end_session_endpoint in discovery document',
errors
);
return false;
}
errors = this.validateUrlFromDiscoveryDocument(doc.token_endpoint);
if (errors.length > 0) {
this.logger.error(
'error validating token_endpoint in discovery document',
errors
);
}
errors = this.validateUrlFromDiscoveryDocument(doc.revocation_endpoint);
if (errors.length > 0) {
this.logger.error(
'error validating revocation_endpoint in discovery document',
errors
);
}
errors = this.validateUrlFromDiscoveryDocument(doc.userinfo_endpoint);
if (errors.length > 0) {
this.logger.error(
'error validating userinfo_endpoint in discovery document',
errors
);
return false;
}
errors = this.validateUrlFromDiscoveryDocument(doc.jwks_uri);
if (errors.length > 0) {
this.logger.error(
'error validating jwks_uri in discovery document',
errors
);
return false;
}
if (this.sessionChecksEnabled && !doc.check_session_iframe) {
this.logger.warn(
'sessionChecksEnabled is activated but discovery document' +
' does not contain a check_session_iframe field'
);
}
return true;
}
/**
* Uses password flow to exchange userName and password for an
* access_token. After receiving the access_token, this method
* uses it to query the userinfo endpoint in order to get information
* about the user in question.
*
* When using this, make sure that the property oidc is set to false.
* Otherwise stricter validations take place that make this operation
* fail.
*
* @param userName
* @param password
* @param headers Optional additional http-headers.
*/
public fetchTokenUsingPasswordFlowAndLoadUserProfile(
userName: string,
password: string,
headers: HttpHeaders = new HttpHeaders()
): Promise<object> {
return this.fetchTokenUsingPasswordFlow(userName, password, headers).then(
() => this.loadUserProfile()
);
}
/**
* 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 place that make this operation fail.
*/
public loadUserProfile(): Promise<object> {
if (!this.hasValidAccessToken()) {
throw new Error('Can not load User Profile without access_token');
}
if (!this.validateUrlForHttps(this.userinfoEndpoint)) {
throw new Error(
"userinfoEndpoint must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)."
);
}
return new Promise((resolve, reject) => {
const headers = new HttpHeaders().set(
'Authorization',
'Bearer ' + this.getAccessToken()
);
this.http
.get(this.userinfoEndpoint, {
headers,
observe: 'response',
responseType: 'text',
})
.subscribe(
(response) => {
this.debug('userinfo received', JSON.stringify(response));
if (
response.headers
.get('content-type')
.startsWith('application/json')
) {
let info = JSON.parse(response.body);
const existingClaims = this.getIdentityClaims() || {};
if (!this.skipSubjectCheck) {
if (
this.oidc &&
(!existingClaims['sub'] || info.sub !== existingClaims['sub'])
) {
const err =
'if property oidc is true, the received user-id (sub) has to be the user-id ' +
'of the user that has logged in with oidc.\n' +
'if you are not using oidc but just oauth2 password flow set oidc to false';
reject(err);
return;
}
}
info = Object.assign({}, existingClaims, info);
this._storage.setItem(
'id_token_claims_obj',
JSON.stringify(info)
);
this.eventsSubject.next(
new OAuthSuccessEvent('user_profile_loaded')
);
resolve({ info });
} else {
this.debug('userinfo is not JSON, treating it as JWE/JWS');
this.eventsSubject.next(
new OAuthSuccessEvent('user_profile_loaded')
);
resolve(JSON.parse(response.body));
}
},
(err) => {
this.logger.error('error loading user info', err);
this.eventsSubject.next(
new OAuthErrorEvent('user_profile_load_error', err)
);
reject(err);
}
);
});
}
/**
* Uses password flow to exchange userName and password for an access_token.
* @param userName
* @param password
* @param headers Optional additional http-headers.
*/
public fetchTokenUsingPasswordFlow(
userName: string,
password: string,
headers: HttpHeaders = new HttpHeaders()
): Promise<TokenResponse> {
const parameters = {
username: userName,
password: password,
};
return this.fetchTokenUsingGrant('password', parameters, headers);
}
/**
* Uses a custom grant type to retrieve tokens.
* @param grantType Grant type.
* @param parameters Parameters to pass.
* @param headers Optional additional HTTP headers.
*/
public fetchTokenUsingGrant(
grantType: string,
parameters: object,
headers: HttpHeaders = new HttpHeaders()
): Promise<TokenResponse> {
this.assertUrlNotNullAndCorrectProtocol(
this.tokenEndpoint,
'tokenEndpoint'
);
/**
* A `HttpParameterCodec` that uses `encodeURIComponent` and `decodeURIComponent` to
* serialize and parse URL parameter keys and values.
*
* @stable
*/
let params = new HttpParams({ encoder: new WebHttpUrlEncodingCodec() })
.set('grant_type', grantType)
.set('scope', this.scope);
if (this.useHttpBasicAuth) {
const header = btoa(`${this.clientId}:${this.dummyClientSecret}`);
headers = headers.set('Authorization', 'Basic ' + header);
}
if (!this.useHttpBasicAuth) {
params = params.set('client_id', this.clientId);
}
if (!this.useHttpBasicAuth && this.dummyClientSecret) {
params = params.set('client_secret', this.dummyClientSecret);
}
if (this.customQueryParams) {
for (const key of Object.getOwnPropertyNames(this.customQueryParams)) {
params = params.set(key, this.customQueryParams[key]);
}
}
// set explicit parameters last, to allow overwriting
for (const key of Object.keys(parameters)) {
params = params.set(key, parameters[key]);
}
headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
return new Promise((resolve, reject) => {
this.http
.post<TokenResponse>(this.tokenEndpoint, params, { headers })
.subscribe(
(tokenResponse) => {
this.debug('tokenResponse', tokenResponse);
this.storeAccessTokenResponse(
tokenResponse.access_token,
tokenResponse.refresh_token,
tokenResponse.expires_in ||
this.fallbackAccessTokenExpirationTimeInSec,
tokenResponse.scope,
this.extractRecognizedCustomParameters(tokenResponse)
);
if (this.oidc && tokenResponse.id_token) {
this.processIdToken(
tokenResponse.id_token,
tokenResponse.access_token
).then((result) => {
this.storeIdToken(result);
resolve(tokenResponse);
});
}
this.eventsSubject.next(new OAuthSuccessEvent('token_received'));
resolve(tokenResponse);
},
(err) => {
this.logger.error('Error performing ${grantType} flow', err);
this.eventsSubject.next(new OAuthErrorEvent('token_error', err));
reject(err);
}
);
});
}
/**
* Refreshes the token using a refresh_token.
* This does not work for implicit flow, b/c
* there is no refresh_token in this flow.
* A solution for this is provided by the
* method silentRefresh.
*/
public refreshToken(): Promise<TokenResponse> {
this.assertUrlNotNullAndCorrectProtocol(
this.tokenEndpoint,
'tokenEndpoint'
);
return new Promise((resolve, reject) => {
let params = new HttpParams({ encoder: new WebHttpUrlEncodingCodec() })
.set('grant_type', 'refresh_token')
.set('scope', this.scope)
.set('refresh_token', this._storage.getItem('refresh_token'));
let headers = new HttpHeaders().set(
'Content-Type',
'application/x-www-form-urlencoded'
);
if (this.useHttpBasicAuth) {
const header = btoa(`${this.clientId}:${this.dummyClientSecret}`);
headers = headers.set('Authorization', 'Basic ' + header);
}
if (!this.useHttpBasicAuth) {
params = params.set('client_id', this.clientId);
}
if (!this.useHttpBasicAuth && this.dummyClientSecret) {
params = params.set('client_secret', this.dummyClientSecret);
}
if (this.customQueryParams) {
for (const key of Object.getOwnPropertyNames(this.customQueryParams)) {
params = params.set(key, this.customQueryParams[key]);
}
}
this.http
.post<TokenResponse>(this.tokenEndpoint, params, { headers })
.pipe(
switchMap((tokenResponse) => {
if (this.oidc && tokenResponse.id_token) {
return from(
this.processIdToken(
tokenResponse.id_token,
tokenResponse.access_token,
true
)
).pipe(
tap((result) => this.storeIdToken(result)),
map(() => tokenResponse)
);
} else {
return of(tokenResponse);
}
})
)
.subscribe(
(tokenResponse) => {
this.debug('refresh tokenResponse', tokenResponse);
this.storeAccessTokenResponse(
tokenResponse.access_token,
tokenResponse.refresh_token,
tokenResponse.expires_in ||
this.fallbackAccessTokenExpirationTimeInSec,
tokenResponse.scope,
this.extractRecognizedCustomParameters(tokenResponse)
);
this.eventsSubject.next(new OAuthSuccessEvent('token_received'));
this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed'));
resolve(tokenResponse);
},
(err) => {
this.logger.error('Error refreshing token', err);
this.eventsSubject.next(
new OAuthErrorEvent('token_refresh_error', err)
);
reject(err);
}
);
});
}
protected removeSilentRefreshEventListener(): void {
if (this.silentRefreshPostMessageEventListener) {
window.removeEventListener(
'message',
this.silentRefreshPostMessageEventListener
);
this.silentRefreshPostMessageEventListener = null;
}
}
protected setupSilentRefreshEventListener(): void {
this.removeSilentRefreshEventListener();
this.silentRefreshPostMessageEventListener = (e: MessageEvent) => {
const message = this.processMessageEventMessage(e);
if (this.checkOrigin && e.origin !== location.origin) {
console.error('wrong origin requested silent refresh!');
}
this.tryLogin({
customHashFragment: message,
preventClearHashAfterLogin: true,
customRedirectUri: this.silentRefreshRedirectUri || this.redirectUri,
}).catch((err) =>
this.debug('tryLogin during silent refresh failed', err)
);
};
window.addEventListener(
'message',
this.silentRefreshPostMessageEventListener
);
}
/**
* Performs a silent refresh for implicit flow.
* Use this method to get new tokens when/before
* the existing tokens expire.
*/
public silentRefresh(
params: object = {},
noPrompt = true
): Promise<OAuthEvent> {
const claims: object = this.getIdentityClaims() || {};
if (this.useIdTokenHintForSilentRefresh && this.hasValidIdToken()) {
params['id_token_hint'] = this.getIdToken();
}
if (!this.validateUrlForHttps(this.loginUrl)) {
throw new Error(
"loginUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)."
);
}
if (typeof this.document === 'undefined') {
throw new Error('silent refresh is not supported on this platform');
}
const existingIframe = this.document.getElementById(
this.silentRefreshIFrameName
);
if (existingIframe) {
this.document.body.removeChild(existingIframe);
}
this.silentRefreshSubject = claims['sub'];
const iframe = this.document.createElement('iframe');
iframe.id = this.silentRefreshIFrameName;
this.setupSilentRefreshEventListener();
const redirectUri = this.silentRefreshRedirectUri || this.redirectUri;
this.createLoginUrl(null, null, redirectUri, noPrompt, params).then(
(url) => {
iframe.setAttribute('src', url);
if (!this.silentRefreshShowIFrame) {
iframe.style['display'] = 'none';
}
this.document.body.appendChild(iframe);
}
);
const errors = this.events.pipe(
filter((e) => e instanceof OAuthErrorEvent),
first()
);
const success = this.events.pipe(
filter((e) => e.type === 'token_received'),
first()
);
const timeout = of(
new OAuthErrorEvent('silent_refresh_timeout', null)
).pipe(delay(this.silentRefreshTimeout));
return race([errors, success, timeout])
.pipe(
map((e) => {
if (e instanceof OAuthErrorEvent) {
if (e.type === 'silent_refresh_timeout') {
this.eventsSubject.next(e);
} else {
e = new OAuthErrorEvent('silent_refresh_error', e);
this.eventsSubject.next(e);
}
throw e;
} else if (e.type === 'token_received') {
e = new OAuthSuccessEvent('silently_refreshed');
this.eventsSubject.next(e);
}
return e;
})
)
.toPromise();
}
/**
* This method exists for backwards compatibility.
* {@link OAuthService#initLoginFlowInPopup} handles both code
* and implicit flows.
*/
public initImplicitFlowInPopup(options?: {
height?: number;
width?: number;
windowRef?: Window;
}) {
return this.initLoginFlowInPopup(options);
}
public initLoginFlowInPopup(options?: {
height?: number;
width?: number;
windowRef?: Window;
}) {
options = options || {};
return this.createLoginUrl(
null,
null,
this.silentRefreshRedirectUri,
false,
{
display: 'popup',
}
).then((url) => {
return new Promise((resolve, reject) => {
/**
* Error handling section
*/
const checkForPopupClosedInterval = 500;
let windowRef = null;
// If we got no window reference we open a window
// else we are using the window already opened
if (!options.windowRef) {
windowRef = window.open(
url,
'ngx-oauth2-oidc-login',
this.calculatePopupFeatures(options)
);
} else if (options.windowRef && !options.windowRef.closed) {
windowRef = options.windowRef;
windowRef.location.href = url;
}
let checkForPopupClosedTimer: any;
const tryLogin = (hash: string) => {
this.tryLogin({
customHashFragment: hash,
preventClearHashAfterLogin: true,
customRedirectUri: this.silentRefreshRedirectUri,
}).then(
() => {
cleanup();
resolve(true);
},
(err) => {
cleanup();
reject(err);
}
);
};
const checkForPopupClosed = () => {
if (!windowRef || windowRef.closed) {
cleanup();
reject(new OAuthErrorEvent('popup_closed', {}));
}
};
if (!windowRef) {
reject(new OAuthErrorEvent('popup_blocked', {}));
} else {
checkForPopupClosedTimer = window.setInterval(
checkForPopupClosed,
checkForPopupClosedInterval
);
}
const cleanup = () => {
window.clearInterval(checkForPopupClosedTimer);
window.removeEventListener('storage', storageListener);
window.removeEventListener('message', listener);
if (windowRef !== null) {
windowRef.close();
}
windowRef = null;
};
const listener = (e: MessageEvent) => {
const message = this.processMessageEventMessage(e);
if (message && message !== null) {
window.removeEventListener('storage', storageListener);
tryLogin(message);
} else {
console.log('false event firing');
}
};
const storageListener = (event: StorageEvent) => {
if (event.key === 'auth_hash') {
window.removeEventListener('message', listener);
tryLogin(event.newValue);
}
};
window.addEventListener('message', listener);
window.addEventListener('storage', storageListener);
});
});
}
protected calculatePopupFeatures(options: {
height?: number;
width?: number;
}): string {
// Specify an static height and width and calculate centered position
const height = options.height || 470;
const width = options.width || 500;
const left = window.screenLeft + (window.outerWidth - width) / 2;
const top = window.screenTop + (window.outerHeight - height) / 2;
return `location=no,toolbar=no,width=${width},height=${height},top=${top},left=${left}`;
}
protected processMessageEventMessage(e: MessageEvent): string {
let expectedPrefix = '#';
if (this.silentRefreshMessagePrefix) {
expectedPrefix += this.silentRefreshMessagePrefix;
}
if (!e || !e.data || typeof e.data !== 'string') {
return;
}
const prefixedMessage: string = e.data;
if (!prefixedMessage.startsWith(expectedPrefix)) {
return;
}
return '#' + prefixedMessage.substr(expectedPrefix.length);
}
protected canPerformSessionCheck(): boolean {
if (!this.sessionChecksEnabled) {
return false;
}
if (!this.sessionCheckIFrameUrl) {
console.warn(
'sessionChecksEnabled is activated but there is no sessionCheckIFrameUrl'
);
return false;
}
const sessionState = this.getSessionState();
if (!sessionState) {
console.warn(
'sessionChecksEnabled is activated but there is no session_state'
);
return false;
}
if (typeof this.document === 'undefined') {
return false;
}
return true;
}
protected setupSessionCheckEventListener(): void {
this.removeSessionCheckEventListener();
this.sessionCheckEventListener = (e: MessageEvent) => {
const origin = e.origin.toLowerCase();
const issuer = this.issuer.toLowerCase();
this.debug('sessionCheckEventListener');
if (!issuer.startsWith(origin)) {
this.debug(
'sessionCheckEventListener',
'wrong origin',
origin,
'expected',
issuer,
'event',
e
);
return;
}
// only run in Angular zone if it is 'changed' or 'error'
switch (e.data) {
case 'unchanged':
this.ngZone.run(() => {
this.handleSessionUnchanged();
});
break;
case 'changed':
this.ngZone.run(() => {
this.handleSessionChange();
});
break;
case 'error':
this.ngZone.run(() => {
this.handleSessionError();
});
break;
}
this.debug('got info from session check inframe', e);
};
// prevent Angular from refreshing the view on every message (runs in intervals)
this.ngZone.runOutsideAngular(() => {
window.addEventListener('message', this.sessionCheckEventListener);
});
}
protected handleSessionUnchanged(): void {
this.debug('session check', 'session unchanged');
this.eventsSubject.next(new OAuthInfoEvent('session_unchanged'));
}
protected handleSessionChange(): void {
this.eventsSubject.next(new OAuthInfoEvent('session_changed'));
this.stopSessionCheckTimer();
if (!this.useSilentRefresh && this.responseType === 'code') {
this.refreshToken()
.then(() => {
this.debug('token refresh after session change worked');
})
.catch(() => {
this.debug('token refresh did not work after session changed');
this.eventsSubject.next(new OAuthInfoEvent('session_terminated'));
this.logOut(true);
});
} else if (this.silentRefreshRedirectUri) {
this.silentRefresh().catch(() =>
this.debug('silent refresh failed after session changed')
);
this.waitForSilentRefreshAfterSessionChange();
} else {
this.eventsSubject.next(new OAuthInfoEvent('session_terminated'));
this.logOut(true);
}
}
protected waitForSilentRefreshAfterSessionChange(): void {
this.events
.pipe(
filter(
(e: OAuthEvent) =>
e.type === 'silently_refreshed' ||
e.type === 'silent_refresh_timeout' ||
e.type === 'silent_refresh_error'
),
first()
)
.subscribe((e) => {
if (e.type !== 'silently_refreshed') {
this.debug('silent refresh did not work after session changed');
this.eventsSubject.next(new OAuthInfoEvent('session_terminated'));
this.logOut(true);
}
});
}
protected handleSessionError(): void {
this.stopSessionCheckTimer();
this.eventsSubject.next(new OAuthInfoEvent('session_error'));
}
protected removeSessionCheckEventListener(): void {
if (this.sessionCheckEventListener) {
window.removeEventListener('message', this.sessionCheckEventListener);
this.sessionCheckEventListener = null;
}
}
protected initSessionCheck(): void {
if (!this.canPerformSessionCheck()) {
return;
}
const existingIframe = this.document.getElementById(
this.sessionCheckIFrameName
);
if (existingIframe) {
this.document.body.removeChild(existingIframe);
}
const iframe = this.document.createElement('iframe');
iframe.id = this.sessionCheckIFrameName;
this.setupSessionCheckEventListener();
const url = this.sessionCheckIFrameUrl;
iframe.setAttribute('src', url);
iframe.style.display = 'none';
this.document.body.appendChild(iframe);
this.startSessionCheckTimer();
}
protected startSessionCheckTimer(): void {
this.stopSessionCheckTimer();
this.ngZone.runOutsideAngular(() => {
this.sessionCheckTimer = setInterval(
this.checkSession.bind(this),
this.sessionCheckIntervall
);
});
}
protected stopSessionCheckTimer(): void {
if (this.sessionCheckTimer) {
clearInterval(this.sessionCheckTimer);
this.sessionCheckTimer = null;
}
}
public checkSession(): void {
const iframe: any = this.document.getElementById(
this.sessionCheckIFrameName
);
if (!iframe) {
this.logger.warn(
'checkSession did not find iframe',
this.sessionCheckIFrameName
);
}
const sessionState = this.getSessionState();
if (!sessionState) {
this.stopSessionCheckTimer();
}
const message = this.clientId + ' ' + sessionState;
iframe.contentWindow.postMessage(message, this.issuer);
}
protected async createLoginUrl(
state = '',
loginHint = '',
customRedirectUri = '',
noPrompt = false,
params: object = {}
): Promise<string> {
const that = this; // eslint-disable-line @typescript-eslint/no-this-alias
let redirectUri: string;
if (customRedirectUri) {
redirectUri = customRedirectUri;
} else {
redirectUri = this.redirectUri;
}
const nonce = await this.createAndSaveNonce();
if (state) {
state =
nonce + this.config.nonceStateSeparator + encodeURIComponent(state);
} else {
state = nonce;
}
if (!this.requestAccessToken && !this.oidc) {
throw new Error('Either requestAccessToken or oidc or both must be true');
}
if (this.config.responseType) {
this.responseType = this.config.responseType;
} else {
if (this.oidc && this.requestAccessToken) {
this.responseType = 'id_token token';
} else if (this.oidc && !this.requestAccessToken) {
this.responseType = 'id_token';
} else {
this.responseType = 'token';
}
}
const seperationChar = that.loginUrl.indexOf('?') > -1 ? '&' : '?';
let scope = that.scope;
if (this.oidc && !scope.match(/(^|\s)openid($|\s)/)) {
scope = 'openid ' + scope;
}
let url =
that.loginUrl +
seperationChar +
'response_type=' +
encodeURIComponent(that.responseType) +
'&client_id=' +
encodeURIComponent(that.clientId) +
'&state=' +
encodeURIComponent(state) +
'&redirect_uri=' +
encodeURIComponent(redirectUri) +
'&scope=' +
encodeURIComponent(scope);
if (this.responseType.includes('code') && !this.disablePKCE) {
const [challenge, verifier] =
await this.createChallangeVerifierPairForPKCE();
if (
this.saveNoncesInLocalStorage &&
typeof window['localStorage'] !== 'undefined'
) {
localStorage.setItem('PKCE_verifier', verifier);
} else {
this._storage.setItem('PKCE_verifier', verifier);
}
url += '&code_challenge=' + challenge;
url += '&code_challenge_method=S256';
}
if (loginHint) {
url += '&login_hint=' + encodeURIComponent(loginHint);
}
if (that.resource) {
url += '&resource=' + encodeURIComponent(that.resource);
}
if (that.oidc) {
url += '&nonce=' + encodeURIComponent(nonce);
}
if (noPrompt) {
url += '&prompt=none';
}
for (const key of Object.keys(params)) {
url +=
'&' + encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}
if (this.customQueryParams) {
for (const key of Object.getOwnPropertyNames(this.customQueryParams)) {
url +=
'&' + key + '=' + encodeURIComponent(this.customQueryParams[key]);
}
}
return url;
}
initImplicitFlowInternal(
additionalState = '',
params: string | object = ''
): void {
if (this.inImplicitFlow) {
return;
}
this.inImplicitFlow = true;
if (!this.validateUrlForHttps(this.loginUrl)) {
throw new Error(
"loginUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)."
);
}
let addParams: object = {};
let loginHint: string = null;
if (typeof params === 'string') {
loginHint = params;
} else if (typeof params === 'object') {
addParams = params;
}
this.createLoginUrl(additionalState, loginHint, null, false, addParams)
.then(this.config.openUri)
.catch((error) => {
console.error('Error in initImplicitFlow', error);
this.inImplicitFlow = false;
});
}
/**
* Starts the implicit flow and redirects to user to
* the auth servers' login url.
*
* @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)
*/
public initImplicitFlow(
additionalState = '',
params: string | object = ''
): void {
if (this.loginUrl !== '') {
this.initImplicitFlowInternal(additionalState, params);
} else {
this.events
.pipe(filter((e) => e.type === 'discovery_document_loaded'))
.subscribe(() =>
this.initImplicitFlowInternal(additionalState, params)
);
}
}
/**
* Reset current implicit flow
*
* @description This method allows resetting the current implict flow in order to be initialized again.
*/
public resetImplicitFlow(): void {
this.inImplicitFlow = false;
}
protected callOnTokenReceivedIfExists(options: LoginOptions): void {
const that = this; // eslint-disable-line @typescript-eslint/no-this-alias
if (options.onTokenReceived) {
const tokenParams = {
idClaims: that.getIdentityClaims(),
idToken: that.getIdToken(),
accessToken: that.getAccessToken(),
state: that.state,
};
options.onTokenReceived(tokenParams);
}
}
protected storeAccessTokenResponse(
accessToken: string,
refreshToken: string,
expiresIn: number,
grantedScopes: string,
customParameters?: Map<string, string>
): void {
this._storage.setItem('access_token', accessToken);
if (grantedScopes && !Array.isArray(grantedScopes)) {
this._storage.setItem(
'granted_scopes',
JSON.stringify(grantedScopes.split(' '))
);
} else if (grantedScopes && Array.isArray(grantedScopes)) {
this._storage.setItem('granted_scopes', JSON.stringify(grantedScopes));
}
this._storage.setItem(
'access_token_stored_at',
'' + this.dateTimeService.now()
);
if (expiresIn) {
const expiresInMilliSeconds = expiresIn * 1000;
const now = this.dateTimeService.new();
const expiresAt = now.getTime() + expiresInMilliSeconds;
this._storage.setItem('expires_at', '' + expiresAt);
}
if (refreshToken) {
this._storage.setItem('refresh_token', refreshToken);
}
if (customParameters) {
customParameters.forEach((value: string, key: string) => {
this._storage.setItem(key, value);
});
}
}
/**
* Delegates to tryLoginImplicitFlow for the sake of competability
* @param options Optional options.
*/
public tryLogin(options: LoginOptions = null): Promise<boolean> {
if (this.config.responseType === 'code') {
return this.tryLoginCodeFlow(options).then(() => true);
} else {
return this.tryLoginImplicitFlow(options);
}
}
private parseQueryString(queryString: string): object {
if (!queryString || queryString.length === 0) {
return {};
}
if (queryString.charAt(0) === '?') {
queryString = queryString.substr(1);
}
return this.urlHelper.parseQueryString(queryString);
}
public async tryLoginCodeFlow(options: LoginOptions = null): Promise<void> {
options = options || {};
const querySource = options.customHashFragment
? options.customHashFragment.substring(1)
: window.location.search;
const parts = this.getCodePartsFromUrl(querySource);
const code = parts['code'];
const state = parts['state'];
const sessionState = parts['session_state'];
if (!options.preventClearHashAfterLogin) {
const href =
location.origin +
location.pathname +
location.search
.replace(/code=[^&$]*/, '')
.replace(/scope=[^&$]*/, '')
.replace(/state=[^&$]*/, '')
.replace(/session_state=[^&$]*/, '')
.replace(/^\?&/, '?')
.replace(/&$/, '')
.replace(/^\?$/, '')
.replace(/&+/g, '&')
.replace(/\?&/, '?')
.replace(/\?$/, '') +
location.hash;
history.replaceState(null, window.name, href);
}
const [nonceInState, userState] = this.parseState(state);
this.state = userState;
if (parts['error']) {
this.debug('error trying to login');
this.handleLoginError(options, parts);
const err = new OAuthErrorEvent('code_error', {}, parts);
this.eventsSubject.next(err);
return Promise.reject(err);
}
if (!options.disableNonceCheck) {
if (!nonceInState) {
this.saveRequestedRoute();
return Promise.resolve();
}
if (!options.disableOAuth2StateCheck) {
const success = this.validateNonce(nonceInState);
if (!success) {
const event = new OAuthErrorEvent('invalid_nonce_in_state', null);
this.eventsSubject.next(event);
return Promise.reject(event);
}
}
}
this.storeSessionState(sessionState);
if (code) {
await this.getTokenFromCode(code, options);
this.restoreRequestedRoute();
return Promise.resolve();
} else {
return Promise.resolve();
}
}
private saveRequestedRoute() {
if (this.config.preserveRequestedRoute) {
this._storage.setItem(
'requested_route',
window.location.pathname + window.location.search
);
}
}
private restoreRequestedRoute() {
const requestedRoute = this._storage.getItem('requested_route');
if (requestedRoute) {
history.replaceState(null, '', window.location.origin + requestedRoute);
}
}
/**
* Retrieve the returned auth code from the redirect uri that has been called.
* If required also check hash, as we could use hash location strategy.
*/
private getCodePartsFromUrl(queryString: string): object {
if (!queryString || queryString.length === 0) {
return this.urlHelper.getHashFragmentParams();
}
// normalize query string
if (queryString.charAt(0) === '?') {
queryString = queryString.substr(1);
}
return this.urlHelper.parseQueryString(queryString);
}
/**
* Get token using an intermediate code. Works for the Authorization Code flow.
*/
private getTokenFromCode(
code: string,
options: LoginOptions
): Promise<object> {
let params = new HttpParams({ encoder: new WebHttpUrlEncodingCodec() })
.set('grant_type', 'authorization_code')
.set('code', code)
.set('redirect_uri', options.customRedirectUri || this.redirectUri);
if (!this.disablePKCE) {
let PKCEVerifier;
if (
this.saveNoncesInLocalStorage &&
typeof window['localStorage'] !== 'undefined'
) {
PKCEVerifier = localStorage.getItem('PKCE_verifier');
} else {
PKCEVerifier = this._storage.getItem('PKCE_verifier');
}
if (!PKCEVerifier) {
console.warn('No PKCE verifier found in oauth storage!');
} else {
params = params.set('code_verifier', PKCEVerifier);
}
}
return this.fetchAndProcessToken(params, options);
}
private fetchAndProcessToken(
params: HttpParams,
options: LoginOptions
): Promise<TokenResponse> {
options = options || {};
this.assertUrlNotNullAndCorrectProtocol(
this.tokenEndpoint,
'tokenEndpoint'
);
let headers = new HttpHeaders().set(
'Content-Type',
'application/x-www-form-urlencoded'
);
if (this.useHttpBasicAuth) {
const header = btoa(`${this.clientId}:${this.dummyClientSecret}`);
headers = headers.set('Authorization', 'Basic ' + header);
}
if (!this.useHttpBasicAuth) {
params = params.set('client_id', this.clientId);
}
if (!this.useHttpBasicAuth && this.dummyClientSecret) {
params = params.set('client_secret', this.dummyClientSecret);
}
return new Promise((resolve, reject) => {
if (this.customQueryParams) {
for (const key of Object.getOwnPropertyNames(this.customQueryParams)) {
params = params.set(key, this.customQueryParams[key]);
}
}
this.http
.post<TokenResponse>(this.tokenEndpoint, params, { headers })
.subscribe(
(tokenResponse) => {
this.debug('refresh tokenResponse', tokenResponse);
this.storeAccessTokenResponse(
tokenResponse.access_token,
tokenResponse.refresh_token,
tokenResponse.expires_in ||
this.fallbackAccessTokenExpirationTimeInSec,
tokenResponse.scope,
this.extractRecognizedCustomParameters(tokenResponse)
);
if (this.oidc && tokenResponse.id_token) {
this.processIdToken(
tokenResponse.id_token,
tokenResponse.access_token,
options.disableNonceCheck
)
.then((result) => {
this.storeIdToken(result);
this.eventsSubject.next(
new OAuthSuccessEvent('token_received')
);
this.eventsSubject.next(
new OAuthSuccessEvent('token_refreshed')
);
resolve(tokenResponse);
})
.catch((reason) => {
this.eventsSubject.next(
new OAuthErrorEvent('token_validation_error', reason)
);
console.error('Error validating tokens');
console.error(reason);
reject(reason);
});
} else {
this.eventsSubject.next(new OAuthSuccessEvent('token_received'));
this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed'));
resolve(tokenResponse);
}
},
(err) => {
console.error('Error getting token', err);
this.eventsSubject.next(
new OAuthErrorEvent('token_refresh_error', err)
);
reject(err);
}
);
});
}
/**
* Checks whether there are tokens in the hash fragment
* as a result of the implicit flow. These tokens are
* parsed, validated and used to sign the user in to the
* current client.
*
* @param options Optional options.
*/
public tryLoginImplicitFlow(options: LoginOptions = null): Promise<boolean> {
options = options || {};
let parts: object;
if (options.customHashFragment) {
parts = this.urlHelper.getHashFragmentParams(options.customHashFragment);
} else {
parts = this.urlHelper.getHashFragmentParams();
}
this.debug('parsed url', parts);
const state = parts['state'];
const [nonceInState, userState] = this.parseState(state);
this.state = userState;
if (parts['error']) {
this.debug('error trying to login');
this.handleLoginError(options, parts);
const err = new OAuthErrorEvent('token_error', {}, parts);
this.eventsSubject.next(err);
return Promise.reject(err);
}
const accessToken = parts['access_token'];
const idToken = parts['id_token'];
const sessionState = parts['session_state'];
const grantedScopes = parts['scope'];
if (!this.requestAccessToken && !this.oidc) {
return Promise.reject(
'Either requestAccessToken or oidc (or both) must be true.'
);
}
if (this.requestAccessToken && !accessToken) {
return Promise.resolve(false);
}
if (this.requestAccessToken && !options.disableOAuth2StateCheck && !state) {
return Promise.resolve(false);
}
if (this.oidc && !idToken) {
return Promise.resolve(false);
}
if (this.sessionChecksEnabled && !sessionState) {
this.logger.warn(
'session checks (Session Status Change Notification) ' +
'were activated in the configuration but the id_token ' +
'does not contain a session_state claim'
);
}
if (this.requestAccessToken && !options.disableNonceCheck) {
const success = this.validateNonce(nonceInState);
if (!success) {
const event = new OAuthErrorEvent('invalid_nonce_in_state', null);
this.eventsSubject.next(event);
return Promise.reject(event);
}
}
if (this.requestAccessToken) {
this.storeAccessTokenResponse(
accessToken,
null,
parts['expires_in'] || this.fallbackAccessTokenExpirationTimeInSec,
grantedScopes
);
}
if (!this.oidc) {
this.eventsSubject.next(new OAuthSuccessEvent('token_received'));
if (this.clearHashAfterLogin && !options.preventClearHashAfterLogin) {
this.clearLocationHash();
}
this.callOnTokenReceivedIfExists(options);
return Promise.resolve(true);
}
return this.processIdToken(idToken, accessToken, options.disableNonceCheck)
.then((result) => {
if (options.validationHandler) {
return options
.validationHandler({
accessToken: accessToken,
idClaims: result.idTokenClaims,
idToken: result.idToken,
state: state,
})
.then(() => result);
}
return result;
})
.then((result) => {
this.storeIdToken(result);
this.storeSessionState(sessionState);
if (this.clearHashAfterLogin && !options.preventClearHashAfterLogin) {
this.clearLocationHash();
}
this.eventsSubject.next(new OAuthSuccessEvent('token_received'));
this.callOnTokenReceivedIfExists(options);
this.inImplicitFlow = false;
return true;
})
.catch((reason) => {
this.eventsSubject.next(
new OAuthErrorEvent('token_validation_error', reason)
);
this.logger.error('Error validating tokens');
this.logger.error(reason);
return Promise.reject(reason);
});
}
private parseState(state: string): [string, string] {
let nonce = state;
let userState = '';
if (state) {
const idx = state.indexOf(this.config.nonceStateSeparator);
if (idx > -1) {
nonce = state.substr(0, idx);
userState = state.substr(idx + this.config.nonceStateSeparator.length);
}
}
return [nonce, userState];
}
protected validateNonce(nonceInState: string): boolean {
let savedNonce;
if (
this.saveNoncesInLocalStorage &&
typeof window['localStorage'] !== 'undefined'
) {
savedNonce = localStorage.getItem('nonce');
} else {
savedNonce = this._storage.getItem('nonce');
}
if (savedNonce !== nonceInState) {
const err = 'Validating access_token failed, wrong state/nonce.';
console.error(err, savedNonce, nonceInState);
return false;
}
return true;
}
protected storeIdToken(idToken: ParsedIdToken): void {
this._storage.setItem('id_token', idToken.idToken);
this._storage.setItem('id_token_claims_obj', idToken.idTokenClaimsJson);
this._storage.setItem('id_token_expires_at', '' + idToken.idTokenExpiresAt);
this._storage.setItem(
'id_token_stored_at',
'' + this.dateTimeService.now()
);
}
protected storeSessionState(sessionState: string): void {
this._storage.setItem('session_state', sessionState);
}
protected getSessionState(): string {
return this._storage.getItem('session_state');
}
protected handleLoginError(options: LoginOptions, parts: object): void {
if (options.onLoginError) {
options.onLoginError(parts);
}
if (this.clearHashAfterLogin && !options.preventClearHashAfterLogin) {
this.clearLocationHash();
}
}
private getClockSkewInMsec(defaultSkewMsc = 600_000) {
if (!this.clockSkewInSec && this.clockSkewInSec !== 0) {
return defaultSkewMsc;
}
return this.clockSkewInSec * 1000;
}
/**
* @ignore
*/
public processIdToken(
idToken: string,
accessToken: string,
skipNonceCheck = false
): Promise<ParsedIdToken> {
const tokenParts = idToken.split('.');
const headerBase64 = this.padBase64(tokenParts[0]);
const headerJson = b64DecodeUnicode(headerBase64);
const header = JSON.parse(headerJson);
const claimsBase64 = this.padBase64(tokenParts[1]);
const claimsJson = b64DecodeUnicode(claimsBase64);
const claims = JSON.parse(claimsJson);
let savedNonce;
if (
this.saveNoncesInLocalStorage &&
typeof window['localStorage'] !== 'undefined'
) {
savedNonce = localStorage.getItem('nonce');
} else {
savedNonce = this._storage.getItem('nonce');
}
if (Array.isArray(claims.aud)) {
if (claims.aud.every((v) => v !== this.clientId)) {
const err = 'Wrong audience: ' + claims.aud.join(',');
this.logger.warn(err);
return Promise.reject(err);
}
} else {
if (claims.aud !== this.clientId) {
const err = 'Wrong audience: ' + claims.aud;
this.logger.warn(err);
return Promise.reject(err);
}
}
if (!claims.sub) {
const err = 'No sub claim in id_token';
this.logger.warn(err);
return Promise.reject(err);
}
/* 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.
*/
if (
this.sessionChecksEnabled &&
this.silentRefreshSubject &&
this.silentRefreshSubject !== claims['sub']
) {
const err =
'After refreshing, we got an id_token for another user (sub). ' +
`Expected sub: ${this.silentRefreshSubject}, received sub: ${claims['sub']}`;
this.logger.warn(err);
return Promise.reject(err);
}
if (!claims.iat) {
const err = 'No iat claim in id_token';
this.logger.warn(err);
return Promise.reject(err);
}
if (!this.skipIssuerCheck && claims.iss !== this.issuer) {
const err = 'Wrong issuer: ' + claims.iss;
this.logger.warn(err);
return Promise.reject(err);
}
if (!skipNonceCheck && claims.nonce !== savedNonce) {
const err = 'Wrong nonce: ' + claims.nonce;
this.logger.warn(err);
return Promise.reject(err);
}
// at_hash is not applicable to authorization code flow
// addressing https://github.com/manfredsteyer/angular-oauth2-oidc/issues/661
// i.e. Based on spec the at_hash check is only true for implicit code flow on Ping Federate
// https://www.pingidentity.com/developer/en/resources/openid-connect-developers-guide.html
if (
Object.prototype.hasOwnProperty.call(this, 'responseType') &&
(this.responseType === 'code' || this.responseType === 'id_token')
) {
this.disableAtHashCheck = true;
}
if (
!this.disableAtHashCheck &&
this.requestAccessToken &&
!claims['at_hash']
) {
const err = 'An at_hash is needed!';
this.logger.warn(err);
return Promise.reject(err);
}
const now = this.dateTimeService.now();
const issuedAtMSec = claims.iat * 1000;
const expiresAtMSec = claims.exp * 1000;
const clockSkewInMSec = this.getClockSkewInMsec(); // (this.getClockSkewInMsec() || 600) * 1000;
if (
issuedAtMSec - clockSkewInMSec >= now ||
expiresAtMSec + clockSkewInMSec - this.decreaseExpirationBySec <= now
) {
const err = 'Token has expired';
console.error(err);
console.error({
now: now,
issuedAtMSec: issuedAtMSec,
expiresAtMSec: expiresAtMSec,
});
return Promise.reject(err);
}
const validationParams: ValidationParams = {
accessToken: accessToken,
idToken: idToken,
jwks: this.jwks,
idTokenClaims: claims,
idTokenHeader: header,
loadKeys: () => this.loadJwks(),
};
if (this.disableAtHashCheck) {
return this.checkSignature(validationParams).then(() => {
const result: ParsedIdToken = {
idToken: idToken,
idTokenClaims: claims,
idTokenClaimsJson: claimsJson,
idTokenHeader: header,
idTokenHeaderJson: headerJson,
idTokenExpiresAt: expiresAtMSec,
};
return result;
});
}
return this.checkAtHash(validationParams).then((atHashValid) => {
if (!this.disableAtHashCheck && this.requestAccessToken && !atHashValid) {
const err = 'Wrong at_hash';
this.logger.warn(err);
return Promise.reject(err);
}
return this.checkSignature(validationParams).then(() => {
const atHashCheckEnabled = !this.disableAtHashCheck;
const result: ParsedIdToken = {
idToken: idToken,
idTokenClaims: claims,
idTokenClaimsJson: claimsJson,
idTokenHeader: header,
idTokenHeaderJson: headerJson,
idTokenExpiresAt: expiresAtMSec,
};
if (atHashCheckEnabled) {
return this.checkAtHash(validationParams).then((atHashValid) => {
if (this.requestAccessToken && !atHashValid) {
const err = 'Wrong at_hash';
this.logger.warn(err);
return Promise.reject(err);
} else {
return result;
}
});
} else {
return result;
}
});
});
}
/**
* Returns the received claims about the user.
*/
public getIdentityClaims(): Record<string, any> {
const claims = this._storage.getItem('id_token_claims_obj');
if (!claims) {
return null;
}
return JSON.parse(claims);
}
/**
* Returns the granted scopes from the server.
*/
public getGrantedScopes(): object {
const scopes = this._storage.getItem('granted_scopes');
if (!scopes) {
return null;
}
return JSON.parse(scopes);
}
/**
* Returns the current id_token.
*/
public getIdToken(): string {
return this._storage ? this._storage.getItem('id_token') : null;
}
protected padBase64(base64data): string {
while (base64data.length % 4 !== 0) {
base64data += '=';
}
return base64data;
}
/**
* Returns the current access_token.
*/
public getAccessToken(): string {
return this._storage ? this._storage.getItem('access_token') : null;
}
public getRefreshToken(): string {
return this._storage ? this._storage.getItem('refresh_token') : null;
}
/**
* Returns the expiration date of the access_token
* as milliseconds since 1970.
*/
public getAccessTokenExpiration(): number {
if (!this._storage.getItem('expires_at')) {
return null;
}
return parseInt(this._storage.getItem('expires_at'), 10);
}
protected getAccessTokenStoredAt(): number {
return parseInt(this._storage.getItem('access_token_stored_at'), 10);
}
protected getIdTokenStoredAt(): number {
return parseInt(this._storage.getItem('id_token_stored_at'), 10);
}
/**
* Returns the expiration date of the id_token
* as milliseconds since 1970.
*/
public getIdTokenExpiration(): number {
if (!this._storage.getItem('id_token_expires_at')) {
return null;
}
return parseInt(this._storage.getItem('id_token_expires_at'), 10);
}
/**
* Checkes, whether there is a valid access_token.
*/
public hasValidAccessToken(): boolean {
if (this.getAccessToken()) {
const expiresAt = this._storage.getItem('expires_at');
const now = this.dateTimeService.new();
if (
expiresAt &&
parseInt(expiresAt, 10) - this.decreaseExpirationBySec <
now.getTime() - this.getClockSkewInMsec()
) {
return false;
}
return true;
}
return false;
}
/**
* Checks whether there is a valid id_token.
*/
public hasValidIdToken(): boolean {
if (this.getIdToken()) {
const expiresAt = this._storage.getItem('id_token_expires_at');
const now = this.dateTimeService.new();
if (
expiresAt &&
parseInt(expiresAt, 10) - this.decreaseExpirationBySec <
now.getTime() - this.getClockSkewInMsec()
) {
return false;
}
return true;
}
return false;
}
/**
* Retrieve a saved custom property of the TokenReponse object. Only if predefined in authconfig.
*/
public getCustomTokenResponseProperty(requestedProperty: string): any {
return this._storage &&
this.config.customTokenParameters &&
this.config.customTokenParameters.indexOf(requestedProperty) >= 0 &&
this._storage.getItem(requestedProperty) !== null
? JSON.parse(this._storage.getItem(requestedProperty))
: null;
}
/**
* Returns the auth-header that can be used
* to transmit the access_token to a service
*/
public authorizationHeader(): string {
return 'Bearer ' + this.getAccessToken();
}
/**
* Removes all tokens and logs the user out.
* If a logout url is configured, the user is
* redirected to it with optional state parameter.
* @param noRedirectToLogoutUrl
* @param state
*/
public logOut(): void;
public logOut(customParameters: boolean | object): void;
public logOut(noRedirectToLogoutUrl: boolean): void;
public logOut(noRedirectToLogoutUrl: boolean, state: string): void;
public logOut(customParameters: boolean | object = {}, state = ''): void {
let noRedirectToLogoutUrl = false;
if (typeof customParameters === 'boolean') {
noRedirectToLogoutUrl = customParameters;
customParameters = {};
}
const id_token = this.getIdToken();
this._storage.removeItem('access_token');
this._storage.removeItem('id_token');
this._storage.removeItem('refresh_token');
if (this.saveNoncesInLocalStorage) {
localStorage.removeItem('nonce');
localStorage.removeItem('PKCE_verifier');
} else {
this._storage.removeItem('nonce');
this._storage.removeItem('PKCE_verifier');
}
this._storage.removeItem('expires_at');
this._storage.removeItem('id_token_claims_obj');
this._storage.removeItem('id_token_expires_at');
this._storage.removeItem('id_token_stored_at');
this._storage.removeItem('access_token_stored_at');
this._storage.removeItem('granted_scopes');
this._storage.removeItem('session_state');
if (this.config.customTokenParameters) {
this.config.customTokenParameters.forEach((customParam) =>
this._storage.removeItem(customParam)
);
}
this.silentRefreshSubject = null;
this.eventsSubject.next(new OAuthInfoEvent('logout'));
if (!this.logoutUrl) {
return;
}
if (noRedirectToLogoutUrl) {
return;
}
// if (!id_token && !this.postLogoutRedirectUri) {
// return;
// }
let logoutUrl: string;
if (!this.validateUrlForHttps(this.logoutUrl)) {
throw new Error(
"logoutUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)."
);
}
// For backward compatibility
if (this.logoutUrl.indexOf('{{') > -1) {
logoutUrl = this.logoutUrl
.replace(/\{\{id_token\}\}/, encodeURIComponent(id_token))
.replace(/\{\{client_id\}\}/, encodeURIComponent(this.clientId));
} else {
let params = new HttpParams({ encoder: new WebHttpUrlEncodingCodec() });
if (id_token) {
params = params.set('id_token_hint', id_token);
}
const postLogoutUrl =
this.postLogoutRedirectUri ||
(this.redirectUriAsPostLogoutRedirectUriFallback && this.redirectUri) ||
'';
if (postLogoutUrl) {
params = params.set('post_logout_redirect_uri', postLogoutUrl);
if (state) {
params = params.set('state', state);
}
}
for (const key in customParameters) {
params = params.set(key, customParameters[key]);
}
logoutUrl =
this.logoutUrl +
(this.logoutUrl.indexOf('?') > -1 ? '&' : '?') +
params.toString();
}
this.config.openUri(logoutUrl);
}
/**
* @ignore
*/
public createAndSaveNonce(): Promise<string> {
const that = this; // eslint-disable-line @typescript-eslint/no-this-alias
return this.createNonce().then(function (nonce: any) {
// Use localStorage for nonce if possible
// localStorage is the only storage who survives a
// redirect in ALL browsers (also IE)
// Otherwiese we'd force teams who have to support
// IE into using localStorage for everything
if (
that.saveNoncesInLocalStorage &&
typeof window['localStorage'] !== 'undefined'
) {
localStorage.setItem('nonce', nonce);
} else {
that._storage.setItem('nonce', nonce);
}
return nonce;
});
}
/**
* @ignore
*/
public ngOnDestroy(): void {
this.clearAccessTokenTimer();
this.clearIdTokenTimer();
this.removeSilentRefreshEventListener();
const silentRefreshFrame = this.document.getElementById(
this.silentRefreshIFrameName
);
if (silentRefreshFrame) {
silentRefreshFrame.remove();
}
this.stopSessionCheckTimer();
this.removeSessionCheckEventListener();
const sessionCheckFrame = this.document.getElementById(
this.sessionCheckIFrameName
);
if (sessionCheckFrame) {
sessionCheckFrame.remove();
}
}
protected createNonce(): Promise<string> {
return new Promise((resolve) => {
if (this.rngUrl) {
throw new Error(
'createNonce with rng-web-api has not been implemented so far'
);
}
/*
* This alphabet is from:
* https://tools.ietf.org/html/rfc7636#section-4.1
*
* [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
*/
const unreserved =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
let size = 45;
let id = '';
const crypto =
typeof self === 'undefined' ? null : self.crypto || self['msCrypto'];
if (crypto) {
let bytes = new Uint8Array(size);
crypto.getRandomValues(bytes);
// Needed for IE
if (!bytes.map) {
(bytes as any).map = Array.prototype.map;
}
bytes = bytes.map((x) => unreserved.charCodeAt(x % unreserved.length));
id = String.fromCharCode.apply(null, bytes);
} else {
while (0 < size--) {
id += unreserved[(Math.random() * unreserved.length) | 0];
}
}
resolve(base64UrlEncode(id));
});
}
protected async checkAtHash(params: ValidationParams): Promise<boolean> {
if (!this.tokenValidationHandler) {
this.logger.warn(
'No tokenValidationHandler configured. Cannot check at_hash.'
);
return true;
}
return this.tokenValidationHandler.validateAtHash(params);
}
protected checkSignature(params: ValidationParams): Promise<any> {
if (!this.tokenValidationHandler) {
this.logger.warn(
'No tokenValidationHandler configured. Cannot check signature.'
);
return Promise.resolve(null);
}
return this.tokenValidationHandler.validateSignature(params);
}
/**
* Start the implicit flow or the code flow,
* depending on your configuration.
*/
public initLoginFlow(additionalState = '', params = {}): void {
if (this.responseType === 'code') {
return this.initCodeFlow(additionalState, params);
} else {
return this.initImplicitFlow(additionalState, params);
}
}
/**
* Starts the authorization code flow and redirects to user to
* the auth servers login url.
*/
public initCodeFlow(additionalState = '', params = {}): void {
if (this.loginUrl !== '') {
this.initCodeFlowInternal(additionalState, params);
} else {
this.events
.pipe(filter((e) => e.type === 'discovery_document_loaded'))
.subscribe(() => this.initCodeFlowInternal(additionalState, params));
}
}
private initCodeFlowInternal(additionalState = '', params = {}): void {
if (!this.validateUrlForHttps(this.loginUrl)) {
throw new Error(
"loginUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)."
);
}
let addParams = {};
let loginHint = null;
if (typeof params === 'string') {
loginHint = params;
} else if (typeof params === 'object') {
addParams = params;
}
this.createLoginUrl(additionalState, loginHint, null, false, addParams)
.then(this.config.openUri)
.catch((error) => {
console.error('Error in initAuthorizationCodeFlow');
console.error(error);
});
}
protected async createChallangeVerifierPairForPKCE(): Promise<
[string, string]
> {
if (!this.crypto) {
throw new Error(
'PKCE support for code flow needs a CryptoHander. Did you import the OAuthModule using forRoot() ?'
);
}
const verifier = await this.createNonce();
const challengeRaw = await this.crypto.calcHash(verifier, 'sha-256');
const challenge = base64UrlEncode(challengeRaw);
return [challenge, verifier];
}
private extractRecognizedCustomParameters(
tokenResponse: TokenResponse
): Map<string, string> {
const foundParameters: Map<string, string> = new Map<string, string>();
if (!this.config.customTokenParameters) {
return foundParameters;
}
this.config.customTokenParameters.forEach((recognizedParameter: string) => {
if (tokenResponse[recognizedParameter]) {
foundParameters.set(
recognizedParameter,
JSON.stringify(tokenResponse[recognizedParameter])
);
}
});
return foundParameters;
}
/**
* Revokes the auth token to secure the vulnarability
* of the token issued allowing the authorization server to clean
* up any security credentials associated with the authorization
*/
public revokeTokenAndLogout(
customParameters: boolean | object = {},
ignoreCorsIssues = false
): Promise<any> {
const revokeEndpoint = this.revocationEndpoint;
const accessToken = this.getAccessToken();
const refreshToken = this.getRefreshToken();
if (!accessToken) {
return Promise.resolve();
}
let params = new HttpParams({ encoder: new WebHttpUrlEncodingCodec() });
let headers = new HttpHeaders().set(
'Content-Type',
'application/x-www-form-urlencoded'
);
if (this.useHttpBasicAuth) {
const header = btoa(`${this.clientId}:${this.dummyClientSecret}`);
headers = headers.set('Authorization', 'Basic ' + header);
}
if (!this.useHttpBasicAuth) {
params = params.set('client_id', this.clientId);
}
if (!this.useHttpBasicAuth && this.dummyClientSecret) {
params = params.set('client_secret', this.dummyClientSecret);
}
if (this.customQueryParams) {
for (const key of Object.getOwnPropertyNames(this.customQueryParams)) {
params = params.set(key, this.customQueryParams[key]);
}
}
return new Promise((resolve, reject) => {
let revokeAccessToken: Observable<void>;
let revokeRefreshToken: Observable<void>;
if (accessToken) {
const revokationParams = params
.set('token', accessToken)
.set('token_type_hint', 'access_token');
revokeAccessToken = this.http.post<void>(
revokeEndpoint,
revokationParams,
{ headers }
);
} else {
revokeAccessToken = of(null);
}
if (refreshToken) {
const revokationParams = params
.set('token', refreshToken)
.set('token_type_hint', 'refresh_token');
revokeRefreshToken = this.http.post<void>(
revokeEndpoint,
revokationParams,
{ headers }
);
} else {
revokeRefreshToken = of(null);
}
if (ignoreCorsIssues) {
revokeAccessToken = revokeAccessToken.pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 0) {
return of<void>(null);
}
return throwError(err);
})
);
revokeRefreshToken = revokeRefreshToken.pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 0) {
return of<void>(null);
}
return throwError(err);
})
);
}
combineLatest([revokeAccessToken, revokeRefreshToken]).subscribe(
(res) => {
this.logOut(customParameters);
resolve(res);
this.logger.info('Token successfully revoked');
},
(err) => {
this.logger.error('Error revoking token', err);
this.eventsSubject.next(
new OAuthErrorEvent('token_revoke_error', err)
);
reject(err);
}
);
});
}
/**
* Clear location.hash if it's present
*/
private clearLocationHash() {
// Checking for empty hash is necessary for Firefox
// as setting an empty hash to an empty string adds # to the URL
if (location.hash != '') {
location.hash = '';
}
}
}