Skip to content

Commit

Permalink
feat(pds): add oauth provider
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieusieben committed May 16, 2024
1 parent d2b6a93 commit a9ee31b
Show file tree
Hide file tree
Showing 34 changed files with 2,414 additions and 472 deletions.
24 changes: 21 additions & 3 deletions packages/pds/example.env
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# See more env options in src/config/env.ts
# Hostname - the public domain that you intend to deploy your service at
PDS_HOSTNAME="example.com"
PDS_PORT="2583"

# Database config - use one or the other
PDS_DB_SQLITE_LOCATION="db.test"
# PDS_DB_POSTGRES_URL="postgresql://pg:password@localhost:5433/postgres"
PDS_DATA_DIRECTORY="data"

# Blobstore - filesystem location to store uploaded blobs
PDS_BLOBSTORE_DISK_LOCATION="blobs"
Expand All @@ -14,11 +14,29 @@ PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX="3ee68..."
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX="e049f..."

# Secrets - update to secure high-entropy strings
PDS_DPOP_SECRET="32-random-bytes-hex-encoded"
PDS_JWT_SECRET="jwt-secret"
PDS_ADMIN_PASSWORD="admin-pass"

# Environment - example is for sandbox
PDS_DID_PLC_URL="https://plc.bsky-sandbox.dev"
PDS_BSKY_APP_VIEW_ENDPOINT="https://api.bsky-sandbox.dev"
PDS_BSKY_APP_VIEW_DID="did:web:api.bsky-sandbox.dev"
PDS_CRAWLERS="https://bgs.bsky-sandbox.dev"
PDS_CRAWLERS="https://bgs.bsky-sandbox.dev"

# OAuth Provider
PDS_OAUTH_PROVIDER_NAME="John's self hosted PDS"
PDS_OAUTH_PROVIDER_LOGO=
PDS_OAUTH_PROVIDER_PRIMARY_COLOR="#7507e3"
PDS_OAUTH_PROVIDER_ERROR_COLOR=
PDS_OAUTH_PROVIDER_HOME_LINK=
PDS_OAUTH_PROVIDER_TOS_LINK=
PDS_OAUTH_PROVIDER_POLICY_LINK=
PDS_OAUTH_PROVIDER_SUPPORT_LINK=

# Debugging
NODE_TLS_REJECT_UNAUTHORIZED=1
LOG_ENABLED=0
LOG_LEVEL=info
PDS_INVITE_REQUIRED=1
PDS_DISABLE_SSRF=0
2 changes: 2 additions & 0 deletions packages/pds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
"migration:create": "ts-node ./bin/migration-create.ts"
},
"dependencies": {
"@atproto-labs/fetch-node": "workspace:*",
"@atproto/api": "workspace:^",
"@atproto/aws": "workspace:^",
"@atproto/common": "workspace:^",
"@atproto/crypto": "workspace:^",
"@atproto/identity": "workspace:^",
"@atproto/lexicon": "workspace:^",
"@atproto/oauth-provider": "workspace:^",
"@atproto/repo": "workspace:^",
"@atproto/syntax": "workspace:^",
"@atproto/xrpc": "workspace:^",
Expand Down
99 changes: 99 additions & 0 deletions packages/pds/src/account-manager/db/migrations/003-oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Kysely, sql } from 'kysely'

export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('authorization_request')
.addColumn('id', 'varchar', (col) => col.primaryKey())
.addColumn('did', 'varchar')
.addColumn('deviceId', 'varchar')
.addColumn('clientId', 'varchar', (col) => col.notNull())
.addColumn('clientAuth', 'varchar', (col) => col.notNull())
.addColumn('parameters', 'varchar', (col) => col.notNull())
.addColumn('expiresAt', 'varchar', (col) => col.notNull())
.addColumn('code', 'varchar')
.execute()

await db.schema
.createIndex('authorization_request_code_idx')
.unique()
.on('authorization_request')
// https://github.com/kysely-org/kysely/issues/302
.expression(sql`code DESC) WHERE (code IS NOT NULL`)
.execute()

