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

149 lines
4.9 KiB
Markdown

# 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.
```ts
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.
```ts
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).
```ts
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:
```ts
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:
```ts
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
```