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

Facilitate authing w/ PDS based on DID doc #1727

Merged
merged 11 commits into from
Oct 26, 2023
3 changes: 2 additions & 1 deletion lexicons/com/atproto/server/createAccount.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"accessJwt": { "type": "string" },
"refreshJwt": { "type": "string" },
"handle": { "type": "string", "format": "handle" },
"did": { "type": "string", "format": "did" }
"did": { "type": "string", "format": "did" },
"didDoc": { "type": "unknown" }
}
}
},
Expand Down
1 change: 1 addition & 0 deletions lexicons/com/atproto/server/createSession.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"refreshJwt": { "type": "string" },
"handle": { "type": "string", "format": "handle" },
"did": { "type": "string", "format": "did" },
"didDoc": { "type": "unknown" },
"email": { "type": "string" },
"emailConfirmed": { "type": "boolean" }
}
Expand Down
3 changes: 2 additions & 1 deletion lexicons/com/atproto/server/refreshSession.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"accessJwt": { "type": "string" },
"refreshJwt": { "type": "string" },
"handle": { "type": "string", "format": "handle" },
"did": { "type": "string", "format": "did" }
"did": { "type": "string", "format": "did" },
"didDoc": { "type": "unknown" }
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"@atproto/xrpc": "workspace:^",
"multiformats": "^9.9.0",
"tlds": "^1.234.0",
"typed-emitter": "^2.1.0"
"typed-emitter": "^2.1.0",
"zod": "^3.21.4"
Copy link
Collaborator

Choose a reason for hiding this comment

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

This isn't an addition to the api because its dependencies already depended on it

},
"devDependencies": {
"@atproto/lex-cli": "workspace:^",
Expand Down
26 changes: 25 additions & 1 deletion packages/api/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
AtpPersistSessionHandler,
AtpAgentOpts,
} from './types'
import { getPdsEndpoint } from './did/did-doc'

const REFRESH_SESSION = 'com.atproto.server.refreshSession'

