From ebce9c96938410319eb64284ad2f06b58980f1c0 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Tue, 10 Feb 2026 18:00:52 -0600 Subject: [PATCH 1/8] I think that's the session store --- modules/cache.ts | 7 +++ server/utils/atproto/oauth-session-store.ts | 50 +++++++++++++++------ server/utils/atproto/oauth-state-store.ts | 22 +++++---- server/utils/atproto/storage.ts | 2 + shared/types/userSession.ts | 4 ++ 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/modules/cache.ts b/modules/cache.ts index a2e7c3a41..31df72a6b 100644 --- a/modules/cache.ts +++ b/modules/cache.ts @@ -4,6 +4,8 @@ import { provider } from 'std-env' // Storage key for fetch cache - must match shared/utils/fetch-cache-config.ts const FETCH_CACHE_STORAGE_BASE = 'fetch-cache' +// Storage key for OAuth cache - must match server/utils/atproto/storage.ts +const OAUTH_CACHE_STORAGE_BASE = 'atproto:oauth' export default defineNuxtModule({ meta: { @@ -37,6 +39,11 @@ export default defineNuxtModule({ ...nitroConfig.storage[FETCH_CACHE_STORAGE_BASE], driver: 'vercel-runtime-cache', } + + nitroConfig.storage[OAUTH_CACHE_STORAGE_BASE] = { + ...nitroConfig.storage[OAUTH_CACHE_STORAGE_BASE], + driver: 'vercel-runtime-cache', + } } const env = process.env.VERCEL_ENV diff --git a/server/utils/atproto/oauth-session-store.ts b/server/utils/atproto/oauth-session-store.ts index 041e09719..9d842862a 100644 --- a/server/utils/atproto/oauth-session-store.ts +++ b/server/utils/atproto/oauth-session-store.ts @@ -1,26 +1,43 @@ import type { NodeSavedSession, NodeSavedSessionStore } from '@atproto/oauth-client-node' import type { UserServerSession } from '#shared/types/userSession' import type { SessionManager } from 'h3' +import { OAUTH_CACHE_STORAGE_BASE } from '#server/utils/atproto/storage' export class OAuthSessionStore implements NodeSavedSessionStore { - private readonly session: SessionManager + private readonly serverSession: SessionManager + private readonly storage = useStorage(OAUTH_CACHE_STORAGE_BASE) constructor(session: SessionManager) { - this.session = session + this.serverSession = this.serverSession = session } - async get(): Promise { - const sessionData = this.session.data - if (!sessionData) return undefined - return sessionData.oauthSession + private createStorageKey(did: string, sessionId: string) { + return `sessions:${did}:${sessionId}` } - async set(_key: string, val: NodeSavedSession) { - // We are ignoring the key since the mapping is already done in the session + async get(key: string): Promise { + const serverSessionData = this.serverSession.data + if (!serverSessionData) return undefined + if (!serverSessionData.oauthSessionId) { + console.warn('[oauth session store] No oauthSessionId found in session data') + return undefined + } + + let session = await this.storage.getItem( + this.createStorageKey(key, serverSessionData.oauthSessionId), + ) + return session ?? undefined + } + + async set(key: string, val: NodeSavedSession) { + let sessionId = crypto.randomUUID() + try { - await this.session.update({ - oauthSession: val, + await this.serverSession.update({ + oauthSessionId: sessionId, }) + + await this.storage.setItem(this.createStorageKey(key, sessionId), val) } catch (error) { // Not sure if this has been happening. But helps with debugging console.error( @@ -31,9 +48,16 @@ export class OAuthSessionStore implements NodeSavedSessionStore { } } - async del() { - await this.session.update({ - oauthSession: undefined, + async del(key: string) { + const serverSessionData = this.serverSession.data + if (!serverSessionData) return undefined + if (!serverSessionData.oauthSessionId) { + console.warn('[oauth session store] No oauthSessionId found in session data') + return undefined + } + await this.storage.removeItem(this.createStorageKey(key, serverSessionData.oauthSessionId)) + await this.serverSession.update({ + oauthSessionId: undefined, }) } } diff --git a/server/utils/atproto/oauth-state-store.ts b/server/utils/atproto/oauth-state-store.ts index d15a95e96..3233599e9 100644 --- a/server/utils/atproto/oauth-state-store.ts +++ b/server/utils/atproto/oauth-state-store.ts @@ -1,29 +1,35 @@ import type { NodeSavedState, NodeSavedStateStore } from '@atproto/oauth-client-node' import type { UserServerSession } from '#shared/types/userSession' import type { SessionManager } from 'h3' +import { OAUTH_CACHE_STORAGE_BASE } from '#server/utils/atproto/storage' export class OAuthStateStore implements NodeSavedStateStore { - private readonly session: SessionManager + private readonly serverSession: SessionManager + private readonly storage = useStorage(OAUTH_CACHE_STORAGE_BASE) constructor(session: SessionManager) { - this.session = session + this.serverSession = session + } + + private createAKey(did: string, sessionId: string) { + return `state:${did}:${sessionId}` } async get(): Promise { - const sessionData = this.session.data - if (!sessionData) return undefined - return sessionData.oauthState + const serverSessionData = this.serverSession.data + if (!serverSessionData) return undefined + return serverSessionData.oauthState } - async set(_key: string, val: NodeSavedState) { + async set(key: string, val: NodeSavedState) { // We are ignoring the key since the mapping is already done in the session - await this.session.update({ + await this.serverSession.update({ oauthState: val, }) } async del() { - await this.session.update({ + await this.serverSession.update({ oauthState: undefined, }) } diff --git a/server/utils/atproto/storage.ts b/server/utils/atproto/storage.ts index 9575250cc..23e0004e2 100644 --- a/server/utils/atproto/storage.ts +++ b/server/utils/atproto/storage.ts @@ -3,6 +3,8 @@ import { OAuthStateStore } from './oauth-state-store' import { OAuthSessionStore } from './oauth-session-store' import type { UserServerSession } from '#shared/types/userSession' +export const OAUTH_CACHE_STORAGE_BASE = 'atproto:oauth' + export const useOAuthStorage = (session: SessionManager) => { return { stateStore: new OAuthStateStore(session), diff --git a/shared/types/userSession.ts b/shared/types/userSession.ts index d761c1336..05eaa4299 100644 --- a/shared/types/userSession.ts +++ b/shared/types/userSession.ts @@ -14,4 +14,8 @@ export interface UserServerSession { // multiple did logins per server session oauthSession?: NodeSavedSession | undefined oauthState?: NodeSavedState | undefined + // TODO: This todo is a place holder to rememebr to clean this up after this current oauth change + // + // Will most likely be crypto.randomUUID() and the did + oauthSessionId?: string | undefined } From d7b1bbfba4866581bd164da1c51a7372208b9db3 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Tue, 10 Feb 2026 18:17:50 -0600 Subject: [PATCH 2/8] Should be state store as well --- server/utils/atproto/oauth-state-store.ts | 23 ++++++++++++++++------- shared/types/userSession.ts | 12 ++---------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/server/utils/atproto/oauth-state-store.ts b/server/utils/atproto/oauth-state-store.ts index 3233599e9..fbea36ad4 100644 --- a/server/utils/atproto/oauth-state-store.ts +++ b/server/utils/atproto/oauth-state-store.ts @@ -11,26 +11,35 @@ export class OAuthStateStore implements NodeSavedStateStore { this.serverSession = session } - private createAKey(did: string, sessionId: string) { + private createStorageKey(did: string, sessionId: string) { return `state:${did}:${sessionId}` } - async get(): Promise { + async get(key: string): Promise { const serverSessionData = this.serverSession.data if (!serverSessionData) return undefined - return serverSessionData.oauthState + if (!serverSessionData.oauthStateId) return undefined + const state = await this.storage.getItem( + this.createStorageKey(key, serverSessionData.oauthStateId), + ) + return state ?? undefined } async set(key: string, val: NodeSavedState) { - // We are ignoring the key since the mapping is already done in the session + let stateId = crypto.randomUUID() await this.serverSession.update({ - oauthState: val, + oauthStateId: stateId, }) + await this.storage.setItem(this.createStorageKey(key, stateId), val) } - async del() { + async del(key: string) { + const serverSessionData = this.serverSession.data + if (!serverSessionData) return undefined + if (!serverSessionData.oauthStateId) return undefined + await this.storage.removeItem(this.createStorageKey(key, serverSessionData.oauthStateId)) await this.serverSession.update({ - oauthState: undefined, + oauthStateId: undefined, }) } } diff --git a/shared/types/userSession.ts b/shared/types/userSession.ts index 05eaa4299..282ec38c7 100644 --- a/shared/types/userSession.ts +++ b/shared/types/userSession.ts @@ -1,5 +1,3 @@ -import type { NodeSavedSession, NodeSavedState } from '@atproto/oauth-client-node' - export interface UserServerSession { public?: | { @@ -9,13 +7,7 @@ export interface UserServerSession { avatar?: string } | undefined - // Only to be used in the atproto session and state stores - // Will need to change to Record and add a current logged in user if we ever want to support - // multiple did logins per server session - oauthSession?: NodeSavedSession | undefined - oauthState?: NodeSavedState | undefined - // TODO: This todo is a place holder to rememebr to clean this up after this current oauth change - // - // Will most likely be crypto.randomUUID() and the did + // These values are tied to the users browser session and used by atproto OAuth oauthSessionId?: string | undefined + oauthStateId?: string | undefined } From cb4619f12b9f902618c3776835a36c3bada67b3c Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Tue, 10 Feb 2026 20:46:14 -0600 Subject: [PATCH 3/8] should be conf client --- nuxt.config.ts | 2 + package.json | 1 + scripts/gen-jwk.ts | 11 +++ server/api/auth/atproto.get.ts | 23 ++--- server/routes/.well-known/jwks.json.get.ts | 12 +++ .../routes/oauth-client-metadata.json.get.ts | 8 +- server/utils/atproto/oauth-session-store.ts | 2 - server/utils/atproto/oauth.ts | 89 +++++++++++++++---- shared/schemas/oauth.ts | 14 --- shared/types/userSession.ts | 6 ++ 10 files changed, 115 insertions(+), 53 deletions(-) create mode 100644 scripts/gen-jwk.ts create mode 100644 server/routes/.well-known/jwks.json.get.ts delete mode 100644 shared/schemas/oauth.ts diff --git a/nuxt.config.ts b/nuxt.config.ts index 5965c351e..cab22e8a4 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -34,6 +34,7 @@ export default defineNuxtConfig({ runtimeConfig: { sessionPassword: '', + oauthJwkOne: process.env.OAUTH_JWK_ONE || undefined, // Upstash Redis for distributed OAuth token refresh locking in production upstash: { redisRestUrl: process.env.UPSTASH_KV_REST_API_URL || process.env.KV_REST_API_URL || '', @@ -119,6 +120,7 @@ export default defineNuxtConfig({ '/_avatar/**': { isr: 3600, proxy: 'https://www.gravatar.com/avatar/**' }, '/opensearch.xml': { isr: true }, '/oauth-client-metadata.json': { prerender: true }, + '/.well-known/jwks.json': { prerender: true }, // never cache '/api/auth/**': { isr: false, cache: false }, '/api/social/**': { isr: false, cache: false }, diff --git a/package.json b/package.json index 12ea115e5..ce7a6ad02 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "generate:sprite": "node scripts/generate-file-tree-sprite.ts", "generate:fixtures": "node scripts/generate-fixtures.ts", "generate:lexicons": "lex build --lexicons lexicons --out shared/types/lexicons --clear", + "generate:jwk": "node scripts/gen-jwk.ts", "test": "vite test", "test:a11y": "pnpm build:test && LIGHTHOUSE_COLOR_MODE=dark pnpm test:a11y:prebuilt && LIGHTHOUSE_COLOR_MODE=light pnpm test:a11y:prebuilt", "test:a11y:prebuilt": "./scripts/lighthouse.sh", diff --git a/scripts/gen-jwk.ts b/scripts/gen-jwk.ts new file mode 100644 index 000000000..39a2740c8 --- /dev/null +++ b/scripts/gen-jwk.ts @@ -0,0 +1,11 @@ +import { JoseKey } from '@atproto/oauth-client-node' + +async function run() { + const kid = Date.now().toString() + const key = await JoseKey.generate(['ES256'], kid) + const jwk = key.privateJwk + + console.log(JSON.stringify(jwk)) +} + +await run() diff --git a/server/api/auth/atproto.get.ts b/server/api/auth/atproto.get.ts index 0ff4b2eaf..8ccff1f85 100644 --- a/server/api/auth/atproto.get.ts +++ b/server/api/auth/atproto.get.ts @@ -1,19 +1,16 @@ import type { OAuthSession } from '@atproto/oauth-client-node' -import { NodeOAuthClient, OAuthCallbackError } from '@atproto/oauth-client-node' +import { OAuthCallbackError } from '@atproto/oauth-client-node' import { createError, getQuery, sendRedirect, setCookie, getCookie, deleteCookie } from 'h3' import type { H3Event } from 'h3' -import { getOAuthLock } from '#server/utils/atproto/lock' -import { useOAuthStorage } from '#server/utils/atproto/storage' import { SLINGSHOT_HOST } from '#shared/utils/constants' import { useServerSession } from '#server/utils/server-session' -import { handleResolver } from '#server/utils/atproto/oauth' import { handleApiError } from '#server/utils/error-handler' import type { DidString } from '@atproto/lex' import { Client } from '@atproto/lex' import * as com from '#shared/types/lexicons/com' import * as app from '#shared/types/lexicons/app' import { isAtIdentifierString } from '@atproto/lex' -import { scope, getOauthClientMetadata } from '#server/utils/atproto/oauth' +import { scope } from '#server/utils/atproto/oauth' import { UNSET_NUXT_SESSION_PASSWORD } from '#shared/utils/constants' // @ts-expect-error virtual file from oauth module import { clientUri } from '#oauth/config' @@ -28,17 +25,8 @@ export default defineEventHandler(async event => { } const query = getQuery(event) - const clientMetadata = getOauthClientMetadata() const session = await useServerSession(event) - const { stateStore, sessionStore } = useOAuthStorage(session) - - const atclient = new NodeOAuthClient({ - stateStore, - sessionStore, - clientMetadata, - requestLock: getOAuthLock(), - handleResolver, - }) + const atclient = await getNodeOAuthClient(session, config) if (query.handle) { // Initiate auth flow @@ -69,7 +57,10 @@ export default defineEventHandler(async event => { const redirectUrl = await atclient.authorize(query.handle, { scope, prompt: query.create ? 'create' : undefined, - ui_locales: query.locale?.toString(), + // TODO: I do not beleive this is working as expected on + // a unsupported locale on the PDS. Gives Invalid at body.ui_locales + // Commenting out for now + // ui_locales: query.locale?.toString(), state: encodeOAuthState(event, { redirectPath }), }) diff --git a/server/routes/.well-known/jwks.json.get.ts b/server/routes/.well-known/jwks.json.get.ts new file mode 100644 index 000000000..a76a955c8 --- /dev/null +++ b/server/routes/.well-known/jwks.json.get.ts @@ -0,0 +1,12 @@ +import { loadJWKs } from '#server/utils/atproto/oauth' + +export default defineEventHandler(async event => { + const config = useRuntimeConfig(event) + const keys = await loadJWKs(config) + if (!keys) { + console.error('Failed to load JWKs. May not be set') + return [] + } + + return keys.publicJwks +}) diff --git a/server/routes/oauth-client-metadata.json.get.ts b/server/routes/oauth-client-metadata.json.get.ts index 0ea235ed6..bd2a39349 100644 --- a/server/routes/oauth-client-metadata.json.get.ts +++ b/server/routes/oauth-client-metadata.json.get.ts @@ -1,3 +1,7 @@ -export default defineEventHandler(() => { - return getOauthClientMetadata() +export default defineEventHandler(async event => { + const config = useRuntimeConfig(event) + const keyset = await loadJWKs(config) + // @ts-expect-error Taken from statusphere-example-app. Throws a ts error + const pk = keyset?.findPrivateKey({ use: 'sig' }) + return getOauthClientMetadata(pk?.alg) }) diff --git a/server/utils/atproto/oauth-session-store.ts b/server/utils/atproto/oauth-session-store.ts index 9d842862a..fcbbc7063 100644 --- a/server/utils/atproto/oauth-session-store.ts +++ b/server/utils/atproto/oauth-session-store.ts @@ -31,12 +31,10 @@ export class OAuthSessionStore implements NodeSavedSessionStore { async set(key: string, val: NodeSavedSession) { let sessionId = crypto.randomUUID() - try { await this.serverSession.update({ oauthSessionId: sessionId, }) - await this.storage.setItem(this.createStorageKey(key, sessionId), val) } catch (error) { // Not sure if this has been happening. But helps with debugging diff --git a/server/utils/atproto/oauth.ts b/server/utils/atproto/oauth.ts index e82ce85e4..2d6c217ed 100644 --- a/server/utils/atproto/oauth.ts +++ b/server/utils/atproto/oauth.ts @@ -1,11 +1,17 @@ -import type { OAuthClientMetadataInput, OAuthSession } from '@atproto/oauth-client-node' +import type { + OAuthClientMetadata, + OAuthRedirectUri, + OAuthSession, + WebUri, +} from '@atproto/oauth-client-node' +import { JoseKey, Keyset, oauthRedirectUriSchema, webUriSchema } from '@atproto/oauth-client-node' import type { EventHandlerRequest, H3Event, SessionManager } from 'h3' import { NodeOAuthClient, AtprotoDohHandleResolver } from '@atproto/oauth-client-node' -import { parse } from 'valibot' import { getOAuthLock } from '#server/utils/atproto/lock' import { useOAuthStorage } from '#server/utils/atproto/storage' import { LIKES_SCOPE } from '#shared/utils/constants' -import { OAuthMetadataSchema } from '#shared/schemas/oauth' +import type { NitroRuntimeConfig } from 'nitropack/types' + // @ts-expect-error virtual file from oauth module import { clientUri } from '#oauth/config' // TODO: If you add writing a new record you will need to add a scope for it @@ -18,29 +24,42 @@ export const handleResolver = new AtprotoDohHandleResolver({ dohEndpoint: 'https://cloudflare-dns.com/dns-query', }) -export function getOauthClientMetadata() { +/** + * Generates the OAuth client metadata. pkAlg is used to signify that the OAuth client is confendital + */ +export function getOauthClientMetadata(pkAlg: string | undefined = undefined): OAuthClientMetadata { const dev = import.meta.dev const client_uri = clientUri - const redirect_uri = `${client_uri}/api/auth/atproto` + const redirect_uri: OAuthRedirectUri = oauthRedirectUriSchema.parse( + `${client_uri}/api/auth/atproto`, + ) + const jwks_uri: WebUri | undefined = pkAlg + ? webUriSchema.parse(`${client_uri}/.well-known/jwks.json`) + : undefined const client_id = dev ? `http://localhost?redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${encodeURIComponent(scope)}` : `${client_uri}/oauth-client-metadata.json` // If anything changes here, please make sure to also update /shared/schemas/oauth.ts to match - return parse(OAuthMetadataSchema, { + return { client_name: 'npmx.dev', client_id, client_uri, scope, - redirect_uris: [redirect_uri] as [string, ...string[]], + redirect_uris: [redirect_uri], grant_types: ['authorization_code', 'refresh_token'], application_type: 'web', - token_endpoint_auth_method: 'none', dpop_bound_access_tokens: true, response_types: ['code'], - }) as OAuthClientMetadataInput + subject_type: 'public', + authorization_signed_response_alg: 'RS256', + // confendital client values + token_endpoint_auth_method: pkAlg ? 'private_key_jwt' : 'none', + jwks_uri, + token_endpoint_auth_signing_alg: pkAlg, + } } type EventHandlerWithOAuthSession = ( @@ -49,22 +68,50 @@ type EventHandlerWithOAuthSession = ( serverSession: SessionManager, ) => Promise +export async function getNodeOAuthClient( + serverSession: SessionManager, + config: NitroRuntimeConfig, +): Promise { + const { stateStore, sessionStore } = useOAuthStorage(serverSession) + + // These are optional and not expected or can be used easily in local development, only in production + const keyset = await loadJWKs(config) + // @ts-expect-error Taken from statusphere-example-app. Throws a ts error + const pk = keyset?.findPrivateKey({ use: 'sig' }) + console.log(pk) + const clientMetadata = getOauthClientMetadata(pk?.alg) + + return new NodeOAuthClient({ + stateStore, + sessionStore, + clientMetadata, + requestLock: getOAuthLock(), + handleResolver, + keyset, + }) +} + +export async function loadJWKs(config: NitroRuntimeConfig): Promise { + // If we ever need to add multiple JWKs to rotate keys we will need to add a new one + // under a new variable and update here + const jwkOne = config.oauthJwkOne + if (!jwkOne) return undefined + + // For multiple keys if we need to rotate + // const keys = await Promise.all([JoseKey.fromImportable(jwkOne)]) + + const keys = await JoseKey.fromImportable(jwkOne) + return new Keyset([keys]) +} + async function getOAuthSession( event: H3Event, ): Promise<{ oauthSession: OAuthSession | undefined; serverSession: SessionManager }> { const serverSession = await useServerSession(event) + const config = useRuntimeConfig(event) try { - const clientMetadata = getOauthClientMetadata() - const { stateStore, sessionStore } = useOAuthStorage(serverSession) - - const client = new NodeOAuthClient({ - stateStore, - sessionStore, - clientMetadata, - requestLock: getOAuthLock(), - handleResolver, - }) + const client = await getNodeOAuthClient(serverSession, config) const currentSession = serverSession.data // TODO (jg): why can a session be `{}`? @@ -72,6 +119,10 @@ async function getOAuthSession( return { oauthSession: undefined, serverSession } } + if (currentSession.oauthSession && currentSession.public.did) { + //TODO clear and redirect to login to clean up old sessions + } + const oauthSession = await client.restore(currentSession.public.did) return { oauthSession, serverSession } } catch (error) { diff --git a/shared/schemas/oauth.ts b/shared/schemas/oauth.ts deleted file mode 100644 index 1e72ff319..000000000 --- a/shared/schemas/oauth.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { object, string, pipe, url, array, minLength, boolean } from 'valibot' - -export const OAuthMetadataSchema = object({ - client_id: pipe(string(), url()), - client_name: string(), - client_uri: pipe(string(), url()), - redirect_uris: pipe(array(string()), minLength(1)), - scope: string(), - grant_types: array(string()), - application_type: string(), - token_endpoint_auth_method: string(), - dpop_bound_access_tokens: boolean(), - response_types: array(string()), -}) diff --git a/shared/types/userSession.ts b/shared/types/userSession.ts index 282ec38c7..7b7f472ee 100644 --- a/shared/types/userSession.ts +++ b/shared/types/userSession.ts @@ -1,3 +1,5 @@ +import type { NodeSavedSession } from '@atproto/oauth-client-node' + export interface UserServerSession { public?: | { @@ -10,4 +12,8 @@ export interface UserServerSession { // These values are tied to the users browser session and used by atproto OAuth oauthSessionId?: string | undefined oauthStateId?: string | undefined + + // Here for historic reasons to redirect users logged in with the previous oauth to login again + // TODO: actually make it do that + oauthSession?: NodeSavedSession | undefined } From fc88622d5e58e4c07b5df44773472f79dee2aa47 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Tue, 10 Feb 2026 21:08:33 -0600 Subject: [PATCH 4/8] commiting to swap --- server/utils/atproto/oauth.ts | 23 +++++++++++++---------- shared/types/userSession.ts | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/server/utils/atproto/oauth.ts b/server/utils/atproto/oauth.ts index 2d6c217ed..6ee5e0e5b 100644 --- a/server/utils/atproto/oauth.ts +++ b/server/utils/atproto/oauth.ts @@ -10,10 +10,11 @@ import { NodeOAuthClient, AtprotoDohHandleResolver } from '@atproto/oauth-client import { getOAuthLock } from '#server/utils/atproto/lock' import { useOAuthStorage } from '#server/utils/atproto/storage' import { LIKES_SCOPE } from '#shared/utils/constants' -import type { NitroRuntimeConfig } from 'nitropack/types' +import { type NitroRuntimeConfig } from 'nitropack/types' // @ts-expect-error virtual file from oauth module import { clientUri } from '#oauth/config' +import type { UserServerSession } from '~~/shared/types/userSession' // TODO: If you add writing a new record you will need to add a scope for it export const scope = `atproto ${LIKES_SCOPE}` @@ -42,7 +43,7 @@ export function getOauthClientMetadata(pkAlg: string | undefined = undefined): O ? `http://localhost?redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${encodeURIComponent(scope)}` : `${client_uri}/oauth-client-metadata.json` - // If anything changes here, please make sure to also update /shared/schemas/oauth.ts to match + // If anything changes here, please make zsure to also update /shared/schemas/oauth.ts to match return { client_name: 'npmx.dev', client_id, @@ -78,7 +79,6 @@ export async function getNodeOAuthClient( const keyset = await loadJWKs(config) // @ts-expect-error Taken from statusphere-example-app. Throws a ts error const pk = keyset?.findPrivateKey({ use: 'sig' }) - console.log(pk) const clientMetadata = getOauthClientMetadata(pk?.alg) return new NodeOAuthClient({ @@ -104,9 +104,10 @@ export async function loadJWKs(config: NitroRuntimeConfig): Promise { +async function getOAuthSession(event: H3Event): Promise<{ + oauthSession: OAuthSession | undefined + serverSession: SessionManager +}> { const serverSession = await useServerSession(event) const config = useRuntimeConfig(event) @@ -119,10 +120,6 @@ async function getOAuthSession( return { oauthSession: undefined, serverSession } } - if (currentSession.oauthSession && currentSession.public.did) { - //TODO clear and redirect to login to clean up old sessions - } - const oauthSession = await client.restore(currentSession.public.did) return { oauthSession, serverSession } } catch (error) { @@ -159,6 +156,12 @@ export function eventHandlerWithOAuthSession( ) { return defineEventHandler(async event => { const { oauthSession, serverSession } = await getOAuthSession(event) + + //A one time redirect to upgrade the previous sessions. Can remove in 2 weeks from merge if we'd like + if (serverSession.data.oauthSession && serverSession.data?.public?.did) { + return sendRedirect(event, `/api/auth/atproto?hanlde=${serverSession.data?.public?.did}`) + } + return await handler(event, oauthSession, serverSession) }) } diff --git a/shared/types/userSession.ts b/shared/types/userSession.ts index 7b7f472ee..f14002c68 100644 --- a/shared/types/userSession.ts +++ b/shared/types/userSession.ts @@ -13,7 +13,7 @@ export interface UserServerSession { oauthSessionId?: string | undefined oauthStateId?: string | undefined + // DO NOT USE // Here for historic reasons to redirect users logged in with the previous oauth to login again - // TODO: actually make it do that oauthSession?: NodeSavedSession | undefined } From 27e7b663424bca57a6bd6785b1503c02fd1c92e1 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Tue, 10 Feb 2026 21:35:57 -0600 Subject: [PATCH 5/8] I think that's the redirect --- app/components/Header/AuthModal.client.vue | 8 ++++++++ server/api/auth/session.get.ts | 12 ++++++++++++ server/utils/atproto/oauth.ts | 5 ----- shared/schemas/publicUserSession.ts | 13 +++++++------ shared/types/userSession.ts | 1 + 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app/components/Header/AuthModal.client.vue b/app/components/Header/AuthModal.client.vue index 437282b19..c77088de1 100644 --- a/app/components/Header/AuthModal.client.vue +++ b/app/components/Header/AuthModal.client.vue @@ -50,6 +50,14 @@ watch(handleInput, newHandleInput => { handleInput.value = normalized } }) + +watch(user, async newUser => { + if (newUser?.relogin) { + await authRedirect(newUser.did, { + redirectTo: route.fullPath, + }) + } +})