okta / okta-oidc-js

okta-oidc-js
https://github.com/okta/okta-oidc-js
Other
395 stars 232 forks source link

Store tokens in a shared domain cookie #909

Closed DavidLozzi closed 4 years ago

DavidLozzi commented 4 years ago

I'm submitting this issue for the package(s):

I'm submitting a:

Current behavior

We are widening our program and now including a few different web apps in our solution. We want to store the Okta token in the cookie, but for the domain, like .domain.com. Currently, the cookie gets stored in app.domain.com, and when we navigate to the next app, web.domain.com we lose the cookie. Is it possible to configure this to store and retrieve the tokens from .domain.com instead?

Expected behavior

Minimal reproduction of the problem with instructions

Extra information about the use case/user story you are trying to implement

Environment

swiftone commented 4 years ago

@DavidLozzi - Thanks for the report. This feels more like the scope of okta-auth-js (which is used by okta-react), but I'll leave this ticket here until we can dig a little deeper and make sure our suggestion works.

DavidLozzi commented 4 years ago

@swiftone do I need to do anything to get that team's attention?

shuowu commented 4 years ago

@DavidLozzi Store cross-domain cookies is not in our support scope right now. But you can always provide a custom storage provider with a custom implementation instead of an option string (like cookie).

For the implementation, you can follow the current cookie storage implementation with providing extra domain option.

DavidLozzi commented 4 years ago

That custom storage provider looks like exactly what we'd need! Thank you!

DavidLozzi commented 4 years ago

@shuowu A new problem has come up, it appears that when I change the storage option, I am now not getting the access token from Okta. If I remove this, I get it in localStorage, add it, I only get idToken in the cookie...

Went from

export default new AuthService({
  history,
  issuer: CONFIG.OKTA.ISSUER,
  pkce: false,
  clientId: CONFIG.OKTA.CLIENT_ID,
  redirectUri: `${window.location.origin}${CONFIG.OKTA.REDIRECT_URL}`,
  tokenManager: { expireEarlySeconds: (30 * 60) }
});

to

export default new AuthService({
  history,
  issuer: CONFIG.OKTA.ISSUER,
  pkce: false,
  clientId: CONFIG.OKTA.CLIENT_ID,
  redirectUri: `${window.location.origin}${CONFIG.OKTA.REDIRECT_URL}`,
  tokenManager: {
    expireEarlySeconds: (30 * 60),
    storage: {
      getItem: (key) => {
        try {
          console.log('get', key);
          console.log(Cookies.get(key));
          return Cookies.get(key);
        } catch (e) {
          console.error('OKTA getItem', e);
        }
      },
      setItem: (key, val) => {
        try {
          let options = { expires: 1 };
          console.log('set', key, val); // I do not see accessToken in the val
          Cookies.set(key, val, options);
        } catch (e) {
          console.error('OKTA setItem', e);
        }
      }
    }
  }
});
shuowu commented 4 years ago

@DavidLozzi Right, the custom storage provider is a replacement of the default implementation. From the issue, looks like you are storing tokens in cookies, if you want tokens be available in both cookies and localStorage, you can add store to localStorage logic in the custom storage provider.

DavidLozzi commented 4 years ago

Sorry for the confusion @shuowu . I don't want it in localStorage, just the cookie. When I remove my config, it uses localStorage. Also, when i try storage: 'cookie' it works as expected.

When I use a custom storage provider, it DOES save the idToken but DOES NOT save the accessToken, i don't even see accessToken being set.

shuowu commented 4 years ago

Cookie has it's size limit, it might because that the accessToken is too big to be stored in cookie. One thing you can try is to delete token.value before saving it into cookie. Also, here is the default cookie storage implementation https://github.com/okta/okta-auth-js/blob/master/lib/TokenManager.ts#L230

DavidLozzi commented 4 years ago

Thanks @shuowu , that makes sense, thanks for the code, I'll check it out

DavidLozzi commented 4 years ago

@shuowu sorry for the firing of posts. I don't get accessToken in the setItem function to be able to parse it out.

from above

setItem: (key, val) => {
        try {
          let options = { expires: 1 };
          console.log('set', key, val); // I do not see accessToken in the val
          Cookies.set(key, val, options);
        } catch (e) {

the val that is being provided to this function does not contain accessToken, so I can't split idToken and accessToken into separate cookies.

DavidLozzi commented 4 years ago

@shuowu I stand corrected, I added the code from your example and I do see okta-token-storage_accessToken as a cookie. I think I misunderstood how the storage provider works. Adding the get piece now but it's looking promising.

DavidLozzi commented 4 years ago

@shuowu Hope I'm not being too hasty, but it's working! Thank you! Closing for now.

DavidLozzi commented 4 years ago

@shuowu Sorry to reopen, but a very related issue. I am using the custom storage provider (code below) and it works great on localhost, with our 24hour token expiration. When we move the same to our Dev and QA environments, we get 2hour expiration. When we were using localStorage we were getting 24hr, but switching to custom storage provider gets us 2hrs, except for localhost, gets us 24hrs.

Thoughts on what might cause this? Doesn't seem like the storage provider should be impacting that.

  storage: {
    getItem: (key) => {
      try {
        const allCookies = Cookies.get();
        const value = {};

        Object.keys(allCookies).forEach((k) => {
          if (k.indexOf(key) === 0) {
            value[k.replace(`${key}_`, '')] = JSON.parse(allCookies[k]);
          }
        });
        // console.log('get', key);
        // console.log(value);
        return JSON.stringify(value);
      } catch (e) {
        console.error('OKTA tokenManager storage getItem', e);
        return null;
      }
    },
    setItem: (key, val) => {
      try {
        let options = { expires: 1 };
        if (CONFIG.ENVIRONMENT !== 'local') { // localhost has some restrictions
          options = Object.assign(options, { secure: true, domain: '.domain.com' });
        }
        // console.log('set', key, val);

        const value = JSON.parse(val);

        Object.keys(value).forEach((k) => {
          const storageKey = `${key}_${k}`;
          delete value[k].value; // unncessary and makes cookie heavy
          Cookies.set(storageKey, value[k], options);
        });
      } catch (e) {
        console.error('OKTA tokenManager storage setItem', e);
      }
    }
  }
DavidLozzi commented 4 years ago

To clarify, I am using the expiresAt value from the accessToken to determine when it expires, taking that value and creating a date, like new Date(JSON.parse(getCookie('okta-token-storage_accessToken')).expiresAt * 1000)

shuowu commented 4 years ago

@DavidLozzi From the code, I don't see issues. You probably can check if there is any special cookie rule has been set in your DEV/QA envs. Also, this link may help: https://github.com/js-cookie/js-cookie/wiki/Frequently-Asked-Questions#how-to-make-the-cookie-expire-in-less-than-a-day

DavidLozzi commented 4 years ago

@shuowu It appears to be something in our Okta config, as I reverted our code to 2 days ago and still get the 2hr expiration, no cookies being used. Sorry for the reopen, I thought the only thing that changed was my code, but alas, it appears that's not the case (I'm assuming). Thanks for the response!