From 6be27f6452413333e7889dbb1dd199e6c0628969 Mon Sep 17 00:00:00 2001 From: Mohan R Date: Fri, 3 Jul 2020 13:37:17 +0530 Subject: [PATCH] fix(api-gitlab-v4): workaround for gitlab redirect_uri issue (close #99) --- .../api-gitlab-v4/__tests__/index.spec.ts | 73 ++++++++++++++++--- packages/@vssue/api-gitlab-v4/src/index.ts | 32 ++++++-- 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/packages/@vssue/api-gitlab-v4/__tests__/index.spec.ts b/packages/@vssue/api-gitlab-v4/__tests__/index.spec.ts index cecdb6d1..27268542 100644 --- a/packages/@vssue/api-gitlab-v4/__tests__/index.spec.ts +++ b/packages/@vssue/api-gitlab-v4/__tests__/index.spec.ts @@ -55,14 +55,19 @@ describe('methods', () => { delete window.location; const url = 'https://vssue.js.org'; - window.location = { href: url } as any; + window.location = { href: url, origin: url } as any; + const stateobj = { + state: options.state, + redirect_uri: window.location.pathname, + }; + const stateobj64 = encodeURIComponent(btoa(JSON.stringify(stateobj))); API.redirectAuth(); expect(window.location.href).toBe( `${baseURL}/oauth/authorize?client_id=${ options.clientId - }&redirect_uri=${encodeURIComponent(url)}&response_type=token&state=${ - options.state - }` + }&redirect_uri=${encodeURIComponent( + url + )}&response_type=token&state=${stateobj64}` ); // reset `window.location` @@ -79,28 +84,76 @@ describe('methods', () => { }); test('with matched state', async () => { - const url = `https://vssue.js.org/#access_token=${mockToken}&state=${options.state}`; + const initial_uri = 'https://vssue.js.org/'; + const stateobj = { + state: options.state, + }; + const stateobj64 = encodeURIComponent(btoa(JSON.stringify(stateobj))); + const url = `${initial_uri}#access_token=${mockToken}&state=${stateobj64}`; window.history.replaceState(null, '', url); const token = await API.handleAuth(); - expect(window.location.href).toBe('https://vssue.js.org/'); + expect(window.location.href).toBe(initial_uri); expect(token).toBe(mockToken); }); test('with unmatched state', async () => { - const url = `https://vssue.js.org/#access_token=${mockToken}&state=${options.state}-unmatched`; + const initial_uri = 'https://vssue.js.org/'; + const stateobj = { + state: `${options.state}-unmatched`, + }; + const stateobj64 = encodeURIComponent(btoa(JSON.stringify(stateobj))); + const url = `${initial_uri}#access_token=${mockToken}&state=${stateobj64}`; window.history.replaceState(null, '', url); const token = await API.handleAuth(); - expect(window.location.href).toBe(url); + expect(window.location.href).toBe(initial_uri); expect(token).toBe(null); }); test('with extra hash', async () => { - const url = `https://vssue.js.org/#access_token=${mockToken}&state=${options.state}&extra=hash`; + const initial_uri = 'https://vssue.js.org/'; + const stateobj = { + state: options.state, + }; + const stateobj64 = encodeURIComponent(btoa(JSON.stringify(stateobj))); + const url = `https://vssue.js.org/#access_token=${mockToken}&state=${stateobj64}&extra=hash`; window.history.replaceState(null, '', url); const token = await API.handleAuth(); - expect(window.location.href).toBe('https://vssue.js.org/#extra=hash'); + expect(window.location.href).toBe(`${initial_uri}#extra=hash`); expect(token).toBe(mockToken); }); + + test('with redirect_uri', async () => { + // to make `window.location` writable + const location = window.location; + delete window.location; + + const stateobj = { + state: options.state, + redirect_uri: '/deep/path', + }; + const stateobj64 = encodeURIComponent(btoa(JSON.stringify(stateobj))); + window.location = { + href: '', + origin: '', + search: '', + hash: `#access_token=${mockToken}&state=${stateobj64}&extra=hash`, + assign: (a, b, c) => { + window.location.href = c; + }, + } as any; + const token = await API.handleAuth(); + delete stateobj.redirect_uri; + const stateobj64result = encodeURIComponent( + btoa(JSON.stringify(stateobj)) + ); + expect(window.location.href).toBe( + `/deep/path#extra=hash&access_token=${mockToken}&state=${stateobj64result}` + ); + expect(token).toBe(mockToken); + + // reset `window.location` + window.location = location; + }); }); test('getUser', async () => { diff --git a/packages/@vssue/api-gitlab-v4/src/index.ts b/packages/@vssue/api-gitlab-v4/src/index.ts index a42ae9d2..a6761ad4 100644 --- a/packages/@vssue/api-gitlab-v4/src/index.ts +++ b/packages/@vssue/api-gitlab-v4/src/index.ts @@ -91,13 +91,17 @@ export default class GitlabV4 implements VssueAPI.Instance { * @see https://docs.gitlab.com/ce/api/oauth2.html#1-requesting-authorization-code */ redirectAuth(): void { + const stateobj = { + state: this.state, + redirect_uri: window.location.pathname, + }; window.location.href = buildURL( concatURL(this.baseURL, 'oauth/authorize'), { client_id: this.clientId, - redirect_uri: window.location.href, + redirect_uri: window.location.origin, response_type: 'token', - state: this.state, + state: btoa(JSON.stringify(stateobj)), } ); } @@ -112,10 +116,10 @@ export default class GitlabV4 implements VssueAPI.Instance { */ async handleAuth(): Promise { const hash = parseQuery(window.location.hash.slice(1)); - if (!hash.access_token || hash.state !== this.state) { - return null; - } + const stateobj = JSON.parse(atob(hash.state ? hash.state : btoa('{}'))); const accessToken = hash.access_token; + const token_type = hash.token_type; + const expires_in = hash.expires_in; delete hash.access_token; delete hash.token_type; delete hash.expires_in; @@ -126,6 +130,24 @@ export default class GitlabV4 implements VssueAPI.Instance { window.location.search }${newHash}`; window.history.replaceState(null, '', replaceURL); + if (accessToken == null) { + return null; + } + if (stateobj == null || stateobj.state !== this.state) { + return null; + } + if (stateobj.redirect_uri != null && stateobj.redirect_uri !== '/') { + const redirect_uri = stateobj.redirect_uri; + delete stateobj.redirect_uri; + hash.access_token = accessToken; + hash.token_type = token_type; + hash.expores_in = expires_in; + hash.state = btoa(JSON.stringify(stateobj)); + const hashString = buildQuery(hash); + window.location.href = `${getCleanURL(redirect_uri)}${ + window.location.search + }#${hashString}`; + } return accessToken; }