Matthieu Sieben a8d6c11235
🚧 OAuth2 - Authorization Server (#2482)
* 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 <devinivy@gmail.com>
2024-06-18 15:11:37 -04:00

4.9 KiB

Universal Handle Resolver implementation for ATPROTO

This package provides a handle resolver implementation for ATPROTO. It is used to resolve handles to their corresponding DID.

This package is meant to be used in any JavaScript environment that support the fetch() function. Because APTORO handle resolution requires DNS resolution, you will need to provide your own DNS resolution function when using this package.

There are two main classes in this package:

  • AtprotoHandleResolver This implements the official ATPROTO handle resolution algorithm (and requires a DNS resolver).
  • AppViewHandleResolver This uses HTTP requests to the Bluesky AppView (bsky.app) to provide handle resolution.

Usage

From a front-end app

Since the ATPROTO handle resolution algorithm requires DNS resolution, and the browser does not provide a built-in DNS resolver, this package offers two options:

  • Delegate handle resolution to an AppView (AppViewHandleResolver). This is the recommended approach for front-end apps.
  • Use a DNS-over-HTTPS (DoH) server (DohHandleResolver). Prefer this method if you don't own an AppView and already have a DoH server that you trust.

Using an AppView:

Caution

Use the Bluesky owned AppView (https://api.bsky.app/), or PDS (https://bsky.social/), at your own risk. Using these servers in a third-party application might expose your users' data (IP address) to Bluesky. Bluesky might log the data sent to it when your app is resolving handles. Bluesky might also change the API, or terms or use, at any time without notice. Make sure you are compliant with the Bluesky terms of use as well as any laws and regulations that apply to your use case.

import { AppViewHandleResolver } from '@atproto-labs/handle-resolver'

const resolver = new AppViewHandleResolver({
  service: 'https://my-app-view.com/',
})
const did = await resolver.resolve('my-handle.bsky.social')

Using DNS-over-HTTPS (DoH) for DNS resolution:

Caution

Using a DoH server that you don't own might expose your users' data to the DoH server provider. The DoH server provider might log the data sent to it by your app, allowing them to track which handles are being resolved by your users. In the browser, it is recommended to use a DoH server that you own and control. Or to implement your own AppView and use the AppViewHandleResolver class.

Note

Using the DohHandleResolver requires a DNS-over-HTTPS server that supports the DNS-over-HTTPS protocol with "application/dns-json" responses.

import { DohHandleResolver } from '@atproto-labs/handle-resolver'

// Also works with 'https://cloudflare-dns.com/dns-query'
const resolver = new DohHandleResolver('https://dns.google/resolve', {
  // Optional: Custom fetch function that will be used both for DNS resolution
  // and well-known resolution.
  fetch: globalThis.fetch.bind(globalThis),
})

const did = await resolver.resolve('my-handle.bsky.social')

From a Node.js app

Note

On a Node.js backend, you will probably want to use the "@atproto-labs/handle-resolver-node" package. The example below applies to Node.js code running on a user's machine (e.g. through Electron).

import { AtprotoHandleResolver } from '@atproto-labs/handle-resolver'
import { resolveTxt } from 'node:dns/promises'

const resolver = new AtprotoHandleResolver({
  // Optional: Custom fetch function (used for well-known resolution)
  fetch: globalThis.fetch.bind(globalThis),

  resolveTxt: async (domain: string) =>
    resolveTxt(domain).then((chunks) => chunks.join('')),
})

Caching

Using a default, in-memory cache, in which items expire after 10 minutes:

import {
  AppViewHandleResolver,
  CachedHandleResolver,
  HandleResolver,
  HandleCache,
} from '@atproto-labs/handle-resolver'

// See previous examples for creating a resolver
declare const sourceResolver: HandleResolver

const resolver = new CachedHandleResolver(sourceResolver)
const did = await resolver.resolve('my-handle.bsky.social')
const did = await resolver.resolve('my-handle.bsky.social') // Result from cache
const did = await resolver.resolve('my-handle.bsky.social') // Result from cache

Using a custom cache:

import {
  AppViewHandleResolver,
  CachedHandleResolver,
  HandleResolver,
  HandleCache,
} from '@atproto-labs/handle-resolver'

// See previous examples for creating a resolver
declare const sourceResolver: HandleResolver

const cache: HandleCache = {
  set(handle, did): Promise<void> {
    /* TODO */
  },
  get(handle): Promise<undefined | string> {
    /* TODO */
  },
  del(handle): Promise<void> {
    /* TODO */
  },
}

const resolver = new CachedHandleResolver(sourceResolver, cache)
const did = await resolver.resolve('my-handle.bsky.social')
const did = await resolver.resolve('my-handle.bsky.social') // Result from cache
const did = await resolver.resolve('my-handle.bsky.social') // Result from cache