diff --git a/src/utils/auth.js b/src/utils/auth.js index c41ee434..f0f71700 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -79,9 +79,9 @@ export async function decodeIdToken(state, idp, idToken, lenient = false) { * * @param {PipelineState} state * @param {PipelineRequest} req - * @return {string} + * @returns {{proto: (*|string), host: string}} the request host and protocol. */ -function getRequestHost(state, req) { +function getRequestHostAndProto(state, req) { // determine the location of 'this' document based on the xfh header. so that logins to // .page stay on .page. etc. but fallback to the config.host if non set let host = req.headers.get('x-forwarded-host'); @@ -91,8 +91,12 @@ function getRequestHost(state, req) { if (!host) { host = state.config.host; } - state.log.info(`request host is: ${host}`); - return host; + const proto = req.headers.get('x-forwarded-proto') || 'https'; + state.log.info(`request host is: ${host} (${proto})`); + return { + host, + proto, + }; } /** @@ -181,7 +185,7 @@ export class AuthInfo { // determine the location of 'this' document based on the xfh header. so that logins to // .page stay on .page. etc. but fallback to the config.host if non set - const host = getRequestHost(state, req); + const { host, proto } = getRequestHostAndProto(state, req); if (!host) { log.error('[auth] unable to create login redirect: no xfh or config.host.'); res.status = 401; @@ -199,6 +203,7 @@ export class AuthInfo { // this is our own login redirect, i.e. the current document requestPath: state.info.path, requestHost: host, + requestProto: proto, }).encode(); url.searchParams.append('client_id', clientId); @@ -239,9 +244,9 @@ export class AuthInfo { // ensure that the request is made to the target host if (req.params.state?.requestHost) { - const host = getRequestHost(state, req); + const { host } = getRequestHostAndProto(state, req); if (host !== req.params.state.requestHost) { - const url = new URL(`https://${req.params.state.requestHost}/.auth`); + const url = new URL(`${req.params.state.requestProto}://${req.params.state.requestHost}/.auth`); url.searchParams.append('state', req.params.rawState); url.searchParams.append('code', req.params.code); const location = state.createExternalLocation(url.href); diff --git a/test/html-pipe.test.js b/test/html-pipe.test.js index 88c93d16..cece6789 100644 --- a/test/html-pipe.test.js +++ b/test/html-pipe.test.js @@ -94,6 +94,7 @@ describe('HTML Pipe Test', () => { // this is our own login redirect, i.e. the current document requestPath: '/en', requestHost: 'www.hlx.live', + requestProto: 'https', }).encode(); const req = new PipelineRequest('https://localhost/.auth', { diff --git a/test/utils/auth.test.js b/test/utils/auth.test.js index 86af41bf..fd510e63 100644 --- a/test/utils/auth.test.js +++ b/test/utils/auth.test.js @@ -349,7 +349,7 @@ describe('AuthInfo tests', () => { redirect_uri: 'https://login.hlx.page/.auth', response_type: 'code', scope: 'openid profile email', - state: 'eyJhbGciOiJub25lIn0.eyJyZXF1ZXN0UGF0aCI6Ii8iLCJyZXF1ZXN0SG9zdCI6Ind3dy5obHgubGl2ZSJ9.', + state: 'eyJhbGciOiJub25lIn0.eyJyZXF1ZXN0UGF0aCI6Ii8iLCJyZXF1ZXN0SG9zdCI6Ind3dy5obHgubGl2ZSIsInJlcXVlc3RQcm90byI6Imh0dHBzIn0.', }); }); @@ -370,6 +370,30 @@ describe('AuthInfo tests', () => { const reqState = new URL(res.headers.get('location')).searchParams.get('state'); assert.deepStrictEqual(decodeJwt(reqState), { requestHost: 'www.hlx.live', + requestProto: 'https', + requestPath: '/', + }); + }); + + it('redirects to the login page (xfh, proto)', async () => { + const authInfo = AuthInfo + .Default() + .withIdp(idpFakeTestIDP); + + const state = new PipelineState({}); + const req = new PipelineRequest('https://localhost', { + headers: { + 'x-forwarded-host': 'localhost', + 'x-forwarded-proto': 'http', + }, + }); + const res = new PipelineResponse(); + await authInfo.redirectToLogin(state, req, res); + assert.strictEqual(res.status, 302); + const reqState = new URL(res.headers.get('location')).searchParams.get('state'); + assert.deepStrictEqual(decodeJwt(reqState), { + requestHost: 'localhost', + requestProto: 'http', requestPath: '/', }); }); @@ -391,6 +415,7 @@ describe('AuthInfo tests', () => { const reqState = new URL(res.headers.get('location')).searchParams.get('state'); assert.deepStrictEqual(decodeJwt(reqState), { requestHost: 'bla.live', + requestProto: 'https', requestPath: '/en/blog', }); }); @@ -463,13 +488,14 @@ describe('AuthInfo tests', () => { req.params.state = { requestPath: '/en', requestHost: 'localhost', + requestProto: 'http', }; req.params.rawState = 'raw'; const res = new PipelineResponse(); await authInfo.exchangeToken(state, req, res); assert.strictEqual(res.status, 302); - assert.strictEqual(res.headers.get('location'), 'https://localhost/.auth?state=raw&code=1234'); + assert.strictEqual(res.headers.get('location'), 'http://localhost/.auth?state=raw&code=1234'); }); it('exchangeToken fetches the token', async () => { @@ -502,6 +528,7 @@ describe('AuthInfo tests', () => { req.params.state = { requestPath: '/en', requestHost: 'localhost', + requestProto: 'https', }; req.headers.set('x-forwarded-host', 'localhost');