Expand All @@ -30,6 +31,11 @@ export class AtpAgent {
api: AtpServiceClient
session?: AtpSessionData

/**
* The PDS URL, driven by the did doc. May be undefined.
*/
pdsUrl: URL | undefined

private _baseClient: AtpBaseClient
private _persistSession?: AtpPersistSessionHandler
private _refreshSessionPromise: Promise<void> | undefined
Expand Down Expand Up @@ -97,6 +103,7 @@ export class AtpAgent {
email: opts.email,
emailConfirmed: false,
}
this._updateApiEndpoint(res.data.didDoc)
return res
} catch (e) {
this.session = undefined
Expand Down Expand Up @@ -129,6 +136,7 @@ export class AtpAgent {
email: res.data.email,
emailConfirmed: res.data.emailConfirmed,
}
this._updateApiEndpoint(res.data.didDoc)
return res
} catch (e) {
this.session = undefined
Expand Down Expand Up @@ -253,7 +261,7 @@ export class AtpAgent {
}

// send the refresh request
const url = new URL(this.service.origin)
const url = new URL((this.pdsUrl || this.service).origin)
url.pathname = `/xrpc/${REFRESH_SESSION}`
const res = await AtpAgent.fetch(
url.toString(),
Expand All @@ -277,6 +285,7 @@ export class AtpAgent {
handle: res.body.handle,
did: res.body.did,
}
this._updateApiEndpoint(res.body.didDoc)
this._persistSession?.('update', this.session)
}
// else: other failures should be ignored - the issue will
Expand Down Expand Up @@ -311,6 +320,21 @@ export class AtpAgent {
*/
createModerationReport: typeof this.api.com.atproto.moderation.createReport =
(data, opts) => this.api.com.atproto.moderation.createReport(data, opts)

/**
* Helper to update the pds endpoint dynamically.
*
* The session methods (create, resume, refresh) may respond with the user's
* did document which contains the user's canonical PDS endpoint. That endpoint
* may differ from the endpoint used to contact the server. We capture that
* PDS endpoint and update the client to use that given endpoint for future
* requests. (This helps ensure smooth migrations between PDSes, especially
* when the PDSes are operated by a single org.)
*/
private _updateApiEndpoint(didDoc: unknown) {
this.pdsUrl = getPdsEndpoint(didDoc)
this.api.xrpc.uri = this.pdsUrl || this.service
}
}

function isErrorObject(v: unknown): v is ErrorResponseBody {
Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2338,6 +2338,9 @@ export const schemaDict = {
type: 'string',
format: 'did',
},
didDoc: {
type: 'unknown',
},
},
},
},
Expand Down Expand Up @@ -2563,6 +2566,9 @@ export const schemaDict = {
type: 'string',
format: 'did',
},
didDoc: {
type: 'unknown',
},
email: {
type: 'string',
},
Expand Down Expand Up @@ -2879,6 +2885,9 @@ export const schemaDict = {
type: 'string',
format: 'did',
},
didDoc: {
type: 'unknown',
},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface OutputSchema {
refreshJwt: string
handle: string
did: string
didDoc?: {}
[k: string]: unknown
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface OutputSchema {
refreshJwt: string
handle: string
did: string
didDoc?: {}
email?: string
emailConfirmed?: boolean
[k: string]: unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface OutputSchema {
refreshJwt: string
handle: string
did: string
didDoc?: {}
[k: string]: unknown
}

Expand Down
24 changes: 24 additions & 0 deletions packages/api/src/did/did-doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { DidDocument, didDocument } from '@atproto/common-web'

export function isValidDidDoc(doc: unknown): doc is DidDocument {
Copy link
Collaborator

Choose a reason for hiding this comment

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

not required, but I think we could take this one step further & move this to common-web as well

return didDocument.safeParse(doc).success
}

export function getPdsEndpoint(doc: unknown): URL | undefined {
if (isValidDidDoc(doc)) {
const pds = doc.service?.find(
(s) => s.id === '#atproto_pds' || s.id === `${doc.id}#atproto_pds`,
)
if (
pds &&
pds.type === 'AtprotoPersonalDataServer' &&
typeof pds.serviceEndpoint === 'string'
) {
try {
return new URL(pds.serviceEndpoint)
} catch {
return undefined
}
}
}
}
3 changes: 3 additions & 0 deletions packages/api/tests/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ describe('agent', () => {
beforeAll(async () => {
network = await TestNetworkNoAppView.create({
dbPostgresSchema: 'api_agent',
pds: {
enableDidDocWithSession: true,
},
})
})

Expand Down
9 changes: 9 additions & 0 deletions packages/bsky/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2338,6 +2338,9 @@ export const schemaDict = {
type: 'string',
format: 'did',
},
didDoc: {
type: 'unknown',
},
},
},
},
Expand Down Expand Up @@ -2563,6 +2566,9 @@ export const schemaDict = {
type: 'string',
format: 'did',
},
didDoc: {
type: 'unknown',
},
email: {
type: 'string',
},
Expand Down Expand Up @@ -2879,6 +2885,9 @@ export const schemaDict = {
type: 'string',
format: 'did',
},
didDoc: {
type: 'unknown',
},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface OutputSchema {
refreshJwt: string
handle: string
did: string
didDoc?: {}
[k: string]: unknown
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface OutputSchema {
refreshJwt: string
handle: string
did: string
didDoc?: {}
email?: string
emailConfirmed?: boolean
[k: string]: unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface OutputSchema {
refreshJwt: string
handle: string
did: string
didDoc?: {}
[k: string]: unknown
}

Expand Down
23 changes: 23 additions & 0 deletions packages/common-web/src/did-doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { z } from 'zod'

const verificationMethod = z.object({
id: z.string(),
type: z.string(),
controller: z.string(),
publicKeyMultibase: z.string().optional(),
})

const service = z.object({
id: z.string(),
type: z.string(),
serviceEndpoint: z.union([z.string(), z.record(z.unknown())]),
})

export const didDocument = z.object({
id: z.string(),
alsoKnownAs: z.array(z.string()).optional(),
verificationMethod: z.array(verificationMethod).optional(),
service: z.array(service).optional(),
})

export type DidDocument = z.infer<typeof didDocument>
1 change: 1 addition & 0 deletions packages/common-web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './ipld'
export * from './types'
export * from './times'
export * from './strings'
export * from './did-doc'
1 change: 1 addition & 0 deletions packages/dev-env/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const run = async () => {
port: 2583,
hostname: 'localhost',
dbPostgresSchema: 'pds',
enableDidDocWithSession: true,
},
bsky: {
dbPostgresSchema: 'bsky',
Expand Down
3 changes: 1 addition & 2 deletions packages/identity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
"dependencies": {
"@atproto/common-web": "workspace:^",
"@atproto/crypto": "workspace:^",
"axios": "^0.27.2",
"zod": "^3.21.4"
"axios": "^0.27.2"
},
"devDependencies": {
"@did-plc/lib": "^0.0.1",
Expand Down
27 changes: 4 additions & 23 deletions packages/identity/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import * as z from 'zod'
import { DidDocument } from '@atproto/common-web'

export { didDocument } from '@atproto/common-web'
export type { DidDocument } from '@atproto/common-web'

export type IdentityResolverOpts = {
timeout?: number
Expand Down Expand Up @@ -42,25 +45,3 @@ export interface DidCache {
clearEntry(did: string): Promise<void>
clear(): Promise<void>
}

export const verificationMethod = z.object({
id: z.string(),
type: z.string(),
controller: z.string(),
publicKeyMultibase: z.string().optional(),
})

export const service = z.object({
id: z.string(),
type: z.string(),
serviceEndpoint: z.union([z.string(), z.record(z.unknown())]),
})

export const didDocument = z.object({
id: z.string(),
alsoKnownAs: z.array(z.string()).optional(),
verificationMethod: z.array(verificationMethod).optional(),
service: z.array(service).optional(),
})

export type DidDocument = z.infer<typeof didDocument>
10 changes: 7 additions & 3 deletions packages/pds/src/api/com/atproto/server/createAccount.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { MINUTE } from '@atproto/common'
import { AtprotoData } from '@atproto/identity'
import { InvalidRequestError } from '@atproto/xrpc-server'
import * as plc from '@did-plc/lib'
import disposable from 'disposable-email'
import { normalizeAndValidateHandle } from '../../../../handle'
import * as plc from '@did-plc/lib'
import * as scrypt from '../../../../db/scrypt'
import { Server } from '../../../../lexicon'
import { InputSchema as CreateAccountInput } from '../../../../lexicon/types/com/atproto/server/createAccount'
import { countAll } from '../../../../db/util'
import { UserAlreadyExistsError } from '../../../../services/account'
import AppContext from '../../../../context'
import Database from '../../../../db'
import { AtprotoData } from '@atproto/identity'
import { MINUTE } from '@atproto/common'
import { didDocForSession } from './util'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.server.createAccount({
Expand Down Expand Up @@ -118,11 +119,14 @@ export default function (server: Server, ctx: AppContext) {
}
})

const didDoc = await didDocForSession(ctx, result.did, true)

return {
encoding: 'application/json',
body: {
handle,
did: result.did,
didDoc,
accessJwt: result.accessJwt,
refreshJwt: result.refreshJwt,
},
Expand Down
Loading