diff --git a/handshake.test.ts b/handshake.test.ts new file mode 100644 index 00000000000..fa513db1a7c --- /dev/null +++ b/handshake.test.ts @@ -0,0 +1,615 @@ +// @ts-ignore ignore types +import * as http from 'http'; +import { generateConfig, getJwksFromSecretKey } from './handshakeTestConfigs'; + +const urlArg = process.argv.find(x => x.startsWith('--url='))?.replace('--url=', ''); +if (!urlArg) { + throw new Error('Must pass URL like: --url=http://localhost:4011'); +} + +// Strip trailing slash +const url = new URL(urlArg).origin; +const devBrowserCookie = '__clerk_db_jwt=needstobeset;'; +const devBrowserQuery = '&__clerk_db_jwt=needstobeset'; + +//create a server object: +const server = http.createServer(function (req, res) { + const sk = req.headers.authorization?.replace('Bearer ', ''); + if (!sk) { + console.log('No SK to', req.url, req.headers); + } + + const jwks = getJwksFromSecretKey(sk); + + res.setHeader('Content-Type', 'application/json'); + res.write(JSON.stringify(getJwksFromSecretKey(sk))); //write a response to the client + res.end(); //end the response +}); + +beforeAll(() => { + console.log( + 'Starting jwks service on 127.0.0.1:4199.\nMake sure the framework has CLERK_API_URL set to http://localhost:4199', + ); + server.listen(4199); + + console.log('Running tests against ', url); +}); + +afterAll(() => { + server.close(); + setImmediate(function () { + server.emit('close'); + }); +}); + +test('Test standard signed-in - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token, claims } = config.generateToken({ state: 'active' }); + const clientUat = claims.iat; + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); +}); + +test('Test standard signed-in - prod', async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token, claims } = config.generateToken({ state: 'active' }); + const clientUat = claims.iat; + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `__client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); +}); + +test('Test expired session token - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token, claims } = config.generateToken({ state: 'expired' }); + const clientUat = claims.iat; + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://${config.pkHost}/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2F${devBrowserQuery}`, + ); +}); + +test('Test expired session token - prod', async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token, claims } = config.generateToken({ state: 'expired' }); + const clientUat = claims.iat; + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `__client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://${config.pkHost}/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2F`, + ); +}); + +test('Test early session token - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token, claims } = config.generateToken({ state: 'early' }); + const clientUat = claims.iat; + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://${config.pkHost}/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2F${devBrowserQuery}`, + ); +}); + +test('Test proxyUrl - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token, claims } = config.generateToken({ state: 'expired' }); + const clientUat = claims.iat; + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + 'X-Proxy-Url': 'https://example.com/clerk', + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://example.com/clerk/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2F${devBrowserQuery}`, + ); +}); + +test('Test proxyUrl - prod', async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token, claims } = config.generateToken({ state: 'expired' }); + const clientUat = claims.iat; + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `__client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + 'X-Proxy-Url': 'https://example.com/clerk', + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://example.com/clerk/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2F`, + ); +}); + +test('Test domain - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token, claims } = config.generateToken({ state: 'expired' }); + const clientUat = claims.iat; + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + 'X-Domain': 'localhost:3000', + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://${config.pkHost}/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2F${devBrowserQuery}`, + ); +}); + +test('Test domain - prod', async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token, claims } = config.generateToken({ state: 'expired' }); + const clientUat = claims.iat; + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `__client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + 'X-Domain': 'example.com', + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://clerk.example.com/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2F`, + ); +}); + +test('Test missing session token, positive uat - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=1`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://${config.pkHost}/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2F${devBrowserQuery}`, + ); +}); + +test('Test missing session token, positive uat - prod', async () => { + const config = generateConfig({ + mode: 'live', + }); + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `__client_uat=1`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://${config.pkHost}/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2F`, + ); +}); + +test('Test missing session token, 0 uat (indicating signed out) - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=0`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); +}); + +test('Test missing session token, 0 uat (indicating signed out) - prod', async () => { + const config = generateConfig({ + mode: 'live', + }); + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `__client_uat=0`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); +}); + +test('Test missing session token, missing uat (indicating signed out) - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const res = await fetch(url + '/', { + headers: new Headers({ + Cookie: `${devBrowserCookie}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); +}); + +test('Test missing session token, missing uat (indicating signed out) - prod', async () => { + const config = generateConfig({ + mode: 'live', + }); + const res = await fetch(url + '/', { + headers: new Headers({ + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); +}); + +test('Test missing session token, missing uat (indicating signed out), missing devbrowser - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const res = await fetch(url + '/', { + headers: new Headers({ + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); +}); + +test('Test redirect url - path and qs - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token, claims } = config.generateToken({ state: 'expired' }); + const clientUat = claims.iat; + const res = await fetch(url + '/hello?foo=bar', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://${config.pkHost}/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2Fhello%3Ffoo%3Dbar${devBrowserQuery}`, + ); +}); + +test('Test redirect url - path and qs - prod', async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token, claims } = config.generateToken({ state: 'expired' }); + const clientUat = claims.iat; + const res = await fetch(url + '/hello?foo=bar', { + headers: new Headers({ + Cookie: `__client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://${config.pkHost}/v1/client/handshake?redirect_url=http%3A%2F%2Flocalhost%3A4011%2Fhello%3Ffoo%3Dbar`, + ); +}); + +test('Test redirect url - proxy - dev', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token, claims } = config.generateToken({ state: 'expired' }); + const clientUat = claims.iat; + const res = await fetch(url + '/hello?foo=bar', { + headers: new Headers({ + Cookie: `${devBrowserCookie} __client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + 'X-Forwarded-Host': 'example.com', + 'X-Forwarded-Proto': 'https', + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://${config.pkHost}/v1/client/handshake?redirect_url=https%3A%2F%2Fexample.com%3A4011%2Fhello%3Ffoo%3Dbar${devBrowserQuery}`, + ); +}); + +test('Test redirect url - proxy - prod', async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token, claims } = config.generateToken({ state: 'expired' }); + const clientUat = claims.iat; + const res = await fetch(url + '/hello?foo=bar', { + headers: new Headers({ + Cookie: `__client_uat=${clientUat}; __session=${token}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + 'X-Forwarded-Host': 'example.com', + 'X-Forwarded-Proto': 'https', + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe( + `https://${config.pkHost}/v1/client/handshake?redirect_url=https%3A%2F%2Fexample.com%3A4011%2Fhello%3Ffoo%3Dbar`, + ); +}); + +test('Handshake result - dev - nominal', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token } = config.generateToken({ state: 'active' }); + const cookiesToSet = [`__session=${token};path=/`, 'foo=bar;path=/;domain=example.com']; + const handshake = btoa(JSON.stringify(cookiesToSet)); + const res = await fetch(url + '/?__clerk_handshake=' + handshake, { + headers: new Headers({ + Cookie: `${devBrowserCookie}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe('/'); + const headers = [...res.headers.entries()]; + cookiesToSet.forEach(cookie => { + expect(headers).toContainEqual(['set-cookie', cookie]); + }); +}); + +test('Handshake result - dev - skew - clock behind', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token } = config.generateToken({ state: 'early' }); + const cookiesToSet = [`__session=${token};path=/`, 'foo=bar;path=/;domain=example.com']; + const handshake = btoa(JSON.stringify(cookiesToSet)); + const res = await fetch(url + '/?__clerk_handshake=' + handshake, { + headers: new Headers({ + Cookie: `${devBrowserCookie}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(500); +}); + +test('Handshake result - dev - skew - clock ahead', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token } = config.generateToken({ state: 'expired' }); + const cookiesToSet = [`__session=${token};path=/`, 'foo=bar;path=/;domain=example.com']; + const handshake = btoa(JSON.stringify(cookiesToSet)); + const res = await fetch(url + '/?__clerk_handshake=' + handshake, { + headers: new Headers({ + Cookie: `${devBrowserCookie}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(500); +}); + +test('Handshake result - dev - mismatched keys', async () => { + const config = generateConfig({ + mode: 'test', + matchedKeys: false, + }); + const { token } = config.generateToken({ state: 'early' }); + const cookiesToSet = [`__session=${token};path=/`, 'foo=bar;path=/;domain=example.com']; + const handshake = btoa(JSON.stringify(cookiesToSet)); + const res = await fetch(url + '/?__clerk_handshake=' + handshake, { + headers: new Headers({ + Cookie: `${devBrowserCookie}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(500); +}); + +// I don't know if we need this one? We might pass new devbrowser back in handshake +test('Handshake result - dev - new devbrowser', async () => { + const config = generateConfig({ + mode: 'test', + }); + const { token } = config.generateToken({ state: 'active' }); + const cookiesToSet = [`__session=${token};path=/`, 'foo=bar;path=/;domain=example.com']; + const handshake = btoa(JSON.stringify(cookiesToSet)); + const res = await fetch(url + '/?__clerk_handshake=' + handshake + '&__clerk_db_jwt=asdf', { + headers: new Headers({ + Cookie: `${devBrowserCookie}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + expect(res.headers.get('location')).toBe('/'); + const headers = [...res.headers.entries()]; + cookiesToSet.forEach(cookie => { + expect(headers).toContainEqual(['set-cookie', cookie]); + }); + console.log(entries); + expect(headers).toContainEqual(['set-cookie', '__clerk_db_jwt=asdf']); +}); + +test('External visit - new devbrowser', async () => { + const config = generateConfig({ + mode: 'test', + }); + const res = await fetch(url + '/?__clerk_db_jwt=asdf', { + headers: new Headers({ + Cookie: `${devBrowserCookie}`, + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + }), + redirect: 'manual', + }); + expect(res.status).toBe(307); + // expect(res.headers.get('location')).toBe('/'); + const headers = [...res.headers.entries()]; + console.log(headers); + // Set cookie should look bigger than this + expect(headers).toContainEqual(['set-cookie', '__clerk_db_jwt=asdf']); +}); + +test('Handshake result - prod - nominal', async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token } = config.generateToken({ state: 'active' }); + const cookiesToSet = [`__session=${token};path=/`, 'foo=bar;path=/;domain=example.com']; + const handshake = btoa(JSON.stringify(cookiesToSet)); + const res = await fetch(url + '/', { + headers: new Headers({ + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + Cookie: `__clerk_handshake=${handshake}`, + }), + redirect: 'manual', + }); + expect(res.status).toBe(200); + const headers = [...res.headers.entries()]; + cookiesToSet.forEach(cookie => { + expect(headers).toContainEqual(['set-cookie', cookie]); + }); +}); + +test('Handshake result - prod - skew - clock behind', async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token } = config.generateToken({ state: 'early' }); + const cookiesToSet = [`__session=${token};path=/`, 'foo=bar;path=/;domain=example.com']; + const handshake = btoa(JSON.stringify(cookiesToSet)); + const res = await fetch(url + '/', { + headers: new Headers({ + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + Cookie: `__clerk_handshake=${handshake}`, + }), + redirect: 'manual', + }); + expect(res.status).toBe(500); +}); + +test('Handshake result - prod - skew - clock ahead', async () => { + const config = generateConfig({ + mode: 'live', + }); + const { token } = config.generateToken({ state: 'expired' }); + const cookiesToSet = [`__session=${token};path=/`, 'foo=bar;path=/;domain=example.com']; + const handshake = btoa(JSON.stringify(cookiesToSet)); + const res = await fetch(url + '/', { + headers: new Headers({ + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + Cookie: `__clerk_handshake=${handshake}`, + }), + redirect: 'manual', + }); + expect(res.status).toBe(500); +}); + +test('Handshake result - prod - mismatched keys', async () => { + const config = generateConfig({ + mode: 'live', + matchedKeys: false, + }); + const { token } = config.generateToken({ state: 'early' }); + const cookiesToSet = [`__session=${token};path=/`, 'foo=bar;path=/;domain=example.com']; + const handshake = btoa(JSON.stringify(cookiesToSet)); + const res = await fetch(url + '/', { + headers: new Headers({ + 'X-Publishable-Key': config.pk, + 'X-Secret-Key': config.sk, + Cookie: `__clerk_handshake=${handshake}`, + }), + redirect: 'manual', + }); + expect(res.status).toBe(500); +}); diff --git a/handshakeTestConfigs.ts b/handshakeTestConfigs.ts new file mode 100644 index 00000000000..124f7d0727b --- /dev/null +++ b/handshakeTestConfigs.ts @@ -0,0 +1,145 @@ +// @ts-ignore ignore types +import * as jwt from 'jsonwebtoken'; +// @ts-ignore ignore types +import * as uuid from 'uuid'; + +const rsaPairs = { + a: { + private: `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAlRVgJiQJ0nfuctIVSLnFJlAC76YPKly8Y5xrY36ADo472G1w +FpeiykQRyDdGOwrkJBEVmLpAybV4yTgQFpQ0A4YzeDlKKkOxBhCmuANZXluAm2MW +3ehNAm0svievMfKtG6UjjYz6v67U9Om/oMt1ehsOmR8MrDYvs3Wy+dxYpZaxyn6w +ajL7GkICHxc8cGsI/MBZr9jKtKyzFY++r8TQKAJwn9TcSQljRivomz1wQvjtdLnq +ZSLP3BFQB7e7DuM6SBsodIHhkVEVK2EaGVLOY+ifAITt7MqEcvast14AP0rICBSq +vbQQjZuwLgIrlJgqvJ4YBRfIaIx/qQzs0+eFtwIDAQABAoIBAD1H4xTqfWsZR1fF +SWBylDqSaxKNRPCZ3ApqEq58IjFZf/oPyiJPRGg2IMUXC3RbnrnAmAsGjHkdcj/s +HpjZZKQKNv/1NKo41vxyPcWoAsVJgYzd51liEr2rmNe1QkuawFN7xyh5Sd0fBYSC +zPVQjMKbep2waKolP+hZui8AxyORLtu6aUQawaCdWFyiyHtqEnlcb/YSTtGl/3W/ +/LqYyv60dG0QdcAO37MAE42vp3R4GGcJelsFo/lxSKg+KiLn7NdsNr7bCJrqbVXz +93Fu5jgHQD9+BeVyvHJ/R2yg+utYEvIMiFvwX7z4MLh9PsWJbf9vbDNlw9ErWpf5 +r1xUiqECgYEA/lLJP+qla0vd+ocYNe3ufOG4kaUFsrqRoChiS1JxwQr/WGTmV8sT +ZyTPwyxnsHtbzn4lwuI6CpAeyvd9O6G3FfTzUqsyPaknsGlymf8LEwL4AVo0BVY0 +YGodRnDISBBU/yPQ2kvq6c72ouq5cQxWF45f8Z/Z+fFDjuHG6Q44hYcCgYEAlhD6 +sm8wTWVklMAxOnhQJoseWQ1vcl6VCxRVv4QBiX9CQm/4ANVOWun4KRC5qqTdoNHc +RyuiWpZVgGblqUu4sWSQgi3CZyyLbHOJ9wTPTeo0oDVaFa9MMwS8rq35HXjpgREz +JtTRi6c9WVsjBygYiE5IYO0FGbEjI9qIiD5CClECgYA+wtVRRamu0dkk0yPhYycg +gF+Y6Z1/XtVDLdQb/GuAFSOwf63sanwOTyJKavHntnmQesb80fE63BgNRIgOKDlT +XNCTTRYn60+VFGCoqizkcy4av1TpID3qsSUqVfjG9+jR0dffly6Qpnds+vnqcP3p +8EOzEByttqFSaFs69jxyjwKBgFCQbQa+isACXy08wTESxnTq2zAT9nEANiPsltxq +kiivGXNxiUNpQNeuJHxnbkYenJ1qDUhoNJFNhDmbBFEPReh2hN5ekq+xSmi+3qKv +AlxiED6yZdqecdoyANoGrGcWMsYH5d5DAvxmnJkMRJHjBMiovlLK7KIOZz8oY4RB +aFMBAoGBAJ8UoGHwz7AdOLAldGY3c0HzaH1NaGrZoocSH19XI3ZYr8Qvl2XUww9G +UC1OG4e2PtZ8PINQGIYPacUaab5Yg1tOmxBoAx4gUkpgyjtSm2ZPd4EUVOdylU3E +aFa08+0FF7mqqJTgz5XlvHMrCcUTsJ9u+e05rr1G1PHsATuuMD9m +-----END RSA PRIVATE KEY-----`, + public: { + kty: 'RSA', + n: 'lRVgJiQJ0nfuctIVSLnFJlAC76YPKly8Y5xrY36ADo472G1wFpeiykQRyDdGOwrkJBEVmLpAybV4yTgQFpQ0A4YzeDlKKkOxBhCmuANZXluAm2MW3ehNAm0svievMfKtG6UjjYz6v67U9Om_oMt1ehsOmR8MrDYvs3Wy-dxYpZaxyn6wajL7GkICHxc8cGsI_MBZr9jKtKyzFY--r8TQKAJwn9TcSQljRivomz1wQvjtdLnqZSLP3BFQB7e7DuM6SBsodIHhkVEVK2EaGVLOY-ifAITt7MqEcvast14AP0rICBSqvbQQjZuwLgIrlJgqvJ4YBRfIaIx_qQzs0-eFtw', + e: 'AQAB', + }, + }, + b: { + private: `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAt9zSYl1hFhFKXvv8uJcT2X15iOqi1mTtxqVxNDnzPQSj1RSa +Jryjhkzpyd16c+PDo+FFtMgZTUv6Z2hr5QYMuAjlsM+apHmfE8MRMQRQHXNF0+sE +Bd1241W0mL7fId2ZChaGgufFOGFl2Obby56FH4Z86lCFi7Z4Ow7TBSpVSN598OKH +oVKwbYOVPKtmWBar0JeCPVpng4Ntx7kvuHGdFSoJ8z8+Uy5ybLlk1qSlQ5lsymfW +hxs0C9j7/x/h24n9jUbq51pzx2URcsEi0Wbuv26Ba0Q4v1ySl0I6IM5Xemwrzjo1 +H6kz6IYldqPtTwkzhJSnJvJFzKJZn3hH9N+rGwIDAQABAoIBAGLGdx/xGp9IWrP8 +nCBuyXMmPYyYwTJ8tmDpsI9mMo6tV3a5wrbc0NztpQuVuJtZ2VjJRTGB7lXgY336 +UzyOq3aTERKT9Xg2/ocXXL0AnCm2K+VVdKvR9nTbLlKA+E6xRe5te4YIDaPkb1q/ +a4VQfCQblDAtYhFUzfKsXCGCRJ8IPlhZxiA9RHfQTmQUSoBW+12IovyMdMxVLoPT +qjdnwL1TS3iARim+eV+buHW+8Drn8eldeSFoTJd0B9eRf7pMpRH/X8G7X0YYdDjF +ADWI770CQj45QQeuVsZYIuONIPmzai4nGiNQ85v+Yy0L47lYUp5XsDvwYO5tMCQK +v1og8cECgYEA7zIfBWM1AIY763FGJ6F5ym41A4i7WSRaWusfQx6fOdGTyHJ3hXu9 ++1kQS97IKElJ1V7oK69dJGxq+aupsd/AaRJb4oVZCBSby4Fo6VeJoKyJSdNSCks6 +tonT3hGUsJO1ER2ItWcgiCxgGY+vrK0rkacX/VgNZKGIjlGv8pQUpaUCgYEAxMeL +2jyx+5odGM51e/1G4Vn6t+ffNNC/03NbwZeJ93+0NPgdagIo1kErZQrFPEfoExOr +KMkwnAsnR/xfn4X21voK1pUc7VhzzoODb8l9LA9kB7efWtRZA79gcsbOH5wNkp9Z +i76AtaVU/p1grFKNcnes1lbFfcRUnO880g5dsb8CgYBacuuEEAWk0x2pZEYRCmCR +iacGVRfzF2oLY0mJCfVP2c42SAKmOSqX9w/QgMfTZBNFWgQVMNTZxx2Ul7Mtjdym +XsjcGWyXP6PCCodvZSin11Z60iv9tIDZMbkqCh/dvZ0EgdSGNB77HzyfrdPSShFl +nHfX1woJeYO3vW/5HMHJ+QKBgQCNema7pq3Ulq5a2n2vgp9GgJn5RXW+lGOG1Mbg +vmJMlv1qpAUJ5bmUqdBYWlEKkSxzIs4JifUwC/jXEcVyfS/GyommVBkzMEg672U9 +pyEe34Xs4oFpHYlOX3cprnQeV+WOSJFqHrKNZuxgD6ik3MmjxhV3GXXugYzQNFWH +NRr6IwKBgH9aN5mY4fcVL76mMEVZ5BIHE+JpPMZ6OOamOHAiA5jrWRX4aRMICq3t +cKVfcj/M4dyBuRV5EW1y1m2QhRECFPSKpScykpD9nyCb+XqbMSLH+f+j1BGfLKWl +t5o8u/dlwJ1fGGday48gs/hA4V/F9zDjecNkYWUB/wUwVStqZljn +-----END RSA PRIVATE KEY-----`, + public: { + kty: 'RSA', + n: 't9zSYl1hFhFKXvv8uJcT2X15iOqi1mTtxqVxNDnzPQSj1RSaJryjhkzpyd16c-PDo-FFtMgZTUv6Z2hr5QYMuAjlsM-apHmfE8MRMQRQHXNF0-sEBd1241W0mL7fId2ZChaGgufFOGFl2Obby56FH4Z86lCFi7Z4Ow7TBSpVSN598OKHoVKwbYOVPKtmWBar0JeCPVpng4Ntx7kvuHGdFSoJ8z8-Uy5ybLlk1qSlQ5lsymfWhxs0C9j7_x_h24n9jUbq51pzx2URcsEi0Wbuv26Ba0Q4v1ySl0I6IM5Xemwrzjo1H6kz6IYldqPtTwkzhJSnJvJFzKJZn3hH9N-rGw', + e: 'AQAB', + }, + }, +}; + +const allConfigs: any = []; + +export function generateConfig({ mode, matchedKeys = true }: { mode: 'test' | 'live'; matchedKeys?: boolean }) { + const ins_id = uuid.v4(); + const pkHost = `clerk.${uuid.v4()}.com`; + const pk = `pk_${mode}_${btoa(`${pkHost}$`)}`; + const sk = `sk_${mode}_${uuid.v4()}`; + const rsa = matchedKeys + ? rsaPairs.a + : { + private: rsaPairs.a.private, + public: rsaPairs.b.public, + }; + const jwks = { + keys: [ + { + ...rsa.public, + kid: ins_id, + use: 'sig', + alg: 'RS256', + }, + ], + }; + + type Claims = { + sub: string; + iat: number; + exp: number; + nbf: number; + }; + const generateToken = ({ state }: { state: 'active' | 'expired' | 'early' }) => { + let claims = { sub: 'user_12345' } as Claims; + + const now = Math.floor(Date.now() / 1000); + if (state === 'active') { + claims.iat = now; + claims.nbf = now - 10; + claims.exp = now + 60; + } else if (state === 'expired') { + claims.iat = now - 600; + claims.nbf = now - 10 - 600; + claims.exp = now + 60 - 600; + } else if (state === 'early') { + claims.iat = now + 600; + claims.nbf = now - 10 + 600; + claims.exp = now + 60 + 600; + } + return { + token: jwt.sign(claims, rsa.private, { + algorithm: 'RS256', + header: { kid: ins_id }, + }), + claims, + }; + }; + const config = Object.freeze({ + pk, + sk, + generateToken, + jwks, + pkHost, + }); + allConfigs.push(config); + return config; +} + +export function getJwksFromSecretKey(sk: any) { + return allConfigs.find((x: any) => x.sk === sk)?.jwks; +} diff --git a/integration/templates/next-app-router/package.json b/integration/templates/next-app-router/package.json index 5e298c001bd..eef4565f52c 100644 --- a/integration/templates/next-app-router/package.json +++ b/integration/templates/next-app-router/package.json @@ -9,6 +9,11 @@ "start": "next start" }, "dependencies": { + "@clerk/backend": "file:.yalc/@clerk/backend", + "@clerk/clerk-react": "file:.yalc/@clerk/clerk-react", + "@clerk/nextjs": "file:.yalc/@clerk/nextjs", + "@clerk/shared": "file:.yalc/@clerk/shared", + "@clerk/types": "file:.yalc/@clerk/types", "@types/node": "^18.17.0", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", diff --git a/jest.config.handshake.js b/jest.config.handshake.js new file mode 100644 index 00000000000..e5897f4fe9e --- /dev/null +++ b/jest.config.handshake.js @@ -0,0 +1,19 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + extensionsToTreatAsEsm: ['.ts'], + testRegex: ['handshake.test.tsx?$'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + transform: { + // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` + // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` + '^.+\\.tsx?$': [ + 'ts-jest', + { + diagnostics: false, + useESM: true, + }, + ], + }, +}; diff --git a/packages/backend/src/tokens/interstitialRule.ts b/packages/backend/src/tokens/interstitialRule.ts index cfc92670081..b28f2b2d53c 100644 --- a/packages/backend/src/tokens/interstitialRule.ts +++ b/packages/backend/src/tokens/interstitialRule.ts @@ -171,17 +171,7 @@ export async function runInterstitialRules( } async function verifyRequestState(options: InterstitialRuleOptions, token: string) { - const { isSatellite, proxyUrl } = options; - let issuer; - if (isSatellite) { - issuer = null; - } else if (proxyUrl) { - issuer = proxyUrl; - } else { - issuer = (iss: string) => iss.startsWith('https://clerk.') || iss.includes('.clerk.accounts'); - } - - return verifyToken(token, { ...options, issuer }); + return verifyToken(token, { ...options }); } /** diff --git a/packages/backend/src/tokens/jwt/verifyJwt.ts b/packages/backend/src/tokens/jwt/verifyJwt.ts index 874d616210b..2fd22c53c40 100644 --- a/packages/backend/src/tokens/jwt/verifyJwt.ts +++ b/packages/backend/src/tokens/jwt/verifyJwt.ts @@ -6,7 +6,6 @@ import runtime from '../../runtime'; import { base64url } from '../../util/rfc4648'; import { TokenVerificationError, TokenVerificationErrorAction, TokenVerificationErrorReason } from '../errors'; import { getCryptoAlgorithm } from './algorithms'; -import type { IssuerResolver } from './assertions'; import { assertActivationClaim, assertAudienceClaim, @@ -15,7 +14,6 @@ import { assertHeaderAlgorithm, assertHeaderType, assertIssuedAtClaim, - assertIssuerClaim, assertSubClaim, } from './assertions'; import { importKey } from './cryptoKeys'; @@ -82,13 +80,12 @@ export type VerifyJwtOptions = { audience?: string | string[]; authorizedParties?: string[]; clockSkewInMs?: number; - issuer: IssuerResolver | string | null; key: JsonWebKey | string; }; export async function verifyJwt( token: string, - { audience, authorizedParties, clockSkewInMs, issuer, key }: VerifyJwtOptions, + { audience, authorizedParties, clockSkewInMs, key }: VerifyJwtOptions, ): Promise { const clockSkew = clockSkewInMs || DEFAULT_CLOCK_SKEW_IN_SECONDS; @@ -108,7 +105,6 @@ export async function verifyJwt( assertSubClaim(sub); assertAudienceClaim([aud], [audience]); assertAuthorizedPartiesClaim(azp, authorizedParties); - assertIssuerClaim(iss, issuer); assertExpirationClaim(exp, clockSkew); assertActivationClaim(nbf, clockSkew); assertIssuedAtClaim(iat, clockSkew); diff --git a/packages/backend/src/tokens/keys.ts b/packages/backend/src/tokens/keys.ts index 41030f2287f..972d10fad12 100644 --- a/packages/backend/src/tokens/keys.ts +++ b/packages/backend/src/tokens/keys.ts @@ -98,7 +98,6 @@ export type LoadClerkJWKFromRemoteOptions = { secretKey?: string; apiUrl?: string; apiVersion?: string; - issuer?: string; }; /** @@ -108,7 +107,6 @@ export type LoadClerkJWKFromRemoteOptions = { * The cache lasts 1 hour by default. * * @param {Object} options - * @param {string} options.issuer - The issuer origin of the JWT * @param {string} options.kid - The id of the key that the JWT was signed with * @param {string} options.alg - The algorithm of the JWT * @param {number} options.jwksCacheTtlInMs - The TTL of the jwks cache (defaults to 1 hour) @@ -118,27 +116,20 @@ export async function loadClerkJWKFromRemote({ secretKey, apiUrl = API_URL, apiVersion = API_VERSION, - issuer, kid, jwksCacheTtlInMs = JWKS_CACHE_TTL_MS, skipJwksCache, }: LoadClerkJWKFromRemoteOptions): Promise { - const shouldRefreshCache = !getFromCache(kid) && reachedMaxCacheUpdatedAt(); - if (skipJwksCache || shouldRefreshCache) { - let fetcher; - - if (secretKey) { - fetcher = () => fetchJWKSFromBAPI(apiUrl, secretKey, apiVersion); - } else if (issuer) { - fetcher = () => fetchJWKSFromFAPI(issuer); - } else { + const needsFetch = !getFromCache(kid) || cacheHasExpired(); + if (skipJwksCache || needsFetch) { + if (!secretKey) { throw new TokenVerificationError({ action: TokenVerificationErrorAction.ContactSupport, message: 'Failed to load JWKS from Clerk Backend or Frontend API.', reason: TokenVerificationErrorReason.RemoteJWKFailedToLoad, }); } - + const fetcher = () => fetchJWKSFromBAPI(apiUrl, secretKey, apiVersion); const { keys } = await callWithRetry<{ keys: JsonWebKeyWithKid[] }>(fetcher); if (!keys || !keys.length) { @@ -231,6 +222,6 @@ async function fetchJWKSFromBAPI(apiUrl: string, key: string, apiVersion: string return response.json(); } -function reachedMaxCacheUpdatedAt() { +function cacheHasExpired() { return Date.now() - lastUpdatedAt >= MAX_CACHE_LAST_UPDATED_AT_SECONDS * 1000; } diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 81b7fdb4fb4..c3b1b26e696 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -95,8 +95,9 @@ export async function authenticateRequest(options: AuthenticateRequestOptions): const pkFapi = pk?.frontendApi || ''; // determine proper FAPI url, taking into account multi-domain setups const frontendApi = proxyUrl || (!isDevOrStagingUrl(pkFapi) ? addClerkPrefix(domain) : '') || pkFapi; + const frontendApiNoProtocol = frontendApi.replace(/http(s)?:\/\//, ''); - const url = new URL(`https://${frontendApi}/v1/client/handshake`); + const url = new URL(`https://${frontendApiNoProtocol}/v1/client/handshake`); url.searchParams.append('redirect_url', redirectUrl); if (pk?.instanceType === 'development' && devBrowserToken) { @@ -106,17 +107,7 @@ export async function authenticateRequest(options: AuthenticateRequestOptions): } async function verifyRequestState(options: InterstitialRuleOptions, token: string) { - const { isSatellite, proxyUrl } = options; - let issuer; - if (isSatellite) { - issuer = null; - } else if (proxyUrl) { - issuer = proxyUrl; - } else { - issuer = (iss: string) => iss.startsWith('https://clerk.') || iss.includes('.clerk.accounts'); - } - - return verifyToken(token, { ...options, issuer }); + return verifyToken(token, options); } const clientUat = parseInt(ruleOptions.clientUat || '', 10) || 0; @@ -196,6 +187,8 @@ export async function authenticateRequest(options: AuthenticateRequestOptions): buildRedirectToHandshake({ publishableKey: ruleOptions.publishableKey!, devBrowserToken: ruleOptions.devBrowserToken!, + proxyUrl: ruleOptions.proxyUrl, + domain: ruleOptions.domain, redirectUrl: ruleOptions.request.url.toString(), }), ); @@ -211,6 +204,8 @@ export async function authenticateRequest(options: AuthenticateRequestOptions): buildRedirectToHandshake({ publishableKey: ruleOptions.publishableKey!, devBrowserToken: ruleOptions.devBrowserToken!, + proxyUrl: ruleOptions.proxyUrl, + domain: ruleOptions.domain, redirectUrl: ruleOptions.request.url.toString(), }), ); @@ -228,6 +223,8 @@ export async function authenticateRequest(options: AuthenticateRequestOptions): buildRedirectToHandshake({ publishableKey: ruleOptions.publishableKey!, devBrowserToken: ruleOptions.devBrowserToken!, + proxyUrl: ruleOptions.proxyUrl, + domain: ruleOptions.domain, redirectUrl: ruleOptions.request.url.toString(), }), ); @@ -257,6 +254,8 @@ export async function authenticateRequest(options: AuthenticateRequestOptions): buildRedirectToHandshake({ publishableKey: ruleOptions.publishableKey!, devBrowserToken: ruleOptions.devBrowserToken!, + proxyUrl: ruleOptions.proxyUrl, + domain: ruleOptions.domain, redirectUrl: ruleOptions.request.url.toString(), }), ); diff --git a/packages/backend/src/tokens/verify.ts b/packages/backend/src/tokens/verify.ts index dafe984910f..9801ed30e42 100644 --- a/packages/backend/src/tokens/verify.ts +++ b/packages/backend/src/tokens/verify.ts @@ -9,13 +9,9 @@ import { loadClerkJWKFromLocal, loadClerkJWKFromRemote } from './keys'; /** * */ -export type VerifyTokenOptions = Pick< - VerifyJwtOptions, - 'authorizedParties' | 'audience' | 'issuer' | 'clockSkewInMs' -> & { jwtKey?: string; proxyUrl?: string } & Pick< - LoadClerkJWKFromRemoteOptions, - 'secretKey' | 'apiUrl' | 'apiVersion' | 'jwksCacheTtlInMs' | 'skipJwksCache' - >; +export type VerifyTokenOptions = Pick & { + jwtKey?: string; +} & Pick; export async function verifyToken(token: string, options: VerifyTokenOptions): Promise { const { @@ -25,7 +21,6 @@ export async function verifyToken(token: string, options: VerifyTokenOptions): P audience, authorizedParties, clockSkewInMs, - issuer, jwksCacheTtlInMs, jwtKey, skipJwksCache, @@ -38,9 +33,6 @@ export async function verifyToken(token: string, options: VerifyTokenOptions): P if (jwtKey) { key = loadClerkJWKFromLocal(jwtKey); - } else if (typeof issuer === 'string') { - // Fetch JWKS from Frontend API if an issuer of type string has been provided - key = await loadClerkJWKFromRemote({ issuer, kid, jwksCacheTtlInMs, skipJwksCache }); } else if (secretKey) { // Fetch JWKS from Backend API using the key key = await loadClerkJWKFromRemote({ secretKey, apiUrl, apiVersion, kid, jwksCacheTtlInMs, skipJwksCache }); @@ -57,6 +49,5 @@ export async function verifyToken(token: string, options: VerifyTokenOptions): P authorizedParties, clockSkewInMs, key, - issuer, }); } diff --git a/playground/nextjs/middleware.ts b/playground/nextjs/middleware.ts index 7de4bebaffa..c33e2ed3863 100644 --- a/playground/nextjs/middleware.ts +++ b/playground/nextjs/middleware.ts @@ -3,9 +3,17 @@ import { authMiddleware } from '@clerk/nextjs/server'; // Set the paths that don't require the user to be signed in const publicPaths = ['/', /^(?!\/(sign-in|sign-up|app-dir|custom)\/*).*$/]; -export default authMiddleware({ - publicRoutes: publicPaths, -}); +export const middleware = (req, evt) => { + console.log("pk", req.headers.get("x-publishable-key")); + return authMiddleware({ + publicRoutes: publicPaths, + publishableKey: req.headers.get("x-publishable-key"), + secretKey: req.headers.get("x-secret-key"), + proxyUrl: req.headers.get("x-proxy-url"), + domain: req.headers.get("x-domain"), + debug: true + })(req, evt) +}; export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], diff --git a/playground/nextjs/package.json b/playground/nextjs/package.json index 4c304acd971..f2d9cf5c3d5 100644 --- a/playground/nextjs/package.json +++ b/playground/nextjs/package.json @@ -9,13 +9,13 @@ "lint": "next lint" }, "dependencies": { - "@clerk/backend": "latest", - "@clerk/clerk-react": "latest", + "@clerk/backend": "file:.yalc/@clerk/backend", + "@clerk/clerk-react": "file:.yalc/@clerk/clerk-react", "@clerk/clerk-sdk-node": "latest", - "@clerk/nextjs": "latest", - "@clerk/shared": "latest", + "@clerk/nextjs": "file:.yalc/@clerk/nextjs", + "@clerk/shared": "file:.yalc/@clerk/shared", "@clerk/themes": "latest", - "@clerk/types": "latest", + "@clerk/types": "file:.yalc/@clerk/types", "next": "^13.5.6", "react": "^18.2.0", "react-dom": "^18.2.0"