Skip to content

Commit a9ab4f9

Browse files
committed
pretend you don't see this till Saturday. I'll be at a football game
1 parent b2e887e commit a9ab4f9

File tree

10 files changed

+54
-119
lines changed

10 files changed

+54
-119
lines changed

server/api/auth/atproto.get.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export default defineEventHandler(async event => {
2626

2727
const query = getQuery(event)
2828
const session = await useServerSession(event)
29-
const atclient = await getNodeOAuthClient(session, config)
3029

3130
if (query.handle) {
3231
// Initiate auth flow
@@ -54,7 +53,7 @@ export default defineEventHandler(async event => {
5453
}
5554

5655
try {
57-
const redirectUrl = await atclient.authorize(query.handle, {
56+
const redirectUrl = await event.context.oauthClient.authorize(query.handle, {
5857
scope,
5958
prompt: query.create ? 'create' : undefined,
6059
// TODO: I do not beleive this is working as expected on
@@ -78,7 +77,7 @@ export default defineEventHandler(async event => {
7877
// Handle callback
7978
try {
8079
const params = new URLSearchParams(query as Record<string, string>)
81-
const result = await atclient.callback(params)
80+
const result = await event.context.oauthClient.callback(params)
8281
try {
8382
const state = decodeOAuthState(event, result.state)
8483
const profile = await getMiniProfile(result.session)

server/plugins/oauth-client.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { NodeOAuthClient } from '@atproto/oauth-client-node'
2+
3+
/**
4+
* Creates a long living instance of the NodeOAuthClient.
5+
*/
6+
export default defineNitroPlugin(async nitroApp => {
7+
const oauthClient = await getNodeOAuthClient()
8+
9+
// Attach to event context for access in composables via useRequestEvent()
10+
nitroApp.hooks.hook('request', event => {
11+
event.context.oauthClient = oauthClient
12+
})
13+
})
14+
15+
// Extend the H3EventContext type
16+
declare module 'h3' {
17+
interface H3EventContext {
18+
oauthClient: NodeOAuthClient
19+
}
20+
}

server/routes/.well-known/jwks.json.get.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { loadJWKs } from '#server/utils/atproto/oauth'
22

3-
export default defineEventHandler(async event => {
4-
const config = useRuntimeConfig(event)
5-
const keys = await loadJWKs(config)
3+
export default defineEventHandler(async _ => {
4+
const keys = await loadJWKs()
65
if (!keys) {
76
console.error('Failed to load JWKs. May not be set')
87
return []
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
export default defineEventHandler(async event => {
2-
const config = useRuntimeConfig(event)
3-
const keyset = await loadJWKs(config)
4-
// @ts-expect-error Taken from statusphere-example-app. Throws a ts error
5-
const pk = keyset?.findPrivateKey({ use: 'sig' })
1+
export default defineEventHandler(async _ => {
2+
const keyset = await loadJWKs()
3+
const pk = keyset?.findPrivateKey({ usage: 'sign' })
64
return getOauthClientMetadata(pk?.alg)
75
})
Lines changed: 6 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import type { NodeSavedSession, NodeSavedSessionStore } from '@atproto/oauth-client-node'
2-
import type { UserServerSession } from '#shared/types/userSession'
3-
import type { SessionManager } from 'h3'
42
import { OAUTH_CACHE_STORAGE_BASE } from '#server/utils/atproto/storage'
53

64
// Refresh tokens from a confidential client should last for 180 days, each new refresh of access token resets
@@ -9,72 +7,26 @@ import { OAUTH_CACHE_STORAGE_BASE } from '#server/utils/atproto/storage'
97
const SESSION_EXPIRATION = CACHE_MAX_AGE_ONE_DAY * 179
108

119
export class OAuthSessionStore implements NodeSavedSessionStore {
12-
private readonly serverSession: SessionManager<UserServerSession>
1310
private readonly cache: CacheAdapter
1411

15-
constructor(session: SessionManager<UserServerSession>) {
16-
this.serverSession = session
12+
constructor() {
1713
this.cache = getCacheAdapter(OAUTH_CACHE_STORAGE_BASE)
1814
}
1915

20-
private createStorageKey(did: string, sessionId: string) {
21-
return `sessions:${did}:${sessionId}`
16+
private createStorageKey(did: string) {
17+
return `sessions:${did}`
2218
}
2319

2420
async get(key: string): Promise<NodeSavedSession | undefined> {
25-
const serverSessionData = this.serverSession.data
26-
if (!serverSessionData) return undefined
27-
if (!serverSessionData.oauthSessionId) {
28-
console.warn('[oauth session store] No oauthSessionId found in session data')
29-
return undefined
30-
}
31-
32-
let session = await this.cache.get<NodeSavedSession>(
33-
this.createStorageKey(key, serverSessionData.oauthSessionId),
34-
)
21+
let session = await this.cache.get<NodeSavedSession>(this.createStorageKey(key))
3522
return session ?? undefined
3623
}
3724

3825
async set(key: string, val: NodeSavedSession) {
39-
const serverSessionData = this.serverSession.data
40-
let sessionId
41-
if (!serverSessionData?.oauthSessionId) {
42-
sessionId = crypto.randomUUID()
43-
await this.serverSession.update({
44-
oauthSessionId: sessionId,
45-
})
46-
} else {
47-
sessionId = serverSessionData.oauthSessionId
48-
}
49-
try {
50-
await this.cache.set<NodeSavedSession>(
51-
this.createStorageKey(key, sessionId),
52-
val,
53-
SESSION_EXPIRATION,
54-
)
55-
await this.serverSession.update({
56-
lastUpdatedAt: new Date(),
57-
})
58-
} catch (error) {
59-
// Not sure if this has been happening. But helps with debugging
60-
console.error(
61-
'[oauth session store] Failed to set session:',
62-
error instanceof Error ? error.message : 'Unknown error',
63-
)
64-
throw error
65-
}
26+
await this.cache.set<NodeSavedSession>(this.createStorageKey(key), val, SESSION_EXPIRATION)
6627
}
6728

6829
async del(key: string) {
69-
const serverSessionData = this.serverSession.data
70-
if (!serverSessionData) return undefined
71-
if (!serverSessionData.oauthSessionId) {
72-
console.warn('[oauth session store] No oauthSessionId found in session data')
73-
return undefined
74-
}
75-
await this.cache.delete(this.createStorageKey(key, serverSessionData.oauthSessionId))
76-
await this.serverSession.update({
77-
oauthSessionId: undefined,
78-
})
30+
await this.cache.delete(this.createStorageKey(key))
7931
}
8032
}
Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,30 @@
11
import type { NodeSavedState, NodeSavedStateStore } from '@atproto/oauth-client-node'
2-
import type { UserServerSession } from '#shared/types/userSession'
3-
import type { SessionManager } from 'h3'
2+
import { OAUTH_CACHE_STORAGE_BASE } from './storage'
43

54
// It is recommended that oauth state is only saved for 30 minutes
65
const STATE_EXPIRATION = CACHE_MAX_AGE_ONE_MINUTE * 30
76

87
export class OAuthStateStore implements NodeSavedStateStore {
9-
private readonly serverSession: SessionManager<UserServerSession>
108
private readonly cache: CacheAdapter
119

12-
constructor(session: SessionManager<UserServerSession>) {
13-
this.serverSession = session
10+
constructor() {
1411
this.cache = getCacheAdapter(OAUTH_CACHE_STORAGE_BASE)
1512
}
1613

17-
private createStorageKey(did: string, sessionId: string) {
18-
return `state:${did}:${sessionId}`
14+
private createStorageKey(key: string) {
15+
return `state:${key}`
1916
}
2017

2118
async get(key: string): Promise<NodeSavedState | undefined> {
22-
const serverSessionData = this.serverSession.data
23-
if (!serverSessionData) return undefined
24-
if (!serverSessionData.oauthStateId) return undefined
25-
const state = await this.cache.get<NodeSavedState>(
26-
this.createStorageKey(key, serverSessionData.oauthStateId),
27-
)
19+
const state = await this.cache.get<NodeSavedState>(this.createStorageKey(key))
2820
return state ?? undefined
2921
}
3022

3123
async set(key: string, val: NodeSavedState) {
32-
let stateId = crypto.randomUUID()
33-
await this.serverSession.update({
34-
oauthStateId: stateId,
35-
})
36-
await this.cache.set<NodeSavedState>(this.createStorageKey(key, stateId), val, STATE_EXPIRATION)
24+
await this.cache.set<NodeSavedState>(this.createStorageKey(key), val, STATE_EXPIRATION)
3725
}
3826

3927
async del(key: string) {
40-
const serverSessionData = this.serverSession.data
41-
if (!serverSessionData) return undefined
42-
if (!serverSessionData.oauthStateId) return undefined
43-
await this.cache.delete(this.createStorageKey(key, serverSessionData.oauthStateId))
44-
await this.serverSession.update({
45-
oauthStateId: undefined,
46-
})
28+
await this.cache.delete(this.createStorageKey(key))
4729
}
4830
}

server/utils/atproto/oauth.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { NodeOAuthClient, AtprotoDohHandleResolver } from '@atproto/oauth-client
1010
import { getOAuthLock } from '#server/utils/atproto/lock'
1111
import { useOAuthStorage } from '#server/utils/atproto/storage'
1212
import { LIKES_SCOPE } from '#shared/utils/constants'
13-
import type { RuntimeConfig } from 'nuxt/schema'
1413
import type { UserServerSession } from '#shared/types/userSession'
1514
// @ts-expect-error virtual file from oauth module
1615
import { clientUri } from '#oauth/config'
@@ -69,16 +68,12 @@ type EventHandlerWithOAuthSession<T extends EventHandlerRequest, D> = (
6968
serverSession: SessionManager,
7069
) => Promise<D>
7170

72-
export async function getNodeOAuthClient(
73-
serverSession: SessionManager,
74-
config: RuntimeConfig,
75-
): Promise<NodeOAuthClient> {
76-
const { stateStore, sessionStore } = useOAuthStorage(serverSession)
71+
export async function getNodeOAuthClient(): Promise<NodeOAuthClient> {
72+
const { stateStore, sessionStore } = useOAuthStorage()
7773

7874
// These are optional and not expected or can be used easily in local development, only in production
79-
const keyset = await loadJWKs(config)
80-
// @ts-expect-error Taken from statusphere-example-app. Throws a ts error
81-
const pk = keyset?.findPrivateKey({ use: 'sig' })
75+
const keyset = await loadJWKs()
76+
const pk = keyset?.findPrivateKey({ usage: 'sign' })
8277
const clientMetadata = getOauthClientMetadata(pk?.alg)
8378

8479
return new NodeOAuthClient({
@@ -91,10 +86,11 @@ export async function getNodeOAuthClient(
9186
})
9287
}
9388

94-
export async function loadJWKs(config: RuntimeConfig): Promise<Keyset | undefined> {
89+
export async function loadJWKs(): Promise<Keyset | undefined> {
9590
// If we ever need to add multiple JWKs to rotate keys we will need to add a new one
9691
// under a new variable and update here
97-
const jwkOne = config.oauthJwkOne
92+
// @ts-expect-error Not sure how to strongly type these or if there's a better way to get this env
93+
const jwkOne = import.meta.env.NUXT_OAUTH_JWK_ONE
9894
if (!jwkOne) return undefined
9995

10096
// For multiple keys if we need to rotate
@@ -109,18 +105,15 @@ async function getOAuthSession(event: H3Event): Promise<{
109105
serverSession: SessionManager<UserServerSession>
110106
}> {
111107
const serverSession = await useServerSession(event)
112-
const config = useRuntimeConfig(event)
113108

114109
try {
115-
const client = await getNodeOAuthClient(serverSession, config)
116-
117110
const currentSession = serverSession.data
118111
// TODO (jg): why can a session be `{}`?
119112
if (!currentSession || !currentSession.public?.did) {
120113
return { oauthSession: undefined, serverSession }
121114
}
122115

123-
const oauthSession = await client.restore(currentSession.public.did)
116+
const oauthSession = await event.context.oauthClient.restore(currentSession.public.did)
124117
return { oauthSession, serverSession }
125118
} catch (error) {
126119
// Log error safely without using util.inspect on potentially problematic objects
@@ -156,11 +149,10 @@ export function eventHandlerWithOAuthSession<T extends EventHandlerRequest, D>(
156149
) {
157150
return defineEventHandler(async event => {
158151
const { oauthSession, serverSession } = await getOAuthSession(event)
159-
let oauthSessionId = serverSession.data.oauthSessionId
160-
152+
const publicData = serverSession.data.public
161153
// User was authenticated at one point, but was not able to restore
162154
// the session to the PDS
163-
if (!oauthSession && oauthSessionId) {
155+
if (!oauthSession && publicData) {
164156
// cleans up our server side session store
165157
await serverSession.clear()
166158
throw createError({

server/utils/atproto/storage.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import type { SessionManager } from 'h3'
21
import { OAuthStateStore } from './oauth-state-store'
32
import { OAuthSessionStore } from './oauth-session-store'
4-
import type { UserServerSession } from '#shared/types/userSession'
53

64
export const OAUTH_CACHE_STORAGE_BASE = 'atproto:oauth'
75

8-
export const useOAuthStorage = (session: SessionManager<UserServerSession>) => {
6+
export const useOAuthStorage = () => {
97
return {
10-
stateStore: new OAuthStateStore(session),
11-
sessionStore: new OAuthSessionStore(session),
8+
stateStore: new OAuthStateStore(),
9+
sessionStore: new OAuthSessionStore(),
1210
}
1311
}

server/utils/cache/adapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Redis } from '@upstash/redis'
33
export function getCacheAdapter(prefix: string): CacheAdapter {
44
const config = useRuntimeConfig()
55

6-
if (!import.meta.dev && config.upstash?.redisRestUrl && config.upstash?.redisRestToken) {
6+
if (import.meta.dev && config.upstash?.redisRestUrl && config.upstash?.redisRestToken) {
77
const redis = new Redis({
88
url: config.upstash.redisRestUrl,
99
token: config.upstash.redisRestToken,

shared/types/userSession.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ export interface UserServerSession {
1010
relogin?: boolean
1111
}
1212
| undefined
13-
// These values are tied to the users browser session and used by atproto OAuth
14-
oauthSessionId?: string | undefined
15-
oauthStateId?: string | undefined
16-
// Last time the oauth session was updated
17-
lastUpdatedAt?: Date | undefined
1813

1914
// DO NOT USE
2015
// Here for historic reasons to redirect users logged in with the previous oauth to login again

0 commit comments

Comments
 (0)