Skip to content

Commit

Permalink
[SDK-1858] Create legacy samsite cookie by default (#568)
Browse files Browse the repository at this point in the history
* Create legacy samsite cookie by default

* CookieStorage -> cookieStorage

Co-authored-by: Steve Hobbs <[email protected]>
Co-authored-by: Steve Hobbs <[email protected]>
  • Loading branch information
3 people committed Sep 2, 2020
1 parent 49661e8 commit f4391f6
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 30 deletions.
25 changes: 25 additions & 0 deletions __tests__/Auth0Client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -860,4 +860,29 @@ describe('Auth0Client', () => {

expect(utils.runIframe).toHaveBeenCalled();
});

it('checks the legacy samesite cookie', async () => {
const auth0 = setup();
(<jest.Mock>esCookie.get).mockReturnValueOnce(undefined);
await auth0.checkSession();
expect(<jest.Mock>esCookie.get).toHaveBeenCalledWith(
'auth0.is.authenticated'
);
expect(<jest.Mock>esCookie.get).toHaveBeenCalledWith(
'_legacy_auth0.is.authenticated'
);
});

it('skips checking the legacy samesite cookie when configured', async () => {
const auth0 = setup({
legacySameSiteCookie: false
});
await auth0.checkSession();
expect(<jest.Mock>esCookie.get).toHaveBeenCalledWith(
'auth0.is.authenticated'
);
expect(<jest.Mock>esCookie.get).not.toHaveBeenCalledWith(
'_legacy_auth0.is.authenticated'
);
});
});
9 changes: 7 additions & 2 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ jest.mock('../src/storage', () => ({
get: jest.fn(),
save: jest.fn(),
remove: jest.fn()
},
CookieStorageWithLegacySameSite: {
get: jest.fn(),
save: jest.fn(),
remove: jest.fn()
}
}));

Expand Down Expand Up @@ -132,7 +137,7 @@ const setup = async (clientOptions: Partial<Auth0ClientOptions> = {}) => {

return {
auth0,
cookieStorage: require('../src/storage').CookieStorage,
cookieStorage: require('../src/storage').CookieStorageWithLegacySameSite,
cache,
tokenVerifier,
transactionManager,
Expand Down Expand Up @@ -2222,7 +2227,7 @@ describe('default creation function', () => {
it('does nothing if there is nothing in storage', async () => {
jest.spyOn(Auth0Client.prototype, 'getTokenSilently');
const getSpy = jest
.spyOn(require('../src/storage').CookieStorage, 'get')
.spyOn(require('../src/storage').CookieStorageWithLegacySameSite, 'get')
.mockReturnValueOnce(false);

const auth0 = await createAuth0Client({
Expand Down
122 changes: 100 additions & 22 deletions __tests__/storage.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { CookieStorage } from '../src/storage';
import * as esCookie from 'es-cookie';
import { mocked } from 'ts-jest/utils';
import { CookieStorage, CookieStorageWithLegacySameSite } from '../src/storage';

jest.mock('es-cookie');

describe('cookie storage', () => {
describe('CookieStorage', () => {
let cookieMock;

beforeEach(() => {
jest.resetAllMocks();
cookieMock = mocked(esCookie);
});
it('saves object', () => {
const key = 'key';
const value = { some: 'value' };
const options = { daysUntilExpire: 1 };
CookieStorage.save(key, value, options);
expect(require('es-cookie').set).toHaveBeenCalledWith(
key,
JSON.stringify(value),
{
expires: options.daysUntilExpire
}
);
expect(cookieMock.set).toHaveBeenCalledWith(key, JSON.stringify(value), {
expires: options.daysUntilExpire
});
});
it('saves object with secure flag and samesite=none when on https', () => {
const key = 'key';
Expand All @@ -26,19 +27,15 @@ describe('cookie storage', () => {
delete window.location;
window.location = { ...originalLocation, protocol: 'https:' };
CookieStorage.save(key, value, options);
expect(require('es-cookie').set).toHaveBeenCalledWith(
key,
JSON.stringify(value),
{
expires: options.daysUntilExpire,
secure: true,
sameSite: 'none'
}
);
expect(cookieMock.set).toHaveBeenCalledWith(key, JSON.stringify(value), {
expires: options.daysUntilExpire,
secure: true,
sameSite: 'none'
});
window.location = originalLocation;
});
it('returns undefined when there is no object', () => {
const Cookie = require('es-cookie');
const Cookie = cookieMock;
const key = 'key';
Cookie.get = k => {
expect(k).toBe(key);
Expand All @@ -48,7 +45,7 @@ describe('cookie storage', () => {
expect(outputValue).toBeUndefined();
});
it('gets object', () => {
const Cookie = require('es-cookie');
const Cookie = cookieMock;
const key = 'key';
const value = { some: 'value' };
Cookie.get = k => {
Expand All @@ -59,9 +56,90 @@ describe('cookie storage', () => {
expect(outputValue).toMatchObject(value);
});
it('removes object', () => {
const Cookie = require('es-cookie');
const Cookie = cookieMock;
const key = 'key';
CookieStorage.remove(key);
expect(Cookie.remove).toHaveBeenCalledWith(key);
});
});

describe('CookieStorageWithLegacySameSite', () => {
let cookieMock;

beforeEach(() => {
cookieMock = mocked(esCookie);
});
it('saves object', () => {
const key = 'key';
const value = { some: 'value' };
const options = { daysUntilExpire: 1 };
CookieStorageWithLegacySameSite.save(key, value, options);
expect(cookieMock.set).toHaveBeenCalledWith(key, JSON.stringify(value), {
expires: options.daysUntilExpire
});
expect(cookieMock.set).toHaveBeenCalledWith(
`_legacy_${key}`,
JSON.stringify(value),
{
expires: options.daysUntilExpire
}
);
});
it('saves object with secure flag and samesite=none and legacy with no samesite when on https', () => {
const key = 'key';
const value = { some: 'value' };
const options = { daysUntilExpire: 1 };
const originalLocation = window.location;
delete window.location;
window.location = { ...originalLocation, protocol: 'https:' };
CookieStorageWithLegacySameSite.save(key, value, options);
expect(cookieMock.set).toHaveBeenCalledWith(key, JSON.stringify(value), {
expires: options.daysUntilExpire,
secure: true,
sameSite: 'none'
});
expect(cookieMock.set).toHaveBeenCalledWith(
`_legacy_${key}`,
JSON.stringify(value),
{
expires: options.daysUntilExpire,
secure: true
}
);
window.location = originalLocation;
});
it('returns undefined when there is no object', () => {
const Cookie = cookieMock;
const key = 'key';
Cookie.get = k => undefined;
const outputValue = CookieStorageWithLegacySameSite.get(key);
expect(outputValue).toBeUndefined();
});
it('returns modern samesite cookie when available', () => {
const Cookie = cookieMock;
const key = 'key';
Cookie.get = k => {
if (k === key) return JSON.stringify({ foo: 1 });
return JSON.stringify({ bar: 2 });
};
const outputValue = CookieStorageWithLegacySameSite.get(key);
expect(outputValue).toEqual({ foo: 1 });
});
it('falls back to legacy cookie when modern cookie is unavailable', () => {
const Cookie = cookieMock;
const key = 'key';
Cookie.get = k => {
if (k === key) return false;
return JSON.stringify({ bar: 2 });
};
const outputValue = CookieStorageWithLegacySameSite.get(key);
expect(outputValue).toEqual({ bar: 2 });
});
it('removes objects', () => {
const Cookie = cookieMock;
const key = 'key';
CookieStorageWithLegacySameSite.remove(key);
expect(Cookie.remove).toHaveBeenCalledWith(key);
expect(Cookie.remove).toHaveBeenCalledWith(`_legacy_${key}`);
});
});
26 changes: 20 additions & 6 deletions src/Auth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import { InMemoryCache, ICache, LocalStorageCache } from './cache';
import TransactionManager from './transaction-manager';
import { verify as verifyIdToken } from './jwt';
import { AuthenticationError } from './errors';
import { CookieStorage, SessionStorage } from './storage';
import {
ClientStorage,
CookieStorage,
CookieStorageWithLegacySameSite,
SessionStorage
} from './storage';

import {
CACHE_LOCATION_MEMORY,
Expand Down Expand Up @@ -128,13 +133,18 @@ export default class Auth0Client {
private tokenIssuer: string;
private defaultScope: string;
private scope: string;
private cookieStorage: ClientStorage;

cacheLocation: CacheLocation;
private worker: Worker;

constructor(private options: Auth0ClientOptions) {
typeof window !== 'undefined' && validateCrypto();
this.cacheLocation = options.cacheLocation || CACHE_LOCATION_MEMORY;
this.cookieStorage =
options.legacySameSiteCookie === false
? CookieStorage
: CookieStorageWithLegacySameSite;

if (!cacheFactory(this.cacheLocation)) {
throw new Error(`Invalid cache location "${this.cacheLocation}"`);
Expand Down Expand Up @@ -367,7 +377,9 @@ export default class Auth0Client {

this.cache.save(cacheEntry);

CookieStorage.save('auth0.is.authenticated', true, { daysUntilExpire: 1 });
this.cookieStorage.save('auth0.is.authenticated', true, {
daysUntilExpire: 1
});
}

/**
Expand Down Expand Up @@ -501,7 +513,9 @@ export default class Auth0Client {

this.cache.save(cacheEntry);

CookieStorage.save('auth0.is.authenticated', true, { daysUntilExpire: 1 });
this.cookieStorage.save('auth0.is.authenticated', true, {
daysUntilExpire: 1
});

return {
appState: transaction.appState
Expand All @@ -526,7 +540,7 @@ export default class Auth0Client {
public async checkSession(options?: GetTokenSilentlyOptions) {
if (
this.cacheLocation === CACHE_LOCATION_MEMORY &&
!CookieStorage.get('auth0.is.authenticated')
!this.cookieStorage.get('auth0.is.authenticated')
) {
return;
}
Expand Down Expand Up @@ -613,7 +627,7 @@ export default class Auth0Client {

this.cache.save({ client_id: this.options.client_id, ...authResult });

CookieStorage.save('auth0.is.authenticated', true, {
this.cookieStorage.save('auth0.is.authenticated', true, {
daysUntilExpire: 1
});

Expand Down Expand Up @@ -708,7 +722,7 @@ export default class Auth0Client {
}

this.cache.clear();
CookieStorage.remove('auth0.is.authenticated');
this.cookieStorage.remove('auth0.is.authenticated');

if (localOnly) {
return;
Expand Down
10 changes: 10 additions & 0 deletions src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ export interface Auth0ClientOptions extends BaseLoginOptions {
*/
auth0Client?: { name: string; version: string };

/**
* Sets an additional cookie with no SameSite attribute to support legacy browsers
* that are not compatible with the latest SameSite changes.
* This will log a warning on modern browsers, you can disable the warning by setting
* this to false but be aware that some older useragents will not work,
* See https://www.chromium.org/updates/same-site/incompatible-clients
* Defaults to true
*/
legacySameSiteCookie?: boolean;

/**
* Changes to recommended defaults, like defaultScope
*/
Expand Down
38 changes: 38 additions & 0 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,44 @@ export const CookieStorage = {
}
} as ClientStorage;

/**
* @ignore
*/
const LEGACY_PREFIX = '_legacy_';

/**
* Cookie storage that creates a cookie for modern and legacy browsers.
* See: https://web.dev/samesite-cookie-recipes/#handling-incompatible-clients
*/
export const CookieStorageWithLegacySameSite = {
get<T extends Object>(key: string) {
const value = CookieStorage.get(key);
if (value) {
return value;
}
return CookieStorage.get(`${LEGACY_PREFIX}${key}`);
},

save(key: string, value: any, options?: ClientStorageOptions): void {
let cookieAttributes: Cookies.CookieAttributes = {};
if ('https:' === window.location.protocol) {
cookieAttributes = { secure: true };
}
cookieAttributes.expires = options.daysUntilExpire;
Cookies.set(
`${LEGACY_PREFIX}${key}`,
JSON.stringify(value),
cookieAttributes
);
CookieStorage.save(key, value, options);
},

remove(key: string) {
CookieStorage.remove(key);
CookieStorage.remove(`${LEGACY_PREFIX}${key}`);
}
} as ClientStorage;

/**
* A storage protocol for marshalling data to/from session storage
*/
Expand Down

0 comments on commit f4391f6

Please sign in to comment.