-
Notifications
You must be signed in to change notification settings - Fork 232
fix[react]: onSessionExpired default behaviour #848
fix[react]: onSessionExpired default behaviour #848
Conversation
packages/okta-react/README.md
Outdated
@@ -314,7 +314,7 @@ These options are used by `Security` to configure the [Auth service][]. The most | |||
- **onAuthRequired** *(optional)* - callback function. Called when authentication is required. If this is not supplied, `okta-react` redirects to Okta. This callback will receive `authService` as the first function parameter. This is triggered when: | |||
1. [login](#authserviceloginfromuri-additionalparams) is called | |||
2. A `SecureRoute` is accessed without authentication | |||
- **onSessionExpired** *(optional)* - callback function. Called when the Okta SSO session has expired or was ended outside of the application. This SDK adds a default handler which will call [login](#authserviceloginfromuri-additionalparams) to initiate a login flow. Passing a function here will disable the default handler. | |||
- **onSessionExpired** *(optional)* - callback function. Called when the Okta SSO session has expired or was ended outside of the application. This SDK adds a default handler which will call [clearAuthState](##authserviceclearauthstate) to clear authState in context. Passing a function here will disable the default handler. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we had decided that the default handler would do nothing, and we'd separately listen to the expired event to call clearAuthState.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this change should make sense: 7311f47
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the effect here is that if the accessToken cannot be renewed, the idToken will also be removed. This might be the right logical choice, but there could be a timing error if the idToken is checked first. If we want to invalidate all tokens when accessToken renew fails, we would need to make sure to always try to retrieve accessToken before idToken. This is kind of subtle and error prone. Maybe a better choice is to make default isAuthenticated
logic only true if BOTH idToken and accessToken are present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That change would definitely be a major version change - discussing that change is what led to the idea of removing isAuthenticated entirely
I also don't think that renewal failure of the accessToken in any way invalidates the idToken. That requires expiration or revocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/okta/okta-auth-js/blob/master/packages/okta-auth-js/lib/browser/browser.js#L184
Looks like based on current auth-js
implementation, onSessionExired
callback will only be triggered on accessToken renew failure, then it would make sense to just clear accessToken
in context.
But how should we handle idToken
renew failure and isAuthenticated
state. I think the purpose for this fix is to update the whole authState
to keep user out of SecureRoute
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No - the purpose is to have authState be accurate. If only accessToken is invalid, then authState is updated. (updateAuthState instead of clearAuthState).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it might will cause infinite loop. error -> updateAuthState -> error -> ...
might need some thoughts on when onSessionExpired be triggered first, imo.
Should also include fixes from okta/okta-auth-js#440 to avoid loop from signOut. |
const fromUri = history.createHref(history.location); | ||
authService.login(fromUri); | ||
} | ||
useEffect(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this use of useEffect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than my question about potential infinite loops from dev code, lgtm
} | ||
useEffect(() => { | ||
if(!authState.isAuthenticated && !authState.isPending) { | ||
authService.login(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this where it is possible to begin concurrent login attempts? If the authState is updated (due to token changes, etc). Do we need to add aa concurrent check on this or place this call into the PromiseQ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Probably the isPending
state is not enough here as it's triggered by emitting an event, which may still run into concurrent flow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved by providing pending state in the login method to make sure it only be called once.
packages/okta-react/README.md
Outdated
@@ -314,7 +314,8 @@ These options are used by `Security` to configure the [Auth service][]. The most | |||
- **onAuthRequired** *(optional)* - callback function. Called when authentication is required. If this is not supplied, `okta-react` redirects to Okta. This callback will receive `authService` as the first function parameter. This is triggered when: | |||
1. [login](#authserviceloginfromuri-additionalparams) is called | |||
2. A `SecureRoute` is accessed without authentication | |||
- **onSessionExpired** *(optional)* - callback function. Called when the Okta SSO session has expired or was ended outside of the application. This SDK adds a default handler which will call [login](#authserviceloginfromuri-additionalparams) to initiate a login flow. Passing a function here will disable the default handler. | |||
- **onSessionExpired** *(optional)* - callback function. Called when the Okta SSO session has expired or was ended outside of the application. This SDK provides an empty function as the default behaviour. Passing a function here will disable the default handler. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this marked as deprecated in authJS 3.2.2. I think we can remove it from the documentation here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still kept for this patch fix, marked as deprecated.
packages/okta-react/CHANGELOG.md
Outdated
|
||
### Bug Fixes | ||
|
||
- [#848](https://github.com/okta/okta-oidc-js/pull/848) Change default `onSessionExpired` behaviour to `authService.clearAuthState` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removes onSessionExpired
behavior
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comments.
- major version bump
- remove onSessionExpired
- change logic on isAuthenticated to return false if either token is missing
- double check for concurrency errors with useEffect -> login()
@@ -1,3 +1,9 @@ | |||
# 3.0.4 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I (again) thought we had decided on a major version bump (see comments from Aaron) - what am I missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we decide to make remove default onSessionExpired
as a patch fix to avoid noise. Then put the isAuthenticated
behavior change into 4.0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. We agreed that removing onSessionExpired
is a fix not a new feature or breaking change. It was never working as intended, and has mostly negative side effects. The positive side effect was triggering a login if only the accessToken is missing, but this can be done better by the user providing an isAuthenticated
function which asserts accessToken && idToken
. We will change this to be default logic in the next version but it would likely be considered a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comments.
- major version bump
- remove onSessionExpired
- change logic on isAuthenticated to return false if either token is missing
- double check for concurrency errors with useEffect -> login()
As we discussed we will push breaking change with isAuthenticated
to subsequent release that includes [email protected]
.
I think we can update the README to provide better guidance for users of this version. (see comments).
packages/okta-react/README.md
Outdated
- **onSessionExpired** *(optional)* - callback function. Called when the Okta SSO session has expired or was ended outside of the application. This SDK adds a default handler which will call [login](#authserviceloginfromuri-additionalparams) to initiate a login flow. Passing a function here will disable the default handler. | ||
> :warning: DO NOT trigger `authService.login()` in this callback. This callback is used inside the `login` method, call it again will trigger the protection logic to end the function. | ||
- **onSessionExpired (deprecated)** *(optional)* - callback function. Called when the Okta SSO session has expired or was ended outside of the application. This SDK provides an empty function as the default behaviour. Passing a function here will disable the default handler. | ||
> :warning: DO NOT trigger token renew process, like `tokenManager.get()` or `tokenManager.renew()`, in this callback as it may end up with infinite loop. | ||
- **isAuthenticated** *(optional)* - callback function. By default, `authService` will consider a user authenticated if both `getIdToken()` and `getAccessToken()` return a value. Setting a `isAuthenticated` function on the config will skip the default logic and call the supplied function instead. The function should return a Promise and resolve to either true or false. Note that this is only evaluated when the `auth` code has reason to think the authentication state has changed. You can call the `authService.updateAuthState()` method to trigger a re-evaluation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this text correct? It says "if both" return a value. But I think the current logic is "if either" return a value. Let's double check this and also indicate that we plan on changing this default logic in the next version. And we recommend the user to provide a function here and assert both tokens are present, unless their application can function with only one token present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've marked this as deprecated, but don't say anything about what they should be doing instead.
@@ -196,12 +190,24 @@ class AuthService { | |||
} | |||
|
|||
async login(fromUri, additionalParams) { | |||
if(this._pending.handleLogin) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice. thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the closer the easier to manage, hah
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If login should only be run once, we probably can move the check to auth-js v5.0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The underlying token.getWithRedirect
will be placed in the PromiseQueue, but there is additional logic here. One possible error would be calling login(myUri)
and then calling login
again with no parameters. This would overwrite the saved URI and affect the final redirect value.
Another option instead of silently ignoring a concurrent login
attempt is to actually error out. Maybe we could do this in the next release? What I'd like is a way to compare authState with previous authState so we only trigger login if we are transitioning isAuthenticated
from pending -> false or true -> false. A transition of false -> false should not start a login flow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we are transitioning isAuthenticated from pending -> false or true -> false
Interesting. I think that level of check is better done in auth-js, but either way it does look like a nice approach to guide developers away from pitfalls that can otherwise mostly work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
login
might not only be triggered by isAuthenticated
state change, users may also call it from callbacks which is out of our control. I think it still makes sense to protect it generally from concurrency cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please double check description of isAuthenticated
in the README and let us also provide some guidance to verify both tokens. This will cover any side effects of removing onSessionExpired
if (this._config.onAuthRequired) { | ||
return await this._config.onAuthRequired(this); | ||
} | ||
return await this.redirect(additionalParams); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Errors are just silently lost - is there anything there we'd want to inform the user of?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
only finally been added here, but without catch, I think users still can catch errors if any happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scrunches face up in confusion
writes test code
blinks furiously
writes more test code
googles
Huh. This is not how I expected finally and errors to interact. I sit corrected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test added.
@@ -314,7 +314,9 @@ These options are used by `Security` to configure the [Auth service][]. The most | |||
- **onAuthRequired** *(optional)* - callback function. Called when authentication is required. If this is not supplied, `okta-react` redirects to Okta. This callback will receive `authService` as the first function parameter. This is triggered when: | |||
1. [login](#authserviceloginfromuri-additionalparams) is called | |||
2. A `SecureRoute` is accessed without authentication | |||
- **onSessionExpired** *(optional)* - callback function. Called when the Okta SSO session has expired or was ended outside of the application. This SDK adds a default handler which will call [login](#authserviceloginfromuri-additionalparams) to initiate a login flow. Passing a function here will disable the default handler. | |||
> :warning: DO NOT trigger `authService.login()` in this callback. This callback is used inside the `login` method, call it again will trigger the protection logic to end the function. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These warnings are just the advice I wanted, thanks!
Updated doc, also provided code example. |
* changelog fix * prevent concurrent login calls by handling pending state in authService * fix error state when load states in loginRedirect state
1bf651c
to
72e82aa
Compare
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: OKTA-318290
What is the new behavior?
auth-js
dependency to 3.2.2onSessionExpired
callbackisPendng
authState when login happens to avoid concurrent login flowuseEffect
inSecureRoute
componentDoes this PR introduce a breaking change?
Other information
Tested SecureRoute Comp for both okta-hosted-signIn and custom-signIn
Reviewers