From cc3e3634ff9ce7f43a2f2a170d230f5694b578db Mon Sep 17 00:00:00 2001 From: Michael Chan Date: Mon, 8 Jul 2024 19:55:11 -0700 Subject: [PATCH] add mvp auth --- chan.dev/package.json | 3 + chan.dev/pnpm-lock.yaml | 47 ++++++ chan.dev/src/content/posts/authkit-astro.md | 178 ++++++++++++++++++++ chan.dev/src/pages/auth/callback.js | 33 ++++ chan.dev/src/pages/auth/index.js | 17 ++ chan.dev/src/pages/user.astro | 41 +++++ 6 files changed, 319 insertions(+) create mode 100644 chan.dev/src/content/posts/authkit-astro.md create mode 100644 chan.dev/src/pages/auth/callback.js create mode 100644 chan.dev/src/pages/auth/index.js create mode 100644 chan.dev/src/pages/user.astro diff --git a/chan.dev/package.json b/chan.dev/package.json index 6d02b380..fdb9882c 100644 --- a/chan.dev/package.json +++ b/chan.dev/package.json @@ -54,6 +54,9 @@ "@astrojs/react": "^3.6.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@workos-inc/node": "^7.12.0", + "iron-session": "^8.0.2", + "jose": "^5.6.3", "react": "^18.3.1", "react-dom": "^18.3.1", "sharp": "0.33.4" diff --git a/chan.dev/pnpm-lock.yaml b/chan.dev/pnpm-lock.yaml index 0414439a..a2a28131 100644 --- a/chan.dev/pnpm-lock.yaml +++ b/chan.dev/pnpm-lock.yaml @@ -23,6 +23,15 @@ importers: '@types/react-dom': specifier: ^18.3.0 version: 18.3.0 + '@workos-inc/node': + specifier: ^7.12.0 + version: 7.12.0 + iron-session: + specifier: ^8.0.2 + version: 8.0.2 + jose: + specifier: ^5.6.3 + version: 5.6.3 react: specifier: ^18.3.1 version: 18.3.1 @@ -1276,6 +1285,10 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 + '@workos-inc/node@7.12.0': + resolution: {integrity: sha512-9ycVeLjMW73WFQBdizn6dxkEdgblJTB4gz4oHdkDerYbHkdnzkOvzvfJXo6bwu+rjbTQUZoHQnG6nfOOVeBj+g==} + engines: {node: '>=16'} + acorn-walk@8.3.3: resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} engines: {node: '>=0.4.0'} @@ -2045,6 +2058,12 @@ packages: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} + iron-session@8.0.2: + resolution: {integrity: sha512-p4Yf1moQr6gnCcXu5vCaxVKRKDmR9PZcQDfp7ZOgbsSHUsgaNti6OgDB2BdgxC2aS6V/6Hu4O0wYlj92sbdIJg==} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-absolute-url@4.0.1: resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2148,6 +2167,9 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + jose@5.6.3: + resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==} + js-base64@3.7.7: resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} @@ -2800,6 +2822,10 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -3264,6 +3290,9 @@ packages: ultrahtml@1.5.3: resolution: {integrity: sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==} + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -4536,6 +4565,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@workos-inc/node@7.12.0': + dependencies: + pluralize: 8.0.0 + acorn-walk@8.3.3: dependencies: acorn: 8.12.0 @@ -5399,6 +5432,14 @@ snapshots: jsbn: 1.1.0 sprintf-js: 1.1.3 + iron-session@8.0.2: + dependencies: + cookie: 0.6.0 + iron-webcrypto: 1.2.1 + uncrypto: 0.1.3 + + iron-webcrypto@1.2.1: {} + is-absolute-url@4.0.1: {} is-alphabetical@2.0.1: {} @@ -5470,6 +5511,8 @@ snapshots: jiti@1.21.6: {} + jose@5.6.3: {} + js-base64@3.7.7: {} js-tokens@4.0.0: {} @@ -6439,6 +6482,8 @@ snapshots: dependencies: find-up: 4.1.0 + pluralize@8.0.0: {} + postcss-import@15.1.0(postcss@8.4.39): dependencies: postcss: 8.4.39 @@ -7020,6 +7065,8 @@ snapshots: ultrahtml@1.5.3: {} + uncrypto@0.1.3: {} + undici-types@5.26.5: {} undici@5.28.4: diff --git a/chan.dev/src/content/posts/authkit-astro.md b/chan.dev/src/content/posts/authkit-astro.md new file mode 100644 index 00000000..fd4eba01 --- /dev/null +++ b/chan.dev/src/content/posts/authkit-astro.md @@ -0,0 +1,178 @@ +--- +title: AuthKit withAstro +date: 2024-07-08 +references: + - https://workos.com/docs/user-management/1-configure-your-project/configure-a-redirect-uri +--- + +## install `@workos-inc/node` + +## add secrets + +- `WORKOS_API_KEY` to `.env`(.local) +- `WORKOS_CLIENT_ID` to `.env`(.local) +- (will also need to add to hosting provider) + +## add redirect endpoint. + +```js +export async function GET({params, redirect}) { + return redirect('https://workos.com', 302) +} +``` + +## return the url (not working yet) + +```js +import {WorkOS} from '@workos-inc/node' +// import type {APIRoute} from 'astro' + +const workos = new WorkOS(import.meta.env.WORKOS_API_KEY) +const client_id = import.meta.env.WORKOS_CLIENT_ID + +export async function GET({params, redirect}) { + const authorizationUrl = + workos.userManagement.getAuthorizationUrl({ + provider: 'authkit', + redirectUri: import.meta.env.WORKOS_REDIRECT_URI, + clientId: client_id, + }) + + return new Response(authorizationUrl) + // return redirect(authorizationUrl, 302) +} +``` + +## redirect to url + +```diff +- return new Response(authorizationUrl) ++ return redirect(authorizationUrl, 302) +``` + +## create callback endpoint + +create dynamic route that recieve authentication token + +```js +import {WorkOS} from '@workos-inc/node' + +export const prerender = false + +const workos = new WorkOS(import.meta.env.WORKOS_API_KEY) +const clientId = import.meta.env.WORKOS_CLIENT_ID + +export async function GET({params, request}) { + const code = new URL(request.url).searchParams.get('code') + + return new Response(code) +} +``` + +## exchange code for user object + +```js +import {WorkOS} from '@workos-inc/node' + +export const prerender = false + +const workos = new WorkOS(import.meta.env.WORKOS_API_KEY) +const clientId = import.meta.env.WORKOS_CLIENT_ID + +export async function GET({request, redirect}) { + const code = new URL(request.url).searchParams + .get('code') + ?.toString() + + const {user} = + await workos.userManagement.authenticateWithCode({ + code, + clientId, + }) + + console.log(user) + + return new Response(JSON.stringify(user)) + // return redirect('/') +} +``` + +## encrypt session and set cookie + +```js +import {WorkOS} from '@workos-inc/node' +import {sealData /_, unsealData_/} from 'iron-session' + +export const prerender = false + +const workos = new WorkOS(import.meta.env.WORKOS_API_KEY) +const clientId = import.meta.env.WORKOS_CLIENT_ID + +export async function GET({request, redirect, cookies}) { +const code = new URL(request.url).searchParams +.get('code') +?.toString() + + const {user, accessToken, refreshToken, impersonator} = + await workos.userManagement.authenticateWithCode({ + code, + clientId, + }) + + const encryptedSession = await sealData( + {accessToken, refreshToken, user, impersonator}, + {password: import.meta.env.WORKOS_COOKIE_PASSWORD} + ) + + cookies.set('wos-session', encryptedSession, { + path: '/', + httpOnly: true, + secure: true, + sameSite: 'lax', + }) + + return redirect('/user') + +} +``` + +## Read session in single view + +```astro +--- +import {unsealData} from 'iron-session' + +export const prerender = false + +const cookie = Astro.cookies.get('wos-session') + +if (!cookie) { + return Astro.redirect('/auth') +} + +const session = await unsealData(cookie.value, { + password: import.meta.env.WORKOS_COOKIE_PASSWORD, +}) +--- + +
{JSON.stringify(session, null, '\t')}
+``` + +## verify session in single view + +```js +const JWKS = createRemoteJWKSet( + new URL( + workos.userManagement.getJwksUrl( + import.meta.env.WORKOS_CLIENT_ID + ) + ) +) + +try { + await jwtVerify(session.accessToken, JWKS) +} catch (e) { + console.warn('Failed to verify session:', e) + return Astro.redirect('/auth') +} +``` diff --git a/chan.dev/src/pages/auth/callback.js b/chan.dev/src/pages/auth/callback.js new file mode 100644 index 00000000..87765888 --- /dev/null +++ b/chan.dev/src/pages/auth/callback.js @@ -0,0 +1,33 @@ +import {WorkOS} from '@workos-inc/node' +import {sealData /*, unsealData*/} from 'iron-session' + +export const prerender = false + +const workos = new WorkOS(import.meta.env.WORKOS_API_KEY) +const clientId = import.meta.env.WORKOS_CLIENT_ID + +export async function GET({request, redirect, cookies}) { + const code = new URL(request.url).searchParams + .get('code') + ?.toString() + + const {user, accessToken, refreshToken, impersonator} = + await workos.userManagement.authenticateWithCode({ + code, + clientId, + }) + + const encryptedSession = await sealData( + {accessToken, refreshToken, user, impersonator}, + {password: import.meta.env.WORKOS_COOKIE_PASSWORD} + ) + + cookies.set('wos-session', encryptedSession, { + path: '/', + httpOnly: true, + secure: true, + sameSite: 'lax', + }) + + return redirect('/user') +} diff --git a/chan.dev/src/pages/auth/index.js b/chan.dev/src/pages/auth/index.js new file mode 100644 index 00000000..accfdd59 --- /dev/null +++ b/chan.dev/src/pages/auth/index.js @@ -0,0 +1,17 @@ +import {WorkOS} from '@workos-inc/node' +// import type {APIRoute} from 'astro' +// export const prerender = false + +const workos = new WorkOS(import.meta.env.WORKOS_API_KEY) +const clientId = import.meta.env.WORKOS_CLIENT_ID + +export async function GET({redirect}) { + const authorizationUrl = + workos.userManagement.getAuthorizationUrl({ + provider: 'authkit', + redirectUri: import.meta.env.WORKOS_REDIRECT_URI, + clientId, + }) + + return redirect(authorizationUrl, 302) +} diff --git a/chan.dev/src/pages/user.astro b/chan.dev/src/pages/user.astro new file mode 100644 index 00000000..d79c57cd --- /dev/null +++ b/chan.dev/src/pages/user.astro @@ -0,0 +1,41 @@ +--- +import {WorkOS} from '@workos-inc/node' +import {createRemoteJWKSet, jwtVerify} from 'jose' + +import {unsealData} from 'iron-session' + +export const prerender = false + +const cookie = Astro.cookies.get('wos-session') + +if (!cookie) { + return Astro.redirect('/auth') +} + +const session = await unsealData(cookie.value, { + password: import.meta.env.WORKOS_COOKIE_PASSWORD, +}) + +const workos = new WorkOS(import.meta.env.WORKOS_API_KEY) + +const JWKS = createRemoteJWKSet( + new URL( + workos.userManagement.getJwksUrl( + import.meta.env.WORKOS_CLIENT_ID + ) + ) +) + +try { + const verifiedSession = await jwtVerify( + session.accessToken, + JWKS + ) + console.log(verifiedSession) +} catch (e) { + console.warn('Failed to verify session:', e) + return Astro.redirect('/auth') +} +--- + +
{JSON.stringify(session, null, '\t')}