Scratch space for learning atproto app development

Revoke sessions on logout

Changed files
+27 -16
src
+1 -1
.env.template
···
# COOKIE_SECRET=""
# May be generated with `./bin/gen-jwk` (requires `npm install` once first)
-
# PRIVATE_JWKS='[{"kty":"EC","kid":"123",...}]'
+
# PRIVATE_KEYS='[{"kty":"EC","kid":"123",...}]'
+7 -11
src/auth/client.ts
···
import { SessionStore, StateStore } from './storage'
export async function createOAuthClient(db: Database) {
-
assert(
-
!env.PUBLIC_URL || env.PRIVATE_JWKS,
-
'ATProto requires backend clients to be confidential',
-
)
-
// Confidential client require a keyset accessible on the internet. Non
// internet clients (e.g. development) cannot expose a keyset on the internet
// so they can't be private..
const keyset =
-
env.PUBLIC_URL && env.PRIVATE_JWKS
+
env.PUBLIC_URL && env.PRIVATE_KEYS
? new Keyset(
await Promise.all(
-
env.PRIVATE_JWKS.map((jwk) => JoseKey.fromJWK(jwk)),
+
env.PRIVATE_KEYS.map((jwk) => JoseKey.fromJWK(jwk)),
),
)
: undefined
+
+
assert(
+
!env.PUBLIC_URL || keyset?.size,
+
'ATProto requires backend clients to be confidential. Make sure to set the PRIVATE_KEYS environment variable.',
+
)
// If a keyset is defined (meaning the client is confidential). Let's make
// sure it has a private key for signing. Note: findPrivateKey will throw if
···
clientMetadata,
stateStore: new StateStore(db),
sessionStore: new SessionStore(db),
-
-
// XXX Staging
-
plcDirectoryUrl: 'https://plc.staging.bsky.dev',
-
handleResolver: 'https://staging.bsky.dev',
})
}
+1 -1
src/env.ts
···
PUBLIC_URL: str({}),
DB_PATH: str({ devDefault: ':memory:' }),
COOKIE_SECRET: str({ devDefault: '00000000000000000000000000000000' }),
-
PRIVATE_JWKS: envalidJsonWebKeys({ default: undefined }),
+
PRIVATE_KEYS: envalidJsonWebKeys({ default: undefined }),
})
+18 -3
src/routes.ts
···
import { home } from '#/pages/home'
import { login } from '#/pages/login'
-
type Session = { did: string }
+
type Session = { did?: string }
// Helper function to get the Atproto Agent for the active session
async function getSessionAgent(
···
})
if (!session.did) return null
try {
-
const oauthSession = await ctx.oauthClient.restore(session.did)
+
// force rotating the credentials if the request has a no-cache header
+
const refresh = req.headers['cache-control']?.includes('no-cache') || 'auto'
+
+
const oauthSession = await ctx.oauthClient.restore(session.did, refresh)
return oauthSession ? new Agent(oauthSession) : null
} catch (err) {
ctx.logger.warn({ err }, 'oauth restore failed')
···
cookieName: 'sid',
password: env.COOKIE_SECRET,
})
-
await session.destroy()
+
+
// Revoke credentials on the server
+
if (session.did) {
+
try {
+
const oauthSession = await ctx.oauthClient.restore(session.did)
+
if (oauthSession) await oauthSession.signOut()
+
} catch (err) {
+
ctx.logger.warn({ err }, 'Failed to revoke credentials')
+
}
+
}
+
+
session.destroy()
+
return res.redirect('/')
}),
)