Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-13570-fixed-1776157063540.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Login issue: Use sessionStorage for codeVerifier and nonce ([#13570](https://github.com/linode/manager/pull/13570))
18 changes: 9 additions & 9 deletions packages/manager/src/OAuth/oauth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ describe('generateOAuthAuthorizeEndpoint', () => {
expect(url).toContain('code_challenge=');
});

it('generates a "state" (aka nonce), stores it in local storage, and includes it in the url', async () => {
it('generates a "state" (aka nonce), stores it in session storage, and includes it in the url', async () => {
storage.authentication.nonce.clear();

expect(storage.authentication.nonce.get()).toBeNull();
expect(storage.authentication.nonce.get()).toBeUndefined();

const url = await generateOAuthAuthorizeEndpoint('/linodes');

Expand All @@ -91,10 +91,10 @@ describe('generateOAuthAuthorizeEndpoint', () => {
expect(url).toContain(`state=${nonceInStorage}`);
});

it('generates a code verifier and stores it in local storage', async () => {
it('generates a code verifier and stores it in session storage', async () => {
storage.authentication.codeVerifier.clear();

expect(storage.authentication.codeVerifier.get()).toBeNull();
expect(storage.authentication.codeVerifier.get()).toBeUndefined();

await generateOAuthAuthorizeEndpoint('/linodes');

Expand Down Expand Up @@ -126,19 +126,19 @@ describe('handleOAuthCallback', () => {
).rejects.toThrowError('Error parsing search params on OAuth callback.');
});

it('should throw if there is no code verifier found in local storage', async () => {
it('should throw if there is no code verifier found in session storage', async () => {
storage.authentication.codeVerifier.clear();

await expect(
handleOAuthCallback({
params: 'state=fehgefhgkefghk&code=gyuwyutfetyfew',
})
).rejects.toThrowError(
'No code codeVerifier found in local storage when running OAuth callback.'
'No code codeVerifier found in session storage when running OAuth callback.'
);
});

it('should throw if there is no nonce found in local storage', async () => {
it('should throw if there is no nonce found in session storage', async () => {
storage.authentication.codeVerifier.set('fakecodeverifier');
storage.authentication.nonce.clear();

Expand All @@ -147,11 +147,11 @@ describe('handleOAuthCallback', () => {
params: 'state=fehgefhgkefghk&code=gyuwyutfetyfew',
})
).rejects.toThrowError(
'No nonce found in local storage when running OAuth callback.'
'No nonce found in session storage when running OAuth callback.'
);
});

it('should throw if the nonce in local storage does not match the "state" sent back by login', async () => {
it('should throw if the nonce in session storage does not match the "state" sent back by login', async () => {
storage.authentication.codeVerifier.set('fakecodeverifier');
storage.authentication.nonce.set('fakenonce');

Expand Down
4 changes: 2 additions & 2 deletions packages/manager/src/OAuth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export async function handleOAuthCallback(options: AuthCallbackOptions) {

if (!codeVerifier) {
throw new AuthenticationError(
'No code codeVerifier found in local storage when running OAuth callback.'
'No code codeVerifier found in session storage when running OAuth callback.'
);
}

Expand All @@ -217,7 +217,7 @@ export async function handleOAuthCallback(options: AuthCallbackOptions) {

if (!storedNonce) {
throw new AuthenticationError(
'No nonce found in local storage when running OAuth callback.'
'No nonce found in session storage when running OAuth callback.'
);
}

Expand Down
32 changes: 26 additions & 6 deletions packages/manager/src/utilities/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import type { SupportTicketFormFields } from 'src/features/Support/SupportTickets/SupportTicketDialog';

const localStorageCache: Record<string, any> = {};
const sessionStorageCache: Record<string, any> = {};

Check warning on line 8 in packages/manager/src/utilities/storage.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Unexpected any. Specify a different type. Raw Output: {"ruleId":"@typescript-eslint/no-explicit-any","severity":1,"message":"Unexpected any. Specify a different type.","line":8,"column":43,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":8,"endColumn":46,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[380,383],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[380,383],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}

export const getStorage = (key: string, fallback?: any) => {
if (localStorageCache[key]) {
Expand Down Expand Up @@ -48,6 +49,25 @@
window.localStorage.removeItem(key);
};

export const getSessionStorage = (key: string): string | undefined => {
if (key in sessionStorageCache) {
return sessionStorageCache[key];
}
const item = window.sessionStorage.getItem(key);
sessionStorageCache[key] = item ?? undefined;
return item ?? undefined;
};

export const setSessionStorage = (key: string, value: string) => {
sessionStorageCache[key] = value;
window.sessionStorage.setItem(key, value);
};

export const clearSessionStorage = (key: string) => {
delete sessionStorageCache[key];
window.sessionStorage.removeItem(key);
};

const PAGE_SIZE = 'PAGE_SIZE';
const INFINITE_PAGE_SIZE = 'INFINITE_PAGE_SIZE';
const TOKEN = 'authentication/token';
Expand Down Expand Up @@ -150,19 +170,19 @@
export const storage: Storage = {
authentication: {
codeVerifier: {
get: () => getStorage(CODE_VERIFIER),
set: (v) => setStorage(CODE_VERIFIER, v),
clear: () => clearStorage(CODE_VERIFIER),
get: () => getSessionStorage(CODE_VERIFIER),
set: (v) => setSessionStorage(CODE_VERIFIER, v),
clear: () => clearSessionStorage(CODE_VERIFIER),
},
expire: {
get: () => getStorage(EXPIRE),
set: (v) => setStorage(EXPIRE, v),
clear: () => clearStorage(EXPIRE),
},
nonce: {
get: () => getStorage(NONCE),
set: (v) => setStorage(NONCE, v),
clear: () => clearStorage(NONCE),
get: () => getSessionStorage(NONCE),
set: (v) => setSessionStorage(NONCE, v),
clear: () => clearSessionStorage(NONCE),
},
scopes: {
get: () => getStorage(SCOPES),
Expand Down
Loading