Skip to content

Commit

Permalink
🚧 OAuth2 - Authorization Server (bluesky-social#2482)
Browse files Browse the repository at this point in the history
* chore(deps): update zod

* chore(deps): update pino to match entryway version

* chore(tsconfig): remove truncation of types through noErrorTruncation

* add support for DPoP token type when logging

* fix(bsky): JSON.parse does not return value of type JSON

* fix(pds): add res property to ReqCtx

* fix(pds): properly type getPreferences return value

* chore(tsconfig): disable noFallthroughCasesInSwitch

* refactor(pds): move tracer config in own file

* feat(dev-env): start with "pnpm dev"

* feat(oauth): add oauth provider & client libs

* feat(pds): add oauth provider

* chore: changeset

* feat: various fixes and improvements

* chore(deps): update better-sqlite3 to version 10.0.0 for node 22 compatibility

* chore(deps): drop unused tslib

* fix(did): normalize service IDs before looking for duplicates

* fix(did): avoid minor type casting

* fix(did): improve argument validation

* fix(fetch): explicit use of negation around number comparison

* fix(oauth-provider): improve argument validation

* feat(did): add ATPROTO specific "isAtprotoDidWeb" method

* feat(rollup-plugin-bundle-manifest): add readme

* feat(lint): add eqeqeq rule (only allow == and != with null)

* fix(oauth-client-browser): typo in gitignore

* fix(oauth-provider): properly name error class file

* fix(oauth-provider): remove un-necessary useMemo

* fix(did-resolver): properly build did:web document url

* fix(did-resolver): remove unused types

* fix(fetch): remove unused utils

* fix(pds): remove unused script and dependency

* fix(oauth-provider): simplify isSubPath util

* fix(oauth-provider): add InvalidRedirectUriError static constructor

* fix(jwk): improve JWT validation to provide better error messages and distinguish between signed and unsigned tokens

* fix(pds): use "debug" log level for fetch method

* fix(pds): allow access tokens to contain an unknown "typ" claim (with the exception of "dpop+jwt")

* fix(jwk): remove un-necessary code

* fix(pds): account for whitespace chars when checking JSON

* fix(pds): remove oauth specific config

* fix(pds): run all write queries through transaction or executeWithRetry
fix(pds): remove outdated comments
fix(pds): rename used_refresh_token columns & added primary key
fix(pds): run cleanup task through backgroundQueue
fix(pds): add device.id foreign key to device_account
fix(pds): add comment on cleanup of used_refresh_token
fix(pds): add primary key on device_account

* fix(oauth-provider:time): simplify constantTime util

* fix(pds): rename disableSsrf into disableSsrfProtection

* fix(oauth-client-react-native): remove incomplete package

* refactor(pds): remove status & active from ActorAccount

* fix(pds): invalidate all oauth tokens on takedown

* fix(oauth-provider): enforce token expiry

* fix(pds): properly support deactivated accounts

* perf(pds:db): allow transaction function to be sync

* refactor(psq:account-manager): expose only query builders & data transformations utils from helpers

* fix(oauth-provider): imports from self

* fix(ci): add nested packages to build artifacts

* style(fetch): rename TODO into @todo

* style(rollup-plugin-bundle-manifest): remove "TODO" from comment

* style(oauth-client): rename TODO into @todo

* style(oauth-provider): rename TODO into @todo

* refactor(oauth-client): remove "OAuth" prefix from types

* fix(oauth-client-browser): better type SessionListener

* style(oauth): rename TODO into @todo

* fix(oauth-provider): enforce provider max session age

* fix(oauth-provider): check authentication parameters against all client metadata

* fix(api): tests

* fix(pds): remove .js from imports for tests

* fix(pds): change account status to match tests

* chore(deps): make all packages depend on the same zod version

* fix(common-web): remove un-necessary binding of Checkable to "zod"

* refactor(jwk): infer jwt schema from refinement definition

* fix(handle-resolver): allow resolution errors to propagate
docs(handle-resolver): better handling of DNS resolution errors
fix(handle-resolver): properly handle DOH responses

* fix(did): service endpoint arrays must contain "one or more" element

* refactor(pipe): simplify implementation

* fix(pds): add missing DB indexes

* feat(oauth): Resolve Authorization Server URI through Protected Resource Metadata

* style:(oauth-client): import order

* docs(oauth-provider:redirect-uri): add reference url

* feat(oauth): implement "OAuth Client ID Metadata Document" from draft-parecki-oauth-client-id-metadata-document-latest internet draft

* feat(oauth-client): backport changes from feat-oauth-client

* docs(simple-store): improve comments

* feat(lexicons): add iterable capabilities

* fix(pds): type error in dev mode

* feat(oauth-provider): improved error reporting

* fix(oauth-types): allow insecure issuer during tests

* fix(xrpc-server): allow upload of empty files

* fix: lint

* feat(fetch): keep request reference in errors
feat(fetch): utilities improvements

* fix(pds): allow more than one session token per user

* feat(ozone): improve env validation error messages

* fix(oauth-client): account for DPoP when checking for invalid_token errors

* fixup! feat(fetch): keep request reference in errors feat(fetch): utilities improvements

* fixup! feat(fetch): keep request reference in errors feat(fetch): utilities improvements

* fix(oauth): various validation fixes
feat(oauth): share client_id validation and parsing utilities between client & provider

* feat(dev-env): fix ozone port number

* fix(fetch-node): prevent fetch against invalid domain names

* fix(oauth-provider): add typings for psl dep

* feat(jwk): make type def compatible with TS 4.x

* fix(oauth): fixed various spec compliance
fix(oauth): return "sub" in refresh token response
fix(oauth): limit token validity for third party clients
fix(oauth): hide client image when not trusted

* fix(oauth): lint

* pds: switch changeset to patch, no breaking changes

* changeset and config for new oauth deps

---------

Co-authored-by: Devin Ivy <[email protected]>
  • Loading branch information
matthieusieben and devinivy authored Jun 18, 2024
1 parent 80dae83 commit a8d6c11
Show file tree
Hide file tree
Showing 448 changed files with 26,273 additions and 629 deletions.
23 changes: 23 additions & 0 deletions .changeset/clever-monkeys-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@atproto/pds": patch
"@atproto-labs/rollup-plugin-bundle-manifest": minor
"@atproto-labs/handle-resolver-node": minor
"@atproto-labs/simple-store-memory": minor
"@atproto-labs/identity-resolver": minor
"@atproto/oauth-client-browser": minor
"@atproto-labs/handle-resolver": minor
"@atproto-labs/did-resolver": minor
"@atproto-labs/simple-store": minor
"@atproto/oauth-provider": minor
"@atproto-labs/fetch-node": minor
"@atproto/jwk-webcrypto": minor
"@atproto/oauth-client": minor
"@atproto/oauth-types": minor
"@atproto-labs/fetch": minor
"@atproto/jwk-jose": minor
"@atproto-labs/pipe": minor
"@atproto/jwk": minor
"@atproto/did": minor
---

Add OAuth provider capability & support for DPoP signed tokens
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"no-var": "error",
"prefer-const": "warn",
"no-misleading-character-class": "warn",
"eqeqeq": ["error", "always", { "null": "ignore" }],
"@typescript-eslint/no-unused-vars": [
"warn",
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/repo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: dist
path: packages/*/dist
path: |
packages/*/dist
packages/*/*/dist
retention-days: 1
test:
name: Test
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ node_modules
lerna-debug.log
npm-debug.log
yarn-error.log
packages/*/dist
packages/**/dist
.idea
packages/*/coverage
.vscode/
test.sqlite
.DS_Store
*.log
tsconfig.build.tsbuildinfo
*.tsbuildinfo
.*.env
.env
\#*\#
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"verify:types": "tsc --build tsconfig.json",
"format": "pnpm lint:fix && pnpm style:fix",
"build": "pnpm --recursive --stream build",
"dev": "pnpm --stream '/^dev:.+$/'",
"dev": "NODE_ENV=development pnpm --stream '/^dev:.+$/'",
"dev:tsc": "tsc --build tsconfig.json --watch",
"dev:pkg": "pnpm --recursive --parallel --stream dev",
"test": "LOG_ENABLED=false ./packages/dev-infra/with-test-redis-and-db.sh pnpm --stream -r test",
Expand Down Expand Up @@ -51,7 +51,9 @@
},
"workspaces": {
"packages": [
"packages/*"
"packages/*",
"packages/oauth/*",
"packages/internal/*"
]
}
}
1 change: 1 addition & 0 deletions packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Crypto](./crypto): Atproto's common cryptographic operations.
- [Syntax](./syntax): A library for identifier syntax: NSID, AT URI, handles, etc.
- [Lexicon](./lexicon): A library for validating data using atproto's schema system.
- [OAuth Provider](./oauth/oauth-provider): A library for supporting ATPROTO's OAuth.
- [Repo](./repo): The "atproto repository" core implementation (a Merkle Search Tree).
- [XRPC](./xrpc): An XRPC client implementation.
- [XRPC Server](./xrpc-server): An XRPC server implementation.
Expand Down
2 changes: 1 addition & 1 deletion packages/api/tests/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ describe('agent', () => {

expect(events.length).toEqual(2)
expect(events[0]).toEqual('create-failed')
expect(events[1]).toEqual('network-error')
expect(events[1]).toEqual('expired')
expect(sessions.length).toEqual(2)
expect(typeof sessions[0]).toEqual('undefined')
expect(typeof sessions[1]).toEqual('undefined')
Expand Down
2 changes: 1 addition & 1 deletion packages/bsky/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"multiformats": "^9.9.0",
"p-queue": "^6.6.2",
"pg": "^8.10.0",
"pino": "^8.15.0",
"pino": "^8.21.0",
"pino-http": "^8.2.1",
"sharp": "^0.32.6",
"structured-headers": "^1.0.1",
Expand Down
4 changes: 1 addition & 3 deletions packages/bsky/src/hydration/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ export const parseRecordBytes = <T>(
return parseJsonBytes(bytes) as T
}

export const parseJsonBytes = (
bytes: Uint8Array | undefined,
): JSON | undefined => {
export const parseJsonBytes = (bytes: Uint8Array | undefined): unknown => {
if (!bytes || bytes.byteLength === 0) return
const parsed = JSON.parse(ui8.toString(bytes, 'utf8'))
return parsed ?? undefined
Expand Down
119 changes: 81 additions & 38 deletions packages/bsky/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import pino from 'pino'
import { stdSerializers } from 'pino'
import pinoHttp from 'pino-http'
import * as jose from 'jose'
import { subsystemLogger } from '@atproto/common'
import { parseBasicAuth } from './auth-verifier'

export const dbLogger: ReturnType<typeof subsystemLogger> =
subsystemLogger('bsky:db')
Expand All @@ -20,40 +18,85 @@ export const httpLogger: ReturnType<typeof subsystemLogger> =
export const loggerMiddleware = pinoHttp({
logger: httpLogger,
serializers: {
err: (err) => {
return {
code: err?.code,
message: err?.message,
}
},
req: (req) => {
const serialized = pino.stdSerializers.req(req)
const authHeader = serialized.headers.authorization || ''
let auth: string | undefined = undefined
if (authHeader.startsWith('Bearer ')) {
const token = authHeader.slice('Bearer '.length)
const { iss } = jose.decodeJwt(token)
if (iss) {
auth = 'Bearer ' + iss
} else {
auth = 'Bearer Invalid'
}
}
if (authHeader.startsWith('Basic ')) {
const parsed = parseBasicAuth(authHeader)
if (!parsed) {
auth = 'Basic Invalid'
} else {
auth = 'Basic ' + parsed.username
}
}
return {
...serialized,
headers: {
...serialized.headers,
authorization: auth,
},
}
},
err: errSerializer,
req: reqSerializer,
},
})

function errSerializer(err: any) {
return {
code: err?.code,
message: err?.message,
}
}

function reqSerializer(req: any) {
const serialized = stdSerializers.req(req)
serialized.headers = obfuscateHeaders(serialized.headers)
return serialized
}

function obfuscateHeaders(headers: Record<string, string>) {
const obfuscatedHeaders: Record<string, string> = {}
for (const key in headers) {
if (key.toLowerCase() === 'authorization') {
obfuscatedHeaders[key] = obfuscateAuthHeader(headers[key])
} else if (key.toLowerCase() === 'dpop') {
obfuscatedHeaders[key] = obfuscateJws(headers[key]) || 'Invalid'
} else {
obfuscatedHeaders[key] = headers[key]
}
}
return obfuscatedHeaders
}

function obfuscateAuthHeader(authHeader: string): string {
// This is a hot path (runs on every request). Avoid using split() or regex.

const spaceIdx = authHeader.indexOf(' ')
if (spaceIdx === -1) return 'Invalid'

const type = authHeader.slice(0, spaceIdx)
switch (type.toLowerCase()) {
case 'bearer':
return `${type} ${obfuscateBearer(authHeader.slice(spaceIdx + 1))}`
case 'dpop':
return `${type} ${obfuscateJws(authHeader.slice(spaceIdx + 1)) || 'Invalid'}`
case 'basic':
return `${type} ${obfuscateBasic(authHeader.slice(spaceIdx + 1)) || 'Invalid'}`
default:
return `Invalid`
}
}

function obfuscateBasic(token: string): null | string {
if (!token) return null
const buffer = Buffer.from(token, 'base64')
if (!buffer.length) return null // Buffer.from will silently ignore invalid base64 chars
const authHeader = buffer.toString('utf8')
const colIdx = authHeader.indexOf(':')
if (colIdx === -1) return null
const username = authHeader.slice(0, colIdx)
return `${username}:***`
}

function obfuscateBearer(token: string): string {
return obfuscateJws(token) || obfuscateToken(token)
}

function obfuscateToken(token: string): string {
return token ? '***' : ''
}

function obfuscateJws(token: string): null | string {
const firstDot = token.indexOf('.')
if (firstDot === -1) return null

const secondDot = token.indexOf('.', firstDot + 1)
if (secondDot === -1) return null

if (token.indexOf('.', secondDot + 1) !== -1) return null

// Strip the signature
return token.slice(0, secondDot) + '.obfuscated'
}
2 changes: 1 addition & 1 deletion packages/common-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"graphemer": "^1.4.0",
"multiformats": "^9.9.0",
"uint8arrays": "3.0.0",
"zod": "^3.21.4"
"zod": "^3.23.8"
},
"devDependencies": {
"jest": "^28.1.2"
Expand Down
5 changes: 3 additions & 2 deletions packages/common-web/src/check.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ZodError } from 'zod'
// Explicitly not using "zod" types here to avoid mismatching types due to
// version differences.

export interface Checkable<T> {
parse: (obj: unknown) => T
safeParse: (
obj: unknown,
) => { success: true; data: T } | { success: false; error: ZodError }
) => { success: true; data: T } | { success: false; error: Error }
}

export interface Def<T> {
Expand Down
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"cbor-x": "^1.5.1",
"iso-datestring-validator": "^2.2.2",
"multiformats": "^9.9.0",
"pino": "^8.15.0"
"pino": "^8.21.0"
},
"devDependencies": {
"jest": "^28.1.2",
Expand Down
3 changes: 2 additions & 1 deletion packages/dev-env/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"bin": "dist/bin.js",
"scripts": {
"build": "tsc --build tsconfig.build.json",
"start": "../dev-infra/with-test-redis-and-db.sh node dist/bin.js"
"start": "../dev-infra/with-test-redis-and-db.sh node dist/bin.js",
"dev": "../dev-infra/with-test-redis-and-db.sh node --watch dist/bin.js"
},
"dependencies": {
"@atproto/api": "workspace:^",
Expand Down
1 change: 1 addition & 0 deletions packages/dev-env/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const run = async () => {
},
plc: { port: 2582 },
ozone: {
port: 2587,
chatUrl: 'http://localhost:2590', // must run separate chat service
chatDid: 'did:example:chat',
},
Expand Down
10 changes: 10 additions & 0 deletions packages/dev-env/src/pds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ export class TestPds {
modServiceDid: 'did:example:invalid',
plcRotationKeyK256PrivateKeyHex: plcRotationPriv,
inviteRequired: false,
fetchDisableSsrfProtection: true,
serviceName: 'Development PDS',
primaryColor: '#ffcb1e',
errorColor: undefined,
logoUrl:
'https://uxwing.com/wp-content/themes/uxwing/download/animals-and-birds/bee-icon.png',
homeUrl: 'https://bsky.social/',
termsOfServiceUrl: 'https://bsky.social/about/support/tos',
privacyPolicyUrl: 'https://bsky.social/about/support/privacy-policy',
supportUrl: 'https://blueskyweb.zendesk.com/hc/en-us',
...config,
}
const cfg = pds.envToCfg(env)
Expand Down
36 changes: 36 additions & 0 deletions packages/did/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@atproto/did",
"version": "0.0.1",
"license": "MIT",
"description": "DID resolution and verification library",
"keywords": [
"atproto",
"did",
"validation",
"types"
],
"homepage": "https://atproto.com",
"repository": {
"type": "git",
"url": "https://github.com/bluesky-social/atproto",
"directory": "packages/did"
},
"type": "commonjs",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"dependencies": {
"zod": "^3.23.8"
},
"devDependencies": {
"typescript": "^5.3.3"
},
"scripts": {
"build": "tsc --build tsconfig.build.json"
}
}
Loading

0 comments on commit a8d6c11

Please sign in to comment.