await db.schema
.createIndex('authorization_request_expires_at_idx')
.on('authorization_request')
.column('expiresAt')
.execute()

await db.schema
.createTable('device')
.addColumn('id', 'varchar', (col) => col.primaryKey())
.addColumn('sessionId', 'varchar', (col) => col.notNull())
.addColumn('userAgent', 'varchar')
.addColumn('ipAddress', 'varchar', (col) => col.notNull())
.addColumn('lastSeenAt', 'varchar', (col) => col.notNull())
.addUniqueConstraint('device_session_id_idx', ['sessionId'])
.execute()

await db.schema
.createTable('device_account')
.addColumn('did', 'varchar', (col) => col.notNull())
.addColumn('deviceId', 'varchar', (col) => col.notNull())
.addColumn('authenticatedAt', 'varchar', (col) => col.notNull())
.addColumn('remember', 'boolean', (col) => col.notNull())
.addColumn('authorizedClients', 'varchar', (col) => col.notNull())
.addUniqueConstraint('device_account_did_device_id_idx', [
'deviceId', // first because this table will be joined from the "device" table
'did',
])
.execute()

await db.schema
.createTable('token')
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
.addColumn('did', 'varchar', (col) => col.notNull())
.addColumn('tokenId', 'varchar', (col) => col.notNull())
.addColumn('createdAt', 'varchar', (col) => col.notNull())
.addColumn('updatedAt', 'varchar', (col) => col.notNull())
.addColumn('expiresAt', 'varchar', (col) => col.notNull())
.addColumn('clientId', 'varchar', (col) => col.notNull())
.addColumn('clientAuth', 'varchar', (col) => col.notNull())
.addColumn('deviceId', 'varchar')
.addColumn('parameters', 'varchar', (col) => col.notNull())
.addColumn('details', 'varchar')
.addColumn('code', 'varchar')
.addColumn('currentRefreshToken', 'varchar')
.addUniqueConstraint('token_current_refresh_token_unique_idx', [
'currentRefreshToken',
])
.addUniqueConstraint('token_id_unique_idx', ['tokenId'])
.execute()

await db.schema
.createIndex('token_code_idx')
.unique()
.on('token')
// https://github.com/kysely-org/kysely/issues/302
.expression(sql`code DESC) WHERE (code IS NOT NULL`)
.execute()

await db.schema
.createTable('used_refresh_token')
.addColumn('id', 'integer', (col) => col.notNull())
.addColumn('usedRefreshToken', 'varchar', (col) => col.notNull())
.addUniqueConstraint('used_refresh_token_used_refresh_token_idx', [
'usedRefreshToken',
'id',
])
.execute()
}

export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('used_refresh_token').execute()
await db.schema.dropTable('token').execute()
await db.schema.dropTable('device_account').execute()
await db.schema.dropTable('device').execute()
await db.schema.dropTable('authorization_request').execute()
}
2 changes: 2 additions & 0 deletions packages/pds/src/account-manager/db/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as mig001 from './001-init'
import * as mig002 from './002-account-deactivation'
import * as mig003 from './003-oauth'

export default {
'001': mig001,
'002': mig002,
'003': mig003,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
Code,
DeviceId,
OAuthClientId,
RequestId,
} from '@atproto/oauth-provider'
import { Selectable } from 'kysely'
import { DateISO, JsonObject } from '../../../db'

export interface AuthorizationRequest {
id: RequestId
did: string | null
deviceId: DeviceId | null

clientId: OAuthClientId
clientAuth: JsonObject
parameters: JsonObject
expiresAt: DateISO // TODO: Index this
code: Code | null
}

export type AuthorizationRequestEntry = Selectable<AuthorizationRequest>

export const tableName = 'authorization_request'

export type PartialDB = { [tableName]: AuthorizationRequest }
15 changes: 15 additions & 0 deletions packages/pds/src/account-manager/db/schema/device-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DeviceId } from '@atproto/oauth-provider'
import { DateISO, JsonArray } from '../../../db'

export interface DeviceAccount {
did: string
deviceId: DeviceId

authenticatedAt: DateISO
authorizedClients: JsonArray
remember: 0 | 1
}

