diff --git a/packages/pds/src/account-manager/helpers/auth.ts b/packages/pds/src/account-manager/helpers/auth.ts index c3dc6f7adb0..4cbfff8cb6d 100644 --- a/packages/pds/src/account-manager/helpers/auth.ts +++ b/packages/pds/src/account-manager/helpers/auth.ts @@ -17,14 +17,15 @@ export type RefreshToken = AuthToken & { scope: AuthScope.Refresh; jti: string } export const createTokens = async (opts: { did: string jwtKey: KeyObject + serviceDid: string scope?: AuthScope jti?: string expiresIn?: string | number }) => { - const { did, jwtKey, scope, jti, expiresIn } = opts + const { did, jwtKey, serviceDid, scope, jti, expiresIn } = opts const [accessJwt, refreshJwt] = await Promise.all([ - createAccessToken({ did, jwtKey, scope, expiresIn }), - createRefreshToken({ did, jwtKey, jti, expiresIn }), + createAccessToken({ did, jwtKey, serviceDid, scope, expiresIn }), + createRefreshToken({ did, jwtKey, serviceDid, jti, expiresIn }), ]) return { accessJwt, refreshJwt } } @@ -32,13 +33,20 @@ export const createTokens = async (opts: { export const createAccessToken = (opts: { did: string jwtKey: KeyObject + serviceDid: string scope?: AuthScope expiresIn?: string | number }): Promise => { - const { did, jwtKey, scope = AuthScope.Access, expiresIn = '120mins' } = opts - // @TODO set alg header? + const { + did, + jwtKey, + serviceDid, + scope = AuthScope.Access, + expiresIn = '120mins', + } = opts const signer = new jose.SignJWT({ scope }) .setProtectedHeader({ alg: 'HS256' }) // only symmetric keys supported + .setAudience(serviceDid) .setSubject(did) .setIssuedAt() .setExpirationTime(expiresIn) @@ -48,13 +56,20 @@ export const createAccessToken = (opts: { export const createRefreshToken = (opts: { did: string jwtKey: KeyObject + serviceDid: string jti?: string expiresIn?: string | number }): Promise => { - const { did, jwtKey, jti = getRefreshTokenId(), expiresIn = '90days' } = opts - // @TODO set alg header? audience? + const { + did, + jwtKey, + serviceDid, + jti = getRefreshTokenId(), + expiresIn = '90days', + } = opts const signer = new jose.SignJWT({ scope: AuthScope.Refresh }) .setProtectedHeader({ alg: 'HS256' }) // only symmetric keys supported + .setAudience(serviceDid) .setSubject(did) .setJti(jti) .setIssuedAt() diff --git a/packages/pds/src/account-manager/index.ts b/packages/pds/src/account-manager/index.ts index 7abd34d4879..b04237c5a13 100644 --- a/packages/pds/src/account-manager/index.ts +++ b/packages/pds/src/account-manager/index.ts @@ -16,7 +16,11 @@ import { StatusAttr } from '../lexicon/types/com/atproto/admin/defs' export class AccountManager { db: AccountDb - constructor(dbLocation: string, private jwtKey: KeyObject) { + constructor( + dbLocation: string, + private jwtKey: KeyObject, + private serviceDid: string, + ) { this.db = getDb(dbLocation) } @@ -70,8 +74,9 @@ export class AccountManager { }) { const { did, handle, email, password, repoCid, repoRev, inviteCode } = opts const { accessJwt, refreshJwt } = await auth.createTokens({ - jwtKey: this.jwtKey, did, + jwtKey: this.jwtKey, + serviceDid: this.serviceDid, scope: AuthScope.Access, }) const refreshPayload = auth.decodeRefreshToken(refreshJwt) @@ -128,8 +133,9 @@ export class AccountManager { async createSession(did: string, appPasswordName: string | null) { const { accessJwt, refreshJwt } = await auth.createTokens({ - jwtKey: this.jwtKey, did, + jwtKey: this.jwtKey, + serviceDid: this.serviceDid, scope: appPasswordName === null ? AuthScope.Access : AuthScope.AppPass, }) const refreshPayload = auth.decodeRefreshToken(refreshJwt) @@ -165,8 +171,9 @@ export class AccountManager { const nextId = token.nextId ?? auth.getRefreshTokenId() const { accessJwt, refreshJwt } = await auth.createTokens({ - jwtKey: this.jwtKey, did: token.did, + jwtKey: this.jwtKey, + serviceDid: this.serviceDid, scope: token.appPasswordName === null ? AuthScope.Access : AuthScope.AppPass, jti: nextId, diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 66a2f347f22..be13ef4fe5f 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -83,7 +83,11 @@ export type AuthVerifierOpts = { adminPass: string moderatorPass: string triagePass: string - adminServiceDid: string + dids: { + pds: string + entryway?: string + admin: string + } } export class AuthVerifier { @@ -91,7 +95,7 @@ export class AuthVerifier { private _adminPass: string private _moderatorPass: string private _triagePass: string - public adminServiceDid: string + public dids: AuthVerifierOpts['dids'] constructor( public accountManager: AccountManager, @@ -102,7 +106,7 @@ export class AuthVerifier { this._adminPass = opts.adminPass this._moderatorPass = opts.moderatorPass this._triagePass = opts.triagePass - this.adminServiceDid = opts.adminServiceDid + this.dids = opts.dids } // verifiers (arrow fns to preserve scope) @@ -135,7 +139,10 @@ export class AuthVerifier { refresh = async (ctx: ReqCtx): Promise => { const { did, scope, token, audience, payload } = - await this.validateBearerToken(ctx.req, [AuthScope.Refresh]) + await this.validateBearerToken(ctx.req, [AuthScope.Refresh], { + // when using entryway, proxying refresh credentials + audience: this.dids.entryway ? this.dids.entryway : this.dids.pds, + }) if (!payload.jti) { throw new AuthRequiredError( 'Unexpected missing refresh token id', @@ -205,14 +212,14 @@ export class AuthVerifier { const payload = await verifyServiceJwt( jwtStr, null, - async (did: string) => { - if (did !== this.adminServiceDid) { + async (did, forceRefresh) => { + if (did !== this.dids.admin) { throw new AuthRequiredError( 'Untrusted issuer for admin actions', 'UntrustedIss', ) } - return this.idResolver.did.resolveAtprotoKey(did) + return this.idResolver.did.resolveAtprotoKey(did, forceRefresh) }, ) return { @@ -273,6 +280,7 @@ export class AuthVerifier { const { did, scope, token, audience } = await this.validateBearerToken( req, scopes, + { audience: this.dids.pds }, ) return { credentials: { diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index 6db961d7b64..5c68d92f424 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -106,11 +106,13 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { if (env.entrywayUrl) { assert( env.entrywayJwtVerifyKeyK256PublicKeyHex && - env.entrywayPlcRotationKeyK256PublicKeyHex, + env.entrywayPlcRotationKeyK256PublicKeyHex && + env.entrywayDid, 'if entryway url is configured, must include all required entryway configuration', ) entrywayCfg = { url: env.entrywayUrl, + did: env.entrywayDid, jwtPublicKeyHex: env.entrywayJwtVerifyKeyK256PublicKeyHex, plcRotationPublicKeyHex: env.entrywayPlcRotationKeyK256PublicKeyHex, } @@ -282,6 +284,7 @@ export type IdentityConfig = { export type EntrywayConfig = { url: string + did: string jwtPublicKeyHex: string plcRotationPublicKeyHex: string } diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 2742dfb5a8f..4c8766ca194 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -44,6 +44,7 @@ export const readEnv = (): ServerEnvironment => { // entryway entrywayUrl: envStr('PDS_ENTRYWAY_URL'), + entrywayDid: envStr('PDS_ENTRYWAY_DID'), entrywayJwtVerifyKeyK256PublicKeyHex: envStr( 'PDS_ENTRYWAY_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEX', ), @@ -142,6 +143,7 @@ export type ServerEnvironment = { // entryway entrywayUrl?: string + entrywayDid?: string entrywayJwtVerifyKeyK256PublicKeyHex?: string entrywayPlcRotationKeyK256PublicKeyHex?: string diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index f69693e5156..bc0e337c03a 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -153,7 +153,11 @@ export class AppContext { : undefined const jwtSecretKey = createSecretKeyObject(secrets.jwtSecret) - const accountManager = new AccountManager(cfg.db.accountDbLoc, jwtSecretKey) + const accountManager = new AccountManager( + cfg.db.accountDbLoc, + jwtSecretKey, + cfg.service.did, + ) await accountManager.migrateOrThrow() const jwtKey = cfg.entryway @@ -165,7 +169,11 @@ export class AppContext { adminPass: secrets.adminPassword, moderatorPass: secrets.moderatorPassword, triagePass: secrets.triagePassword, - adminServiceDid: cfg.bskyAppView.did, + dids: { + pds: cfg.service.did, + entryway: cfg.entryway?.did, + admin: cfg.bskyAppView.did, + }, }) const plcRotationKey = diff --git a/packages/pds/tests/auth.test.ts b/packages/pds/tests/auth.test.ts index 75b10844440..d8d29942ccd 100644 --- a/packages/pds/tests/auth.test.ts +++ b/packages/pds/tests/auth.test.ts @@ -252,8 +252,9 @@ describe('auth', () => { password: 'password', }) const refreshJwt = await createRefreshToken({ - jwtKey: network.pds.jwtSecretKey(), did: account.did, + jwtKey: network.pds.jwtSecretKey(), + serviceDid: network.pds.ctx.cfg.service.did, expiresIn: -1, }) const refreshExpired = refreshSession(refreshJwt) diff --git a/packages/pds/tests/entryway.test.ts b/packages/pds/tests/entryway.test.ts index ff10d2e9dba..3ecd8d82eb7 100644 --- a/packages/pds/tests/entryway.test.ts +++ b/packages/pds/tests/entryway.test.ts @@ -24,6 +24,7 @@ describe('entryway', () => { plc = await TestPlc.create({}) pds = await TestPds.create({ entrywayUrl: `http://localhost:${entrywayPort}`, + entrywayDid: 'did:example:entryway', entrywayJwtVerifyKeyK256PublicKeyHex: getPublicHex(jwtSigningKey), entrywayPlcRotationKeyK256PublicKeyHex: getPublicHex(plcRotationKey), adminPassword: 'admin-pass',