Skip to content

Commit

Permalink
Entryway: support jwe self-verification tokens (#2234)
Browse files Browse the repository at this point in the history
* entryway: support jwe self-verification tokens

* entryway: add test for verification code w/ bad jwe key
  • Loading branch information
devinivy authored Feb 27, 2024
1 parent 40397e5 commit abf8011
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 42 deletions.
25 changes: 17 additions & 8 deletions packages/pds/src/api/com/atproto/server/createAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ const ensurePhoneVerification = async (
}

const selfVerificationCode =
code && isSelfVerificationCode(code)
code && getSelfVerificationCodeType(code) !== null
? await parseValidationCode(ctx, code)
: null

Expand All @@ -542,7 +542,8 @@ const ensurePhoneVerification = async (

if (selfVerificationCode) {
if (
selfVerificationCode.verdict === 'bad' ||
selfVerificationCode.verdict === 'bad' || // @NOTE deprecated value
selfVerificationCode.verdict === 'nay' ||
selfVerificationCode.handle !== handle
) {
// nonce is checked by registrationChecker
Expand Down Expand Up @@ -600,10 +601,16 @@ const cleanupUncreatedAccount = async (
}

const parseValidationCode = async (ctx: AppContext, code: string) => {
const payload = await ctx.authVerifier.verifyJwt({
token: code,
verifyOptions: { audience: ctx.cfg.service.did },
})
const payload =
getSelfVerificationCodeType(code) === 'jwe'
? await ctx.authVerifier.decryptJwt({
token: code,
decryptOptions: { audience: ctx.cfg.service.did },
})
: await ctx.authVerifier.verifyJwt({
token: code,
verifyOptions: { audience: ctx.cfg.service.did },
})
if (
payload.scope !== AuthScope.CreateAccount ||
typeof payload.jti !== 'string' ||
Expand All @@ -621,7 +628,9 @@ const parseValidationCode = async (ctx: AppContext, code: string) => {

// if it contains two dots then it looks like a jwt and we treat it as a
// self-verification code. otherwise we treat it as a phone verification code.
const isSelfVerificationCode = (code: string) => {
const getSelfVerificationCodeType = (code: string) => {
const dots = code.match(/\./g) ?? []
return dots.length === 2
if (dots.length === 2) return 'jwt'
if (dots.length === 4) return 'jwe'
return null
}
46 changes: 45 additions & 1 deletion packages/pds/src/auth-verifier.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from 'node:assert'
import {
KeyObject,
createPrivateKey,
Expand Down Expand Up @@ -108,6 +109,7 @@ export type AuthVerifierOpts = {
export class AuthVerifier {
private _signingSecret: KeyObject
private _verifyKey?: KeyObject
private _decryptSecret?: KeyObject
private _adminPass: string
private _moderatorPass: string
private _triagePass: string
Expand All @@ -121,6 +123,7 @@ export class AuthVerifier {
) {
this._signingSecret = opts.authKeys.signingSecret
this._verifyKey = opts.authKeys.verifyKey
this._decryptSecret = opts.authKeys.decryptSecret
this._adminPass = opts.adminPass
this._moderatorPass = opts.moderatorPass
this._triagePass = opts.triagePass
Expand Down Expand Up @@ -320,6 +323,20 @@ export class AuthVerifier {
})
}

decryptJwt(params: {
token: string
decryptOptions?: jose.JWTDecryptOptions
}) {
assert(
this._decryptSecret,
'decrypt secret not configured, cannot decrypt jwt',
)
return decryptJwt({
...params,
key: this._decryptSecret,
})
}

async validateAccessToken(
req: express.Request,
scopes: AuthScope[],
Expand Down Expand Up @@ -404,19 +421,28 @@ export const getAuthKeys = async (opts: AuthKeyOptions): Promise<AuthKeys> => {
const verifyKey = opts.jwtVerifyKey
? await createPublicKeyObject(opts.jwtVerifyKey.publicKeyHex)
: signingKey
return { signingSecret, signingKey, verifyKey }
const decryptSecret = opts.jweSecret128BitHex
? createSecretKey(Buffer.from(opts.jweSecret128BitHex, 'hex'))
: undefined
assert(
!decryptSecret || decryptSecret.symmetricKeySize === 16,
'decryption secret has wrong size: must be 128bit',
)
return { signingSecret, signingKey, verifyKey, decryptSecret }
}

type AuthKeyOptions = {
jwtSecret: string
jwtSigningKey?: SigningKeyMemory
jwtVerifyKey?: VerifyKey
jweSecret128BitHex?: string
}

export type AuthKeys = {
signingSecret: KeyObject
signingKey?: KeyObject
verifyKey?: KeyObject
decryptSecret?: KeyObject
}

const isBearerToken = (req: express.Request): boolean => {
Expand Down Expand Up @@ -454,6 +480,24 @@ const verifyJwt = async (params: {
return result.payload
}

const decryptJwt = async (params: {
token: string
key: KeyObject
decryptOptions?: jose.JWTDecryptOptions
}): Promise<jose.JWTPayload> => {
const { token, key, decryptOptions } = params
let result: jose.JWTDecryptResult
try {
result = await jose.jwtDecrypt(token, key, decryptOptions)
} catch (err) {
if (err?.['code'] === 'ERR_JWT_EXPIRED') {
throw new InvalidRequestError('Token has expired', 'ExpiredToken')
}
throw new InvalidRequestError('Token could not be verified', 'InvalidToken')
}
return result.payload
}

export const parseBasicAuth = (
token: string,
): { username: string; password: string } | null => {
Expand Down
2 changes: 2 additions & 0 deletions packages/pds/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const readEnv = (): ServerEnvironment => {
jwtVerifyKeyK256PublicKeyHex: envStr(
'PDS_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEX',
),
jweSecret128BitHex: envStr('PDS_JWE_SECRET_128BIT_HEX'),
adminPassword: envStr('PDS_ADMIN_PASSWORD'),
moderatorPassword: envStr('PDS_MODERATOR_PASSWORD'),
triagePassword: envStr('PDS_TRIAGE_PASSWORD'),
Expand Down Expand Up @@ -231,6 +232,7 @@ export type ServerEnvironment = {
jwtSecret?: string
jwtSigningKeyK256PrivateKeyHex?: string
jwtVerifyKeyK256PublicKeyHex?: string
jweSecret128BitHex?: string
adminPassword?: string
moderatorPassword?: string
triagePassword?: string
Expand Down
2 changes: 2 additions & 0 deletions packages/pds/src/config/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const envToSecrets = (env: ServerEnvironment): ServerSecrets => {
jwtSecret: env.jwtSecret,
jwtSigningKey,
jwtVerifyKey,
jweSecret128BitHex: env.jweSecret128BitHex,
adminPassword: env.adminPassword,
moderatorPassword: env.moderatorPassword ?? env.adminPassword,
triagePassword:
Expand All @@ -76,6 +77,7 @@ export type ServerSecrets = {
jwtSecret: string
jwtSigningKey?: SigningKeyMemory
jwtVerifyKey?: VerifyKey
jweSecret128BitHex?: string
adminPassword: string
moderatorPassword: string
triagePassword: string
Expand Down
Loading

0 comments on commit abf8011

Please sign in to comment.