Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Service auth method binding - PDS #2668

Merged
merged 16 commits into from
Aug 5, 2024
5 changes: 5 additions & 0 deletions .changeset/metal-cameras-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/api": patch
---

Add lxm and exp parameters to com.atproto.server.getServiceAuth
6 changes: 6 additions & 0 deletions .changeset/selfish-emus-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@atproto/xrpc-server": minor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming— this is a breaking change in xrpc-server?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it is 👍

"@atproto/pds": patch
---

Add lxm and nonce to signed service auth tokens.
17 changes: 16 additions & 1 deletion lexicons/com/atproto/server/getServiceAuth.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
"type": "string",
"format": "did",
"description": "The DID of the service that the token will be used to authenticate with"
},
"exp": {
"type": "integer",
"description": "The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope."
},
"lxm": {
"type": "string",
"format": "nsid",
"description": "Lexicon (XRPC) method to bind the requested token to"
}
}
},
Expand All @@ -27,7 +36,13 @@
}
}
}
}
},
"errors": [
{
"name": "BadExpiration",
"description": "Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes."
}
]
}
}
}
18 changes: 18 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2607,6 +2607,17 @@ export const schemaDict = {
description:
'The DID of the service that the token will be used to authenticate with',
},
exp: {
type: 'integer',
description:
'The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope.',
},
lxm: {
type: 'string',
format: 'nsid',
description:
'Lexicon (XRPC) method to bind the requested token to',
},
},
},
output: {
Expand All @@ -2621,6 +2632,13 @@ export const schemaDict = {
},
},
},
errors: [
{
name: 'BadExpiration',
description:
'Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes.',
},
],
},
},
},
Expand Down
11 changes: 11 additions & 0 deletions packages/api/src/client/types/com/atproto/server/getServiceAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { CID } from 'multiformats/cid'
export interface QueryParams {
/** The DID of the service that the token will be used to authenticate with */
aud: string
/** The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope. */
exp?: number
/** Lexicon (XRPC) method to bind the requested token to */
lxm?: string
}

export type InputSchema = undefined
Expand All @@ -29,8 +33,15 @@ export interface Response {
data: OutputSchema
}

export class BadExpirationError extends XRPCError {
constructor(src: XRPCError) {
super(src.status, src.error, src.message, src.headers)
}
}

export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
if (e.error === 'BadExpiration') return new BadExpirationError(e)
}
return e
}
7 changes: 6 additions & 1 deletion packages/bsky/src/auth-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,12 @@ export class AuthVerifier {
if (!jwtStr) {
throw new AuthRequiredError('missing jwt', 'MissingJwt')
}
const payload = await verifyServiceJwt(jwtStr, opts.aud, getSigningKey)
const payload = await verifyServiceJwt(
jwtStr,
opts.aud,
null,
getSigningKey,
)
return { iss: payload.iss, aud: payload.aud }
}

Expand Down
10 changes: 0 additions & 10 deletions packages/bsky/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as plc from '@did-plc/lib'
import { IdResolver } from '@atproto/identity'
import AtpAgent from '@atproto/api'
import { Keypair } from '@atproto/crypto'
import { createServiceJwt } from '@atproto/xrpc-server'
import { ServerConfig } from './config'
import { DataPlaneClient } from './data-plane/client'
import { Hydrator } from './hydration/hydrator'
Expand Down Expand Up @@ -89,15 +88,6 @@ export class AppContext {
return this.opts.featureGates
}

async serviceAuthJwt(aud: string) {
const iss = this.cfg.serverDid
return createServiceJwt({
iss,
aud,
keypair: this.signingKey,
})
}

