Skip to content

Commit b4a028b

Browse files
committed
should be conf client
1 parent 67e5787 commit b4a028b

10 files changed

Lines changed: 115 additions & 53 deletions

File tree

nuxt.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export default defineNuxtConfig({
3737
github: {
3838
orgToken: '',
3939
},
40+
oauthJwkOne: process.env.OAUTH_JWK_ONE || undefined,
4041
// Upstash Redis for distributed OAuth token refresh locking in production
4142
upstash: {
4243
redisRestUrl: process.env.UPSTASH_KV_REST_API_URL || process.env.KV_REST_API_URL || '',
@@ -122,6 +123,7 @@ export default defineNuxtConfig({
122123
'/_avatar/**': { isr: 3600, proxy: 'https://www.gravatar.com/avatar/**' },
123124
'/opensearch.xml': { isr: true },
124125
'/oauth-client-metadata.json': { prerender: true },
126+
'/.well-known/jwks.json': { prerender: true },
125127
// never cache
126128
'/api/auth/**': { isr: false, cache: false },
127129
'/api/social/**': { isr: false, cache: false },

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"generate:sprite": "node scripts/generate-file-tree-sprite.ts",
3535
"generate:fixtures": "node scripts/generate-fixtures.ts",
3636
"generate:lexicons": "lex build --lexicons lexicons --out shared/types/lexicons --clear",
37+
"generate:jwk": "node scripts/gen-jwk.ts",
3738
"test": "vite test",
3839
"test:a11y": "pnpm build:test && LIGHTHOUSE_COLOR_MODE=dark pnpm test:a11y:prebuilt && LIGHTHOUSE_COLOR_MODE=light pnpm test:a11y:prebuilt",
3940
"test:a11y:prebuilt": "./scripts/lighthouse.sh",

scripts/gen-jwk.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { JoseKey } from '@atproto/oauth-client-node'
2+
3+
async function run() {
4+
const kid = Date.now().toString()
5+
const key = await JoseKey.generate(['ES256'], kid)
6+
const jwk = key.privateJwk
7+
8+
console.log(JSON.stringify(jwk))
9+
}
10+
11+
await run()

server/api/auth/atproto.get.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import type { OAuthSession } from '@atproto/oauth-client-node'
2-
import { NodeOAuthClient, OAuthCallbackError } from '@atproto/oauth-client-node'
2+
import { OAuthCallbackError } from '@atproto/oauth-client-node'
33
import { createError, getQuery, sendRedirect, setCookie, getCookie, deleteCookie } from 'h3'
44
import type { H3Event } from 'h3'
5-
import { getOAuthLock } from '#server/utils/atproto/lock'
6-
import { useOAuthStorage } from '#server/utils/atproto/storage'
75
import { SLINGSHOT_HOST } from '#shared/utils/constants'
86
import { useServerSession } from '#server/utils/server-session'
9-
import { handleResolver } from '#server/utils/atproto/oauth'
107
import { handleApiError } from '#server/utils/error-handler'
118
import type { DidString } from '@atproto/lex'
129
import { Client } from '@atproto/lex'
1310
import * as com from '#shared/types/lexicons/com'
1411
import * as app from '#shared/types/lexicons/app'
1512
import { isAtIdentifierString } from '@atproto/lex'
16-
import { scope, getOauthClientMetadata } from '#server/utils/atproto/oauth'
13+
import { scope } from '#server/utils/atproto/oauth'
1714
import { UNSET_NUXT_SESSION_PASSWORD } from '#shared/utils/constants'
1815
// @ts-expect-error virtual file from oauth module
1916
import { clientUri } from '#oauth/config'
@@ -28,17 +25,8 @@ export default defineEventHandler(async event => {
2825
}
2926

3027
const query = getQuery(event)
31-
const clientMetadata = getOauthClientMetadata()
3228
const session = await useServerSession(event)
33-
const { stateStore, sessionStore } = useOAuthStorage(session)
34-
35-
const atclient = new NodeOAuthClient({
36-
stateStore,
37-
sessionStore,
38-
clientMetadata,
39-
requestLock: getOAuthLock(),
40-
handleResolver,
41-
})
29+
const atclient = await getNodeOAuthClient(session, config)
4230

4331
if (query.handle) {
4432
// Initiate auth flow
@@ -69,7 +57,10 @@ export default defineEventHandler(async event => {
6957
const redirectUrl = await atclient.authorize(query.handle, {
7058
scope,
7159
prompt: query.create ? 'create' : undefined,
72-
ui_locales: query.locale?.toString(),
60+
// TODO: I do not beleive this is working as expected on
61+
// a unsupported locale on the PDS. Gives Invalid at body.ui_locales
62+
// Commenting out for now
63+
// ui_locales: query.locale?.toString(),
7364
state: encodeOAuthState(event, { redirectPath }),
7465
})
7566

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { loadJWKs } from '#server/utils/atproto/oauth'
2+
3+
export default defineEventHandler(async event => {
4+
const config = useRuntimeConfig(event)
5+
const keys = await loadJWKs(config)
6+
if (!keys) {
7+
console.error('Failed to load JWKs. May not be set')
8+
return []
9+
}
10+
11+
return keys.publicJwks
12+
})
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
export default defineEventHandler(() => {
2-
return getOauthClientMetadata()
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' })
6+
return getOauthClientMetadata(pk?.alg)
37
})

server/utils/atproto/oauth-session-store.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,10 @@ export class OAuthSessionStore implements NodeSavedSessionStore {
3131

3232
async set(key: string, val: NodeSavedSession) {
3333
let sessionId = crypto.randomUUID()
34-
3534
try {
3635
await this.serverSession.update({
3736
oauthSessionId: sessionId,
3837
})
39-
4038
await this.storage.setItem<NodeSavedSession>(this.createStorageKey(key, sessionId), val)
4139
} catch (error) {
4240
// Not sure if this has been happening. But helps with debugging

server/utils/atproto/oauth.ts

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import type { OAuthClientMetadataInput, OAuthSession } from '@atproto/oauth-client-node'
1+
import type {
2+
OAuthClientMetadata,
3+
OAuthRedirectUri,
4+
OAuthSession,
5+
WebUri,
6+
} from '@atproto/oauth-client-node'
7+
import { JoseKey, Keyset, oauthRedirectUriSchema, webUriSchema } from '@atproto/oauth-client-node'
28
import type { EventHandlerRequest, H3Event, SessionManager } from 'h3'
39
import { NodeOAuthClient, AtprotoDohHandleResolver } from '@atproto/oauth-client-node'
4-
import { parse } from 'valibot'
510
import { getOAuthLock } from '#server/utils/atproto/lock'
611
import { useOAuthStorage } from '#server/utils/atproto/storage'
712
import { LIKES_SCOPE } from '#shared/utils/constants'
8-
import { OAuthMetadataSchema } from '#shared/schemas/oauth'
13+
import type { NitroRuntimeConfig } from 'nitropack/types'
14+
915
// @ts-expect-error virtual file from oauth module
1016
import { clientUri } from '#oauth/config'
1117
// 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({
1824
dohEndpoint: 'https://cloudflare-dns.com/dns-query',
1925
})
2026

21-
export function getOauthClientMetadata() {
27+
/**
28+
* Generates the OAuth client metadata. pkAlg is used to signify that the OAuth client is confendital
29+
*/
30+
export function getOauthClientMetadata(pkAlg: string | undefined = undefined): OAuthClientMetadata {
2231
const dev = import.meta.dev
2332

2433
const client_uri = clientUri
25-
const redirect_uri = `${client_uri}/api/auth/atproto`
34+
const redirect_uri: OAuthRedirectUri = oauthRedirectUriSchema.parse(
35+
`${client_uri}/api/auth/atproto`,
36+
)
37+
const jwks_uri: WebUri | undefined = pkAlg
38+
? webUriSchema.parse(`${client_uri}/.well-known/jwks.json`)
39+
: undefined
2640

2741
const client_id = dev
2842
? `http://localhost?redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${encodeURIComponent(scope)}`
2943
: `${client_uri}/oauth-client-metadata.json`
3044

3145
// If anything changes here, please make sure to also update /shared/schemas/oauth.ts to match
32-
return parse(OAuthMetadataSchema, {
46+
return {
3347
client_name: 'npmx.dev',
3448
client_id,
3549
client_uri,
3650
scope,
37-
redirect_uris: [redirect_uri] as [string, ...string[]],
51+
redirect_uris: [redirect_uri],
3852
grant_types: ['authorization_code', 'refresh_token'],
3953
application_type: 'web',
40-
token_endpoint_auth_method: 'none',
4154
dpop_bound_access_tokens: true,
4255
response_types: ['code'],
43-
}) as OAuthClientMetadataInput
56+
subject_type: 'public',
57+
authorization_signed_response_alg: 'RS256',
58+
// confendital client values
59+
token_endpoint_auth_method: pkAlg ? 'private_key_jwt' : 'none',
60+
jwks_uri,
61+
token_endpoint_auth_signing_alg: pkAlg,
62+
}
4463
}
4564

4665
type EventHandlerWithOAuthSession<T extends EventHandlerRequest, D> = (
@@ -49,29 +68,61 @@ type EventHandlerWithOAuthSession<T extends EventHandlerRequest, D> = (
4968
serverSession: SessionManager,
5069
) => Promise<D>
5170

71+
export async function getNodeOAuthClient(
72+
serverSession: SessionManager,
73+
config: NitroRuntimeConfig,
74+
): Promise<NodeOAuthClient> {
75+
const { stateStore, sessionStore } = useOAuthStorage(serverSession)
76+
77+
// These are optional and not expected or can be used easily in local development, only in production
78+
const keyset = await loadJWKs(config)
79+
// @ts-expect-error Taken from statusphere-example-app. Throws a ts error
80+
const pk = keyset?.findPrivateKey({ use: 'sig' })
81+
console.log(pk)
82+
const clientMetadata = getOauthClientMetadata(pk?.alg)
83+
84+
return new NodeOAuthClient({
85+
stateStore,
86+
sessionStore,
87+
clientMetadata,
88+
requestLock: getOAuthLock(),
89+
handleResolver,
90+
keyset,
91+
})
92+
}
93+
94+
export async function loadJWKs(config: NitroRuntimeConfig): Promise<Keyset | undefined> {
95+
// If we ever need to add multiple JWKs to rotate keys we will need to add a new one
96+
// under a new variable and update here
97+
const jwkOne = config.oauthJwkOne
98+
if (!jwkOne) return undefined
99+
100+
// For multiple keys if we need to rotate
101+
// const keys = await Promise.all([JoseKey.fromImportable(jwkOne)])
102+
103+
const keys = await JoseKey.fromImportable(jwkOne)
104+
return new Keyset([keys])
105+
}
106+
52107
async function getOAuthSession(
53108
event: H3Event,
54109
): Promise<{ oauthSession: OAuthSession | undefined; serverSession: SessionManager }> {
55110
const serverSession = await useServerSession(event)
111+
const config = useRuntimeConfig(event)
56112

57113
try {
58-
const clientMetadata = getOauthClientMetadata()
59-
const { stateStore, sessionStore } = useOAuthStorage(serverSession)
60-
61-
const client = new NodeOAuthClient({
62-
stateStore,
63-
sessionStore,
64-
clientMetadata,
65-
requestLock: getOAuthLock(),
66-
handleResolver,
67-
})
114+
const client = await getNodeOAuthClient(serverSession, config)
68115

69116
const currentSession = serverSession.data
70117
// TODO (jg): why can a session be `{}`?
71118
if (!currentSession || !currentSession.public?.did) {
72119
return { oauthSession: undefined, serverSession }
73120
}
74121

122+
if (currentSession.oauthSession && currentSession.public.did) {
123+
//TODO clear and redirect to login to clean up old sessions
124+
}
125+
75126
const oauthSession = await client.restore(currentSession.public.did)
76127
return { oauthSession, serverSession }
77128
} catch (error) {

shared/schemas/oauth.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

shared/types/userSession.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { NodeSavedSession } from '@atproto/oauth-client-node'
2+
13
export interface UserServerSession {
24
public?:
35
| {
@@ -10,4 +12,8 @@ export interface UserServerSession {
1012
// These values are tied to the users browser session and used by atproto OAuth
1113
oauthSessionId?: string | undefined
1214
oauthStateId?: string | undefined
15+
16+
// Here for historic reasons to redirect users logged in with the previous oauth to login again
17+
// TODO: actually make it do that
18+
oauthSession?: NodeSavedSession | undefined
1319
}

0 commit comments

Comments
 (0)