export const tableName = 'device_account'

export type PartialDB = { [tableName]: DeviceAccount }
18 changes: 18 additions & 0 deletions packages/pds/src/account-manager/db/schema/device.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DeviceId, SessionId } from '@atproto/oauth-provider'
import { Selectable } from 'kysely'
import { DateISO } from '../../../db'

export interface Device {
id: DeviceId
sessionId: SessionId

userAgent: string | null
ipAddress: string
lastSeenAt: DateISO
}

export type DeviceEntry = Selectable<Device>

export const tableName = 'device'

export type PartialDB = { [tableName]: Device }
15 changes: 15 additions & 0 deletions packages/pds/src/account-manager/db/schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as actor from './actor'
import * as account from './account'
import * as device from './device.js'
import * as deviceAccount from './device-account.js'
import * as oauthRequest from './authorization-request.js'
import * as token from './token.js'
import * as usedRefreshToken from './used-refresh-token.js'
import * as repoRoot from './repo-root'
import * as refreshToken from './refresh-token'
import * as appPassword from './app-password'
Expand All @@ -8,6 +13,11 @@ import * as emailToken from './email-token'

export type DatabaseSchema = actor.PartialDB &
account.PartialDB &
device.PartialDB &
deviceAccount.PartialDB &
oauthRequest.PartialDB &
token.PartialDB &
usedRefreshToken.PartialDB &
refreshToken.PartialDB &
appPassword.PartialDB &
repoRoot.PartialDB &
Expand All @@ -16,6 +26,11 @@ export type DatabaseSchema = actor.PartialDB &

export type { Actor, ActorEntry } from './actor'
export type { Account, AccountEntry } from './account'
export type { Device } from './device'
export type { DeviceAccount } from './device-account'
export type { AuthorizationRequest } from './authorization-request'
export type { Token } from './token'
export type { UsedRefreshToken } from './used-refresh-token'
export type { RepoRoot } from './repo-root'
export type { RefreshToken } from './refresh-token'
export type { AppPassword } from './app-password'
Expand Down
34 changes: 34 additions & 0 deletions packages/pds/src/account-manager/db/schema/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
Code,
DeviceId,
OAuthClientId,
RefreshToken,
Sub,
TokenId,
} from '@atproto/oauth-provider'
import { Generated, Selectable } from 'kysely'

import { DateISO, JsonArray, JsonObject } from '../../../db/cast.js'

export interface Token {
id: Generated<number>
did: Sub

tokenId: TokenId
createdAt: DateISO
updatedAt: DateISO
expiresAt: DateISO
clientId: OAuthClientId
clientAuth: JsonObject
deviceId: DeviceId | null
parameters: JsonObject
details: JsonArray | null
code: Code | null
currentRefreshToken: RefreshToken | null // TODO: Index this
}

export type TokenEntry = Selectable<Token>

export const tableName = 'token'

export type PartialDB = { [tableName]: Token }
13 changes: 13 additions & 0 deletions packages/pds/src/account-manager/db/schema/used-refresh-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { RefreshToken } from '@atproto/oauth-provider'
import { Selectable } from 'kysely'

export interface UsedRefreshToken {
id: number // TODO: Index this (foreign key to token)
usedRefreshToken: RefreshToken
}

export type UsedRefreshTokenEntry = Selectable<UsedRefreshToken>

export const tableName = 'used_refresh_token'

export type PartialDB = { [tableName]: UsedRefreshToken }
2 changes: 1 addition & 1 deletion packages/pds/src/account-manager/helpers/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type AvailabilityFlags = {
includeDeactivated?: boolean
}

const selectAccountQB = (db: AccountDb, flags?: AvailabilityFlags) => {
export const selectAccountQB = (db: AccountDb, flags?: AvailabilityFlags) => {
const { includeTakenDown = false, includeDeactivated = false } = flags ?? {}
const { ref } = db.db.dynamic
return db.db
Expand Down
Loading

0 comments on commit a9ee31b

Please sign in to comment.