reqLabelers(req: express.Request): ParsedLabelers {
const val = req.header('atproto-accept-labelers')
let parsed: ParsedLabelers | null
Expand Down
18 changes: 18 additions & 0 deletions packages/bsky/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2607,6 +2607,17 @@ export const schemaDict = {
description:
'The DID of the service that the token will be used to authenticate with',
},
exp: {
type: 'integer',
description:
'The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope.',
},
lxm: {
type: 'string',
format: 'nsid',
description:
'Lexicon (XRPC) method to bind the requested token to',
},
},
},
output: {
Expand All @@ -2621,6 +2632,13 @@ export const schemaDict = {
},
},
},
errors: [
{
name: 'BadExpiration',
description:
'Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes.',
},
],
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
export interface QueryParams {
/** The DID of the service that the token will be used to authenticate with */
aud: string
/** The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope. */
exp?: number
/** Lexicon (XRPC) method to bind the requested token to */
lxm?: string
}

export type InputSchema = undefined
Expand All @@ -31,6 +35,7 @@ export interface HandlerSuccess {
export interface HandlerError {
status: number
message?: string
error?: 'BadExpiration'
}

export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
Expand Down
5 changes: 5 additions & 0 deletions packages/bsky/tests/admin/admin-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ describe('admin auth', () => {
const headers = await createServiceAuthHeaders({
iss: modServiceDid,
aud: bskyDid,
lxm: null,
keypair: modServiceKey,
})
await agent.api.com.atproto.admin.updateSubjectStatus(
Expand All @@ -96,6 +97,7 @@ describe('admin auth', () => {
const headers = await createServiceAuthHeaders({
iss: altModDid,
aud: bskyDid,
lxm: null,
keypair: modServiceKey,
})
const attempt = agent.api.com.atproto.admin.updateSubjectStatus(
Expand All @@ -116,6 +118,7 @@ describe('admin auth', () => {
const headers = await createServiceAuthHeaders({
iss: sc.dids.alice,
aud: bskyDid,
lxm: null,
keypair: aliceKey,
})
const attempt = agent.api.com.atproto.admin.updateSubjectStatus(
Expand All @@ -136,6 +139,7 @@ describe('admin auth', () => {
const headers = await createServiceAuthHeaders({
iss: modServiceDid,
aud: bskyDid,
lxm: null,
keypair: badKey,
})
const attempt = agent.api.com.atproto.admin.updateSubjectStatus(
Expand All @@ -158,6 +162,7 @@ describe('admin auth', () => {
const headers = await createServiceAuthHeaders({
iss: modServiceDid,
aud: sc.dids.alice,
lxm: null,
keypair: modServiceKey,
})
const attempt = agent.api.com.atproto.admin.updateSubjectStatus(
Expand Down
1 change: 1 addition & 0 deletions packages/bsky/tests/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('auth', () => {
const jwt = await createServiceJwt({
iss: issuer,
aud: network.bsky.ctx.cfg.serverDid,
lxm: null,
keypair,
})
return agent.api.app.bsky.actor.getProfile(
Expand Down
1 change: 1 addition & 0 deletions packages/dev-env/src/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export class TestNetwork extends TestNetworkNoAppView {
const jwt = await createServiceJwt({
iss: did,
aud: aud ?? this.bsky.ctx.cfg.serverDid,
lxm: null,
keypair,
})
return { authorization: `Bearer ${jwt}` }
Expand Down
1 change: 1 addition & 0 deletions packages/dev-env/src/ozone-service-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class OzoneServiceProfile {
const serviceJwtRes =
await this.thirdPartyPdsClient.com.atproto.server.getServiceAuth({
aud: newServerDid,
lxm: 'com.atproto.server.createAccount',
})
const serviceJwt = serviceJwtRes.data.token

Expand Down
1 change: 1 addition & 0 deletions packages/dev-env/src/ozone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export class TestOzone {
const jwt = await createServiceJwt({
iss: account.did,
aud: this.ctx.cfg.service.did,
lxm: null,
keypair: account.key,
})
return { authorization: `Bearer ${jwt}` }
Expand Down
7 changes: 6 additions & 1 deletion packages/ozone/src/auth-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,12 @@ export class AuthVerifier {
if (!jwtStr) {
throw new AuthRequiredError('missing jwt', 'MissingJwt')
}
const payload = await verifyJwt(jwtStr, this.serviceDid, getSigningKey)
const payload = await verifyJwt(
jwtStr,
this.serviceDid,
null,
getSigningKey,
)
const iss = payload.iss

const member = await this.teamService.getMember(iss)
Expand Down
2 changes: 2 additions & 0 deletions packages/ozone/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export class AppContext {
createServiceAuthHeaders({
iss: `${cfg.service.did}#atproto_labeler`,
aud,
lxm: null,
keypair: signingKey,
})

Expand Down Expand Up @@ -230,6 +231,7 @@ export class AppContext {
return createServiceAuthHeaders({
iss,
aud,
lxm: null,
keypair: this.signingKey,
})
}
Expand Down
1 change: 1 addition & 0 deletions packages/ozone/src/daemon/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class DaemonContext {
createServiceAuthHeaders({
iss: `${cfg.service.did}#atproto_labeler`,
aud,
lxm: null,
keypair: signingKey,
})

Expand Down
18 changes: 18 additions & 0 deletions packages/ozone/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2607,6 +2607,17 @@ export const schemaDict = {
description:
'The DID of the service that the token will be used to authenticate with',
},
exp: {
type: 'integer',
description:
'The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope.',
},
lxm: {
type: 'string',
format: 'nsid',
description:
'Lexicon (XRPC) method to bind the requested token to',
},
},
},
output: {
Expand All @@ -2621,6 +2632,13 @@ export const schemaDict = {
},
},
},
errors: [
{
name: 'BadExpiration',
description:
'Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes.',
},
],
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
export interface QueryParams {
/** The DID of the service that the token will be used to authenticate with */
aud: string
/** The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope. */
exp?: number
/** Lexicon (XRPC) method to bind the requested token to */
lxm?: string
}

export type InputSchema = undefined
Expand All @@ -31,6 +35,7 @@ export interface HandlerSuccess {
export interface HandlerError {
status: number
message?: string
error?: 'BadExpiration'
}

export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
Expand Down
14 changes: 12 additions & 2 deletions packages/pds/src/api/app/bsky/feed/getFeed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { pipethrough } from '../../../../pipethrough'
import { ids } from '../../../../lexicon/lexicons'

export default function (server: Server, ctx: AppContext) {
const { appViewAgent } = ctx
Expand All @@ -14,9 +15,18 @@ export default function (server: Server, ctx: AppContext) {
const { data: feed } =
await appViewAgent.api.app.bsky.feed.getFeedGenerator(
{ feed: params.feed },
await ctx.appviewAuthHeaders(requester),
await ctx.appviewAuthHeaders(
requester,
ids.AppBskyFeedGetFeedGenerator,
),
)
return pipethrough(ctx, req, requester, feed.view.did)
return pipethrough(
ctx,
req,
requester,
feed.view.did,
ids.AppBskyFeedGetFeedSkeleton,
)
},
})
}
3 changes: 2 additions & 1 deletion packages/pds/src/api/app/bsky/feed/getPostThread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
formatMungedResponse,
} from '../../../../read-after-write'
import { pipethrough } from '../../../../pipethrough'
import { ids } from '../../../../lexicon/lexicons'

const METHOD_NSID = 'app.bsky.feed.getPostThread'

Expand Down Expand Up @@ -189,7 +190,7 @@ const readAfterWriteNotFound = async (
assert(ctx.appViewAgent)
const parentsRes = await ctx.appViewAgent.api.app.bsky.feed.getPostThread(
{ uri: highestParent, parentHeight: params.parentHeight, depth: 0 },
await ctx.appviewAuthHeaders(requester),
await ctx.appviewAuthHeaders(requester, ids.AppBskyFeedGetPostThread),
)
thread.parent = parentsRes.data.thread
} catch (err) {
Expand Down
Loading