Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plenty-mangos-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@atproto/pds': patch
---

Fix rpc audience check for service proxying.
45 changes: 27 additions & 18 deletions packages/pds/src/pipethrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ export const proxyHandler = (ctx: AppContext): CatchallHandler => {
throw new InvalidRequestError('Bad token method', 'InvalidToken')
}

const { url: origin, did: aud } = await parseProxyInfo(ctx, req, lxm)
const proxy = await parseProxyInfo(ctx, req, lxm)
const aud = serviceAud(proxy)
Copy link
Copy Markdown
Collaborator Author

@devinivy devinivy Feb 3, 2026

Choose a reason for hiding this comment

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

This is where the issue was: aud was a bare DID without a service id, causing it to fail the scope checks.


const authResult = await performAuth({ req, res, params: { lxm, aud } })

Expand Down Expand Up @@ -84,7 +85,7 @@ export const proxyHandler = (ctx: AppContext): CatchallHandler => {
}

const dispatchOptions: Dispatcher.RequestOptions = {
origin,
origin: proxy.serviceInfo.url,
method: req.method,
path: req.originalUrl,
body,
Expand Down Expand Up @@ -156,10 +157,11 @@ export async function pipethrough(

const lxm = parseReqNsid(req)

const { url: origin, did: aud } = await parseProxyInfo(ctx, req, lxm)
const proxy = await parseProxyInfo(ctx, req, lxm)
const aud = serviceAud(proxy)

const dispatchOptions: Dispatcher.RequestOptions = {
origin,
origin: proxy.serviceInfo.url,
method: req.method,
path: req.originalUrl,
headers: {
Expand Down Expand Up @@ -209,9 +211,7 @@ export function computeProxyTo(
if (proxyToHeader) return proxyToHeader

const service = defaultService(ctx, lxm)
if (service.serviceInfo) {
return `${service.serviceInfo.did}#${service.serviceId}`
}
if (service) return serviceAud(service)

throw new InvalidRequestError(`No service configured for ${lxm}`)
}
Expand All @@ -220,14 +220,14 @@ export async function parseProxyInfo(
ctx: AppContext,
req: Request,
lxm: string,
): Promise<{ url: string; did: string }> {
): Promise<{ serviceId: string; serviceInfo: { url: string; did: string } }> {
// /!\ Hot path

const proxyToHeader = req.header('atproto-proxy')
if (proxyToHeader) return parseProxyHeader(ctx, proxyToHeader)

const { serviceInfo } = defaultService(ctx, lxm)
if (serviceInfo) return serviceInfo
const service = defaultService(ctx, lxm)
if (service) return service

throw new InvalidRequestError(`No service configured for ${lxm}`)
}
Expand All @@ -236,7 +236,7 @@ export const parseProxyHeader = async (
// Using subset of AppContext for testing purposes
ctx: Pick<AppContext, 'cfg' | 'idResolver'>,
proxyTo: string,
): Promise<{ did: string; url: string }> => {
): Promise<ServiceDetails> => {
// /!\ Hot path

const hashIndex = proxyTo.indexOf('#')
Expand All @@ -260,27 +260,31 @@ export const parseProxyHeader = async (
}

const did = proxyTo.slice(0, hashIndex)
const serviceId = proxyTo.slice(hashIndex)

// Special case a configured appview, while still proxying correctly any other appview
if (
ctx.cfg.bskyAppView &&
proxyTo === `${ctx.cfg.bskyAppView.did}#bsky_appview`
) {
return { did, url: ctx.cfg.bskyAppView.url }
return { serviceId, serviceInfo: ctx.cfg.bskyAppView }
}

const didDoc = await ctx.idResolver.did.resolve(did)
if (!didDoc) {
throw new InvalidRequestError('could not resolve proxy did')
}

const serviceId = proxyTo.slice(hashIndex)
const url = getServiceEndpoint(didDoc, { id: serviceId })
if (!url) {
throw new InvalidRequestError('could not resolve proxy did service url')
}

return { did, url }
return { serviceId, serviceInfo: { did, url } }
}

function serviceAud(details: ServiceDetails): string {
return `${details.serviceInfo.did}#${details.serviceId}`
}

/**
Expand Down Expand Up @@ -556,10 +560,7 @@ export const PROTECTED_METHODS = new Set<string>([
const defaultService = (
ctx: AppContext,
nsid: string,
): {
serviceId: string
serviceInfo: { url: string; did: string } | null
} => {
): ServiceDetails | null => {
switch (nsid) {
case ids.ToolsOzoneTeamAddMember:
case ids.ToolsOzoneTeamDeleteMember:
Expand Down Expand Up @@ -588,16 +589,19 @@ const defaultService = (
case ids.ToolsOzoneModerationListScheduledActions:
case ids.ToolsOzoneModerationCancelScheduledActions:
case ids.ToolsOzoneModerationScheduleAction:
if (!ctx.cfg.modService) return null
return {
serviceId: 'atproto_labeler',
serviceInfo: ctx.cfg.modService,
}
case ids.ComAtprotoModerationCreateReport:
if (!ctx.cfg.reportService) return null
return {
serviceId: 'atproto_labeler',
serviceInfo: ctx.cfg.reportService,
}
default:
if (!ctx.cfg.bskyAppView) return null
return {
serviceId: 'bsky_appview',
serviceInfo: ctx.cfg.bskyAppView,
Expand All @@ -612,3 +616,8 @@ const safeString = (str: unknown): string | undefined => {
function logResponseError(this: ServerResponse, err: unknown): void {
httpLogger.warn({ err }, 'error forwarding upstream response')
}

type ServiceDetails = {
serviceId: string
serviceInfo: { url: string; did: string }
}
Loading