Skip to content

Commit

Permalink
set and validate token audience on pds v2
Browse files Browse the repository at this point in the history
  • Loading branch information
devinivy committed Nov 1, 2023
1 parent 1d6aba9 commit 30723aa
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 22 deletions.
29 changes: 22 additions & 7 deletions packages/pds/src/account-manager/helpers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,36 @@ 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 }
}

export const createAccessToken = (opts: {
did: string
jwtKey: KeyObject
serviceDid: string
scope?: AuthScope
expiresIn?: string | number
}): Promise<string> => {
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)
Expand All @@ -48,13 +56,20 @@ export const createAccessToken = (opts: {
export const createRefreshToken = (opts: {
did: string
jwtKey: KeyObject
serviceDid: string
jti?: string
expiresIn?: string | number
}): Promise<string> => {
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()
Expand Down
15 changes: 11 additions & 4 deletions packages/pds/src/account-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 15 additions & 7 deletions packages/pds/src/auth-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,19 @@ export type AuthVerifierOpts = {
adminPass: string
moderatorPass: string
triagePass: string
adminServiceDid: string
dids: {
pds: string
entryway?: string
admin: string
}
}

export class AuthVerifier {
private _jwtKey: KeyObject
private _adminPass: string
private _moderatorPass: string
private _triagePass: string
public adminServiceDid: string
public dids: AuthVerifierOpts['dids']

constructor(
public accountManager: AccountManager,
Expand All @@ -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)
Expand Down Expand Up @@ -135,7 +139,10 @@ export class AuthVerifier {

refresh = async (ctx: ReqCtx): Promise<RefreshOutput> => {
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',
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -273,6 +280,7 @@ export class AuthVerifier {
const { did, scope, token, audience } = await this.validateBearerToken(
req,
scopes,
{ audience: this.dids.pds },
)
return {
credentials: {
Expand Down
5 changes: 4 additions & 1 deletion packages/pds/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -282,6 +284,7 @@ export type IdentityConfig = {

export type EntrywayConfig = {
url: string
did: string
jwtPublicKeyHex: string
plcRotationPublicKeyHex: string
}
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 @@ -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',
),
Expand Down Expand Up @@ -142,6 +143,7 @@ export type ServerEnvironment = {

// entryway
entrywayUrl?: string
entrywayDid?: string
entrywayJwtVerifyKeyK256PublicKeyHex?: string
entrywayPlcRotationKeyK256PublicKeyHex?: string

Expand Down
12 changes: 10 additions & 2 deletions packages/pds/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =
Expand Down
3 changes: 2 additions & 1 deletion packages/pds/tests/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/pds/tests/entryway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 30723aa

Please sign in to comment.