okta / okta-auth-js

The official js wrapper around Okta's auth API
Other
450 stars 265 forks source link

Chrome: auto renewal in iframe not working (JSESSIONID needs samesite=none) #461

Open joao-fonseca-inmarsat opened 4 years ago

joao-fonseca-inmarsat commented 4 years ago

I'm using okta-vue to build a SPA, and using an invisible iframe embedded in the page to try renewing the token without any redirects visible to the user. Basically, the iframe periodically calls getAccessToken() to ensure the auto renewal kicks-in, even if the user is not at his desk.

This works in Firefox, but in Chrome get "error.errorCode: login_required, error.description: The client specified not to prompt, but the user is not logged in." and "Session has expired or was closed outside the application.".

Upon investigation, I think this is what's happening:

  1. getAccessToken() is called and the auto renewal kick-in
  2. The iframe redirects to the Okta server
  3. That web page tries to access the session (using the JSESSIONID cookie)
  4. Chrome prevents the cookies from being read because samesite was not set to none

The Okta web server needs to set the samesite=none, secure=true on the session cookies. I can see secure=true, but samesite is empty.

Is there a way to enable this on the server? Any work-arounds?

shuowu commented 4 years ago

@joao-fonseca-inmarsat okta-auth-js, the upstream SDK of okta-vue, has already implemented hidden iframe logic to renew tokens in the background. The getAccessToken from okta-vue only returns the token from storage. I think the error you saw was thrown from auth-js, which means get a new token failed because of current session has expired.

Can you try the tokenManager.renew and isAuthenticated options from https://github.com/okta/okta-vue#auth to see if it still introduce errors?

Also, there is a thread tracks the discussion of the error you saw in app, please check https://github.com/okta/okta-oidc-js/issues/460

chrismllr commented 4 years ago

Ah, I just got linked here from https://github.com/okta/okta-oidc-js/issues/460. I am currently using okta-auth-js solo, this may be the error I'm running into. Seeing this in Safari though.

I am able to see 1-2 successful renewals while sitting on the page -- this usually only happens when I go back to the page after having closed the tab for a couple hours.

joao-fonseca-inmarsat commented 4 years ago

I think the error you saw was thrown from auth-js, which means get a new token failed because of current session has expired.

The session is not expired. If I access the Okta dev-*.okta.com of my account on a separate tab, I can see the session is still there. When I list the cookies, I can see that JSESSIONID doesn't have "samesite=none", which Chrome requires when accessed from an iframe.

On my page, when the library tries to renew, I see that the cookies for dev-*.okta.com are empty - Chrome is just not allowing the session cookies to be loaded from the iframe.

Everything works from Firefox, because it doesn't enforce the "samesite=none" policy.

The fix is very simple - the dev-*.okta.com servers must set the "samesite=none" attribute of the JSESSIONID cookie.

shuowu commented 4 years ago

@joao-fonseca-inmarsat You can customize browser cookies setting with https://github.com/okta/okta-auth-js#cookies

joao-fonseca-inmarsat commented 4 years ago

@joao-fonseca-inmarsat You can customize browser cookies setting with https://github.com/okta/okta-auth-js#cookies

I tried that and it didn't work. I think those settings define how the library stores its own cookies (if any) in the web page.

The cookies that are the source of this problem are the JSESSIONID and other cookies that the Okta SSO server at "dev-*.okta.com" returns when the user logs in. It's that server that needs to set "samesite=none" on the cookies. I didn't find any option to configure this on the dashboard.

shuowu commented 4 years ago

@joao-fonseca-inmarsat I am not aware of any auto renewal issue in chrome (with default cookies setting). Can you check your browser settings to see if it's blocking third party cookies or not?

joao-fonseca-inmarsat commented 4 years ago

@joao-fonseca-inmarsat I am not aware of any auto renewal issue in chrome (with default cookies setting). Can you check your browser settings to see if it's blocking third party cookies or not?

I'm using the default cookie settings from Chrome.

Note that this bug manifests itself from within an iframe; it's very simple to reproduce:

-Create a web page that requires a login from the user -Configure okta-auth with expireEarlySeconds to a large value, so that you don't need to wait hours for a refresh -In the main web page, add an iframe containing a second page -The second page runs a script that periodically calls getAccessToken() every 30 seconds (you can log it to the console) -When okta-auth decides to refresh the token, you will get the error message

shuowu commented 4 years ago

@joao-fonseca-inmarsat Then it's back to my first comment (https://github.com/okta/okta-auth-js/issues/461#issuecomment-685828554). Wondering why you want to wrap the getAcceesToken in an iframe. In auth-js 3.x, getAccessToken triggers a renew process (happen in iframe). In auth-js 4.x, getAccessToken is only getting token from the storage. By either way, I don't think you need to wrap the process in an iframe in your app.

If you are using okta-vue, I think you most probably are still using auth-js@3.x

joao-fonseca-inmarsat commented 4 years ago

@joao-fonseca-inmarsat By either way, I don't think you need to wrap the process in an iframe in your app.

If you are using okta-vue, I think you most probably are still using auth-js@3.x

Yes, I'm using okta-vue 2.1.0/okta-auth-js 3.2.3.

I removed the iframe, and got the same result. Here's an abridged version of my code:


const routes = [

  {
    path: '(redirect uri)',
    component: Auth.handleCallback()
  },

  {
    path: '/',
    component: () => import('pages/Page.vue'),
    meta: {
      requiresAuth: true
    }
  },

  // Always leave this as last one,
  // but you can also remove it
  {
    path: '*',
    component: () => import('pages/Error404.vue')
  }
]

Vue.use(VueRouter)

const oktaConfig = {
  issuer: '(my issuer)',
  clientId: '(my client id)',
  redirectUri: '(redirect URI)',
  scope: [scopes]
  pkce: true,
  tokenManager: {
    autoRenew: true,
    expireEarlySeconds: 3420
  }
}

Vue.use(Auth, { ...oktaConfig })

<!-- The page -->

<template>
   <p>Hello</p>
</template>

<script>
export default {
  data () {
    return {}
  },
  created () {
    var me = this
    setInterval(function () {
      if (me.$auth) {
        me.$auth.getAccessToken().then(function (accessToken) {
          console.debug('Fetched access token', accessToken)
        })
      }
    },  10000)
  }
joao-fonseca-inmarsat commented 4 years ago

With the above code, I sometimes get an "Invalid redirect URI" response. I think during the initial login, the redirect gets back with some URI parameters (e.g. "implicit/callback?code=xxxx"), which somehow is stored by okta-auth. If I don't do anything, the auto renewal sends the redirect uri as "implicit/callback?code=xxxx" and the server complains about it.

To work around the above, after login I reload the page (clicking enter in the address bar); this resets the information kept by okta-auth, and the auto renewal sends the correct redirect URI.

But, then I get the "The client specified not to prompt, but the user is not logged in." problem.

shuowu commented 4 years ago

@joao-fonseca-inmarsat The error, The client specified not to prompt, but the user is not logged in, only mean your app's session is expired when attempt to renew tokens, and the users need to re-login to get a new valid session.

Per the code, looks like the timer you add only triggers the get token process (renew process included), but didn't handle the failure case (the error you see). I would suggest you check the sample code, which is doing the passive auto renew process without an active timer in the app. https://github.com/okta/samples-js-vue/blob/master/okta-hosted-login/src/App.vue#L84

joao-fonseca-inmarsat commented 4 years ago

@joao-fonseca-inmarsat The error, The client specified not to prompt, but the user is not logged in, only mean your app's session is expired when attempt to renew tokens, and the users need to re-login to get a new valid session.

Per the code, looks like the timer you add only triggers the get token process (renew process included), but didn't handle the failure case (the error you see). I would suggest you check the sample code, which is doing the passive auto renew process without an active timer in the app. https://github.com/okta/samples-js-vue/blob/master/okta-hosted-login/src/App.vue#L84

As I said in a previous post, the app's session is not expired. I can confirm that by accessing the dev-*.okta.com web pages on a different tab. Also, I had only logged in 3 minutes before (I'm using expireEarlySeconds=3420 to speed up the renewal process).

Again: the session expired error is caused by Chrome and the iframe used by okta-auth; Chrome requires cross-domain cookies (in this case dev-*.okta.com) to be set with "samesite=none" and they are not. When okta-auth tries to read the JSESSIONID cookie, it doesn't see it and thinks there's no session. But there is (and I've said it many times now), it's Chrome who is preventing the cookie from being read.

The example app you mention is not what I want. Before rendering each page, it verifies if the user is authenticated; if it's not, it will redirect to the Okta server. The user will perceive this as a blank screen for a few moments. Also, I'm on a SPA - the user may work for extended periods of time on the same page, which will be doing a lot of back-end requests. I don't want the web page to suddenly become blank while there's a token renewal in progress.