From 06561e2a50163575ad21de4e088efa3c324ea661 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Fri, 29 Oct 2021 16:55:22 +0800 Subject: [PATCH] feat: handle redirect callback --- packages/client/src/index.test.ts | 30 +++++++++++++++++++--- packages/client/src/index.ts | 13 +++++++++- packages/client/src/parse-callback.test.ts | 16 ++++++++++++ packages/client/src/parse-callback.ts | 18 +++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 packages/client/src/parse-callback.test.ts create mode 100644 packages/client/src/parse-callback.ts diff --git a/packages/client/src/index.test.ts b/packages/client/src/index.test.ts index d9df001e9..ae46d5ea1 100644 --- a/packages/client/src/index.test.ts +++ b/packages/client/src/index.test.ts @@ -19,6 +19,8 @@ const CLIENT_ID = 'client1'; const SUBJECT = 'subject1'; const REDIRECT_URI = 'http://localhost:3000'; const SESSEION_MANAGER_KEY = 'LOGTO_SESSION_MANAGER'; +const REDIRECT_CALLBACK = `${REDIRECT_URI}?code=authorization_code`; +const REDIRECT_CALLBACK_WITH_ERROR = `${REDIRECT_CALLBACK}&error=invalid_request&error_description=code_challenge%20must%20be%20a%20string%20with%20a%20minimum%20length%20of%2043%20characters`; const discoverResponse = { authorization_endpoint: `${BASE_URL}/oidc/auth`, @@ -170,7 +172,27 @@ describe('LogtoClient', () => { clientId: CLIENT_ID, storage, }); - await expect(logto.handleCallback('code')).rejects.toThrowError(); + await expect(logto.handleCallback(REDIRECT_CALLBACK)).rejects.toThrowError(); + }); + + test('no code in url should fail', async () => { + const storage = new MemoryStorage(); + const logto = await LogtoClient.create({ + domain: DOMAIN, + clientId: CLIENT_ID, + storage, + }); + await expect(logto.handleCallback('')).rejects.toThrowError(); + }); + + test('should throw response error', async () => { + const storage = new MemoryStorage(); + const logto = await LogtoClient.create({ + domain: DOMAIN, + clientId: CLIENT_ID, + storage, + }); + await expect(logto.handleCallback(REDIRECT_CALLBACK_WITH_ERROR)).rejects.toThrowError(); }); test('verifyIdToken should be called', async () => { @@ -181,7 +203,7 @@ describe('LogtoClient', () => { storage, }); await logto.loginWithRedirect(REDIRECT_URI); - await logto.handleCallback('code'); + await logto.handleCallback(REDIRECT_CALLBACK); expect(verifyIdToken).toHaveBeenCalled(); }); @@ -193,7 +215,7 @@ describe('LogtoClient', () => { storage, }); await logto.loginWithRedirect(REDIRECT_URI); - await logto.handleCallback('code'); + await logto.handleCallback(REDIRECT_CALLBACK); expect(storage.getItem('LOGTO_SESSION_MANAGER')).toBeUndefined(); }); @@ -205,7 +227,7 @@ describe('LogtoClient', () => { storage, }); await logto.loginWithRedirect(REDIRECT_URI); - await logto.handleCallback('code'); + await logto.handleCallback(REDIRECT_CALLBACK); expect(logto.isAuthenticated()).toBeTruthy(); }); }); diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 16d77091e..832639226 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -2,6 +2,7 @@ import { Optional } from '@silverhand/essentials'; import discover, { OIDCConfiguration } from './discover'; import { grantTokenByAuthorizationCode, TokenSetParameters } from './grant-token'; +import { parseRedirectCallback } from './parse-callback'; import { getLoginUrlAndCodeVerifier } from './request-login'; import { getLogoutUrl } from './request-logout'; import SessionManager from './session-manager'; @@ -57,7 +58,17 @@ export default class LogtoClient { window.location.assign(url); } - public async handleCallback(code: string) { + public async handleCallback(url: string) { + const { code, error, error_description } = parseRedirectCallback(url); + + if (error) { + throw new Error(error_description ?? error); + } + + if (!code) { + throw new Error(`Can not found authorization_code in url: ${url}`); + } + const session = this.sessionManager.get(); if (!session) { diff --git a/packages/client/src/parse-callback.test.ts b/packages/client/src/parse-callback.test.ts new file mode 100644 index 000000000..2e511d6b3 --- /dev/null +++ b/packages/client/src/parse-callback.test.ts @@ -0,0 +1,16 @@ +import { parseRedirectCallback } from './parse-callback'; + +describe('parseRedirectCallback', () => { + test('get result', () => { + const url = + 'http://localhost:3000/callback?code=random-code&error=error&error_description=some%20description'; + const result = parseRedirectCallback(url); + expect(result.code).toEqual('random-code'); + expect(result.error).toEqual('error'); + expect(result.error_description).toEqual('some description'); + }); + + test('no query params should fail', () => { + expect(() => parseRedirectCallback('http://localhost:3000')).toThrow(); + }); +}); diff --git a/packages/client/src/parse-callback.ts b/packages/client/src/parse-callback.ts new file mode 100644 index 000000000..065ed4a29 --- /dev/null +++ b/packages/client/src/parse-callback.ts @@ -0,0 +1,18 @@ +import qs from 'query-string'; + +export interface AuthenticationResult { + code?: string; + error?: string; + error_description?: string; +} + +export const parseRedirectCallback = (url: string) => { + const [, queryString] = url.split('?'); + if (!queryString) { + throw new Error('There are no query params available for parsing.'); + } + + const result = qs.parse(queryString) as AuthenticationResult; + + return result; +};