Skip to content

Commit 1dac7e0

Browse files
committed
feat!: migrate to WorkOS as hosted auth, polish auth middlewares, fix logout
1 parent 0717bfd commit 1dac7e0

26 files changed

Lines changed: 488 additions & 525 deletions

File tree

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
* [Utilities](#utilities)
2424
* [Build](#build)
2525
* [Develop](#develop)
26-
* [Wrangler / Cloudflare Workers](#wrangler--cloudflare-workers)
26+
* [Wrangler / Cloudflare Workers (Local dev via workerd)](#wrangler--cloudflare-workers-local-dev-via-workerd)
2727
* [Deploy](#deploy)
2828
* [Notes](#notes)
2929
* [`import` ordering](#import-ordering)
3030
* [Dev with SSL](#dev-with-ssl)
31-
* [Remote Caching](#remote-caching)
31+
* [Turborepo Remote Caching](#turborepo-remote-caching)
3232
* [Useful Links](#useful-links)
33+
* [Turborepo](#turborepo)
34+
* [grammY](#grammy)
3335

3436
## Overview
3537

@@ -56,10 +58,10 @@ It is recommended to use an AI Agent ([`Roo Code`](https://github.com/RooVetGit/
5658
simply remove `sst` dependency and `sst.config.ts` if you want to use another solution.
5759
- *currently only `backend` app is configured, which will deploy a Lambda with Function URL enabled*
5860

59-
🔐 Comes with starter-kit for [**Kinde**](https://kinde.com/) [typescript-sdk](https://github.com/kinde-oss/kinde-typescript-sdk), see: `/apps/backend/api/auth`
60-
- *Add your env variables, activate the auth routes, profit$*
61+
🔐 Comes with authentication boilerplate via [**WorkOS AuthKit**](https://workos.com/), see: `/apps/backend/api/auth`
62+
- *Add your env variables, DONE!*
6163
- Please note that by default `backend` comes with a cookies-based session manager, which have great DX, security and does not require an external database (which also means great performance), but as the `backend` is decoupled with the Nuxt's SSR server, it will not work well with SSR (the session/auth state is not shared).
62-
So if you use SSR, you could use the official [Nuxt Kinde](https://nuxt.com/modules/kinde) module or implement your own way to manage the session at `apps/backend/src/middlewares/session.ts`.
64+
So, if you use SSR, you should implement another auth solution.
6365
- If you have a good session manager implementation, a PR is greatly appreciated!
6466

6567
💯 JS is always [**TypeScript**](https://www.typescriptlang.org/) where possible.

apps/backend-convex/.env

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Use `npx convex env` command to set the following environment variables:
22

3-
# CUSTOM_JWT_ISSUER=
4-
# CUSTOM_JWT_JWKS_URL=
3+
# CUSTOM_JWT_ISSUER=https://api.workos.com
4+
# CUSTOM_JWT_JWKS_URL=https://api.workos.com/sso/jwks/client_01JW6JMTP3CCKVBR5V7K8BT6R4
5+
# CUSTOM_JWT_CLIENT_ID=client_01JW6JMTP3CCKVBR5V7K8BT6R4

apps/backend-convex/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ Convex helps you kick-start amazing apps super fast and simple.
88

99
Auth is optional, you can remove it or just put the following sample config into Convex (for testing and development, do not deploy a production app with this config):
1010
```
11-
CUSTOM_JWT_ISSUER=https://topnames-dev.eu.kinde.com
12-
CUSTOM_JWT_JWKS_URL=https://topnames-dev.eu.kinde.com/.well-known/jwks
11+
CUSTOM_JWT_ISSUER=https://api.workos.com
12+
CUSTOM_JWT_JWKS_URL=https://api.workos.com/sso/jwks/client_01JW6JMTP3CCKVBR5V7K8BT6R4
13+
CUSTOM_JWT_CLIENT_ID=client_01JW6JMTP3CCKVBR5V7K8BT6R4
1314
```
15+
**NOTE**: for auth to work with **WorkOS**, you currently need to manually add `aud` field to WorkOS's JWT payload yourself via `JWT Template` feature of WorkOS.
16+
Ref: https://docs.convex.dev/auth/authkit/#debugging-authentication
1417

1518
`convex dev` might create a few new files when you run it for the first time, you should discard it: `tsconfig.json`, `.gitignore`, `README.md`
1619

apps/backend-convex/convex/auth.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export default {
44
type: 'customJwt',
55
issuer: process.env.CUSTOM_JWT_ISSUER!,
66
jwks: process.env.CUSTOM_JWT_JWKS_URL!,
7+
applicationID: process.env.CUSTOM_JWT_CLIENT_ID!,
78
algorithm: 'RS256',
89
},
910
],

apps/backend/.env

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
# FRONTEND_URL=https://YOUR_FRONTEND_URL
55
# CONVEX_URL=https://YOUR_CONVEX_URL
66

7-
# KINDE_DOMAIN=https://<YOUR_SUBDOMAIN>.kinde.com
8-
# KINDE_CLIENT_ID=
9-
# KINDE_CLIENT_SECRET=
10-
# KINDE_REDIRECT_URI=https://127.0.0.1:3300/api/auth/callback
11-
# KINDE_LOGOUT_REDIRECT_URI=https://127.0.0.1:3300/signed-out
7+
# WORKOS_CLIENT_ID=
8+
# WORKOS_API_KEY=
9+
# WORKOS_REDIRECT_URI=https://127.0.0.1:3300/api/auth/callback
1210

1311
# GRAMMY_MAIN_TOKEN=
1412
# GRAMMY_MAIN_WEBHOOK_SECRET_TOKEN=

apps/backend/.env.dev

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ APP_DEV_port=3301
55
FRONTEND_URL=https://127.0.0.1:3300
66
CONVEX_URL=
77

8-
# KINDE_DOMAIN=https://<YOUR_SUBDOMAIN>.kinde.com
9-
# KINDE_CLIENT_ID=
10-
# KINDE_CLIENT_SECRET=
11-
KINDE_REDIRECT_URI=https://127.0.0.1:3300/api/auth/callback
12-
KINDE_LOGOUT_REDIRECT_URI=https://127.0.0.1:3300/signed-out
8+
# WORKOS_CLIENT_ID=
9+
# WORKOS_API_KEY=
10+
WORKOS_REDIRECT_URI=https://127.0.0.1:3300/api/auth/callback
1311

1412
# GRAMMY_MAIN_TOKEN=
1513
# GRAMMY_MAIN_WEBHOOK_SECRET_TOKEN=

apps/backend/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- [Session management](./src/middlewares/session.ts).
1111
- Multi-platform support, one codebase that works for Node, Cloudflare Workers, AWS Lambda, and more.
1212
- Efficient providers management pattern, auto-optimize depends on platforms, with on-demand initialization support, sample providers includes:
13-
- Kinde auth
13+
- WorkOS AuthKit
1414
- Convex
1515
- grammY telegram bot
1616
- And more minor goodies!
@@ -19,7 +19,7 @@
1919

2020
## Structuring cookbook:
2121
#### Root level:
22-
Things like 3rd party APIs, DBs, Storages connectors, etc, should be placed in `#src/providers` folder, grouped by their purpose if possible, e.g: `#src/providers/auth/kinde-main.ts`, `#src/providers/db/neon-main.ts`.
22+
Things like 3rd party APIs, DBs, Storages connectors, etc, should be placed in `#src/providers` folder, grouped by their purpose if possible, e.g: `#src/providers/auth/workos-main.ts`, `#src/providers/db/neon-main.ts`.
2323

2424
Things that interact with `#src/providers` should be placed in `#src/services` folder. (like an `user` service)
2525

apps/backend/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
},
2222
"devDependencies": {
2323
"@hono/standard-validator": "^0.1.5",
24-
"@kinde-oss/kinde-typescript-sdk": "^2.13.0",
2524
"@local/common": "workspace:*",
2625
"@local/locales": "workspace:*",
2726
"@local/tsconfig": "workspace:*",
2827
"@namesmt/utils": "^0.5.18",
2928
"@scalar/hono-api-reference": "^0.9.22",
3029
"@vitest/coverage-v8": "^3.2.4",
30+
"@workos-inc/node": "^7.71.0",
3131
"arktype": "^2.1.23",
3232
"backend-convex": "workspace:*",
3333
"convex": "^1.28.0",
@@ -36,7 +36,6 @@
3636
"hono-adapter-aws-lambda": "^1.3.3",
3737
"hono-cookie-state": "^0.1.3",
3838
"hono-openapi": "^1.1.0",
39-
"hono-sessions": "^0.8.0",
4039
"petite-vue-i18n": "^11.1.12",
4140
"std-env": "^3.10.0",
4241
"tsdown": "^0.15.7",

apps/backend/src/api/$.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const apiApp = appFactory.createApp()
1010
// Simple health check route
1111
.route('', apiRoute)
1212

13-
// Auth app - you'll need to setup Kinde environment variables.
13+
// Auth app - you'll need to setup WorkOS environment variables.
1414
.route('/auth', authApp)
1515

1616
// Some example routes

apps/backend/src/api/auth/$.middleware.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
import { appFactory } from '#src/helpers/factory.js'
2-
import { getSessionManager } from '#src/helpers/kinde.js'
3-
import { getKindeClient } from '#src/providers/auth/kinde-main.js'
2+
import { getWorkOS, getWorkOSJwks } from '#src/providers/auth/workos-main.js'
43
import { DetailedError } from '@namesmt/utils'
5-
import { decode } from 'hono/jwt'
4+
import { decode, verifyWithJwks } from 'hono/jwt'
65

76
export function keepAuthFresh() {
87
return appFactory.createMiddleware(async (c, next) => {
9-
const kindeClient = await getKindeClient()
10-
const session = c.get('session')
11-
const sessionManager = getSessionManager(c)
8+
const workos = await getWorkOS()
9+
const auth = c.get('backend-auth')
10+
const userAuth = auth.data.userAuth
1211

13-
const userAuth = session.data.userAuth
1412
if (!userAuth)
1513
return await next()
1614

17-
const accessToken = await kindeClient.getToken(getSessionManager(c))
18-
1915
// If token will expire in less than 20 minutes, refresh it
20-
if ((decode(accessToken)?.payload?.exp || 0) * 1000 < Date.now() + 1000 * 60 * 20) {
21-
const tokenRefresh = await kindeClient.refreshTokens(sessionManager, true)
16+
if ((decode(userAuth.private.accessToken)?.payload?.exp || 0) * 1000 < Date.now() + 1000 * 60 * 20) {
17+
const { accessToken, refreshToken } = await workos.userManagement.authenticateWithRefreshToken({
18+
clientId: workos.clientId!,
19+
refreshToken: userAuth.private.refreshToken,
20+
})
2221

23-
session.data.userAuth = {
22+
auth.data.userAuth = {
2423
...userAuth,
25-
tokens: { accessToken: tokenRefresh.access_token },
24+
private: {
25+
accessToken,
26+
refreshToken,
27+
sessionId: decode(accessToken).payload.sid as string,
28+
},
2629
}
2730
}
2831

@@ -32,23 +35,40 @@ export function keepAuthFresh() {
3235

3336
export type checkAuthParams = {
3437
/**
35-
* If false, will only throw if user is authenticated but token verification fails.
38+
* Throws when the user is simply not authenticated yet (expired/revoked is still valid).
3639
*
3740
* @default true
3841
*/
3942
throwOnUnauthenticated?: boolean
43+
44+
/**
45+
* Throws when user is authenticated but the token fails to verify (including expired/revoked, etc).
46+
*
47+
* @default true
48+
*/
49+
throwOnBadToken?: boolean
4050
}
41-
export function checkAuth({ throwOnUnauthenticated = true }: checkAuthParams = {}) {
51+
export function checkAuth({ throwOnUnauthenticated = true, throwOnBadToken = true }: checkAuthParams = {}) {
4252
return appFactory.createMiddleware(async (c, next) => {
43-
const session = c.get('session')
44-
const userAuth = session.data.userAuth
53+
const auth = c.get('backend-auth')
54+
const userAuth = auth.data.userAuth
55+
4556
if (!userAuth) {
4657
if (throwOnUnauthenticated)
4758
throw new DetailedError('user is not authenticated', { statusCode: 401 })
4859

4960
return await next()
5061
}
5162

63+
const _wosPayload = userAuth && await verifyWithJwks(userAuth.private.accessToken, await getWorkOSJwks())
64+
.catch((e) => {
65+
// Clears `userAuth` if token is bad
66+
auth.data.userAuth = undefined
67+
68+
if (throwOnBadToken)
69+
throw e
70+
})
71+
5272
// TODO: permission tokens system here
5373

5474
await next()

0 commit comments

Comments
 (0)