Matthieu Sieben f9dc9aa4c9
Permission set (#4108)
* Export constants and type assertion utilities

* Add permission set support to oauth provider

* improve permission set parsing

* Rename `PermissionSet` to `ScopePermissions`

* Improve performance of NSID validation

* Add support for `permission-set` in lexicon document

* Validate NSID syntax using `@atproto/syntax`

* Export all types used in public interfaces (from `lexicon-resolver`)

* Small performance improvement

* Rework scope parsing utilities to work with Lexicon defined permissions

* file rename

* fixup! Rework scope parsing utilities to work with Lexicon defined permissions

* removed outdated comment

* removed outdated comment

* fix comment typo

* Improve `SimpleStore` api

* permission-set NSID auth scopes

* Remove dev dependency on dev-env

* fix build script

* pnpm-lock

* Improve fetch-node unicast protection

* Explicitly set the `redirect: "follow"` `fetch()` option

* Add delay when building oauth-provider-ui in watch mode

* Remove external dependencies from auth-scopes

* Add customizable lexicon authority to pds (for dev purposes)

* fix pds migration

* update permission-set icon

* Add support for `include:` syntax in scopes

* tidy

* Renaming of "resource" concept to better reflect the fact that not all oauth scope values are about resources

* changeset

* ui improvmeents

* i18n

* ui imporvements

* add `AtprotoAudience` type

* Enforce proper formatting of audience (atproto supported did + fragment part)

* tidy

* tidy

* tidy

* fix ci ?

* ci fix ?

* tidy ?

* Apply consistent outline around focusable items

* Use `inheritAud: true` to control `aud` inheritance

* Update packages/oauth/oauth-provider/src/lexicon/lexicon-manager.ts

Co-authored-by: devin ivy <devinivy@gmail.com>

* Review comments

* Add `nsid` property to `LexiconResolutionError`

* improve nsid validation

* i18n

* Improve oauth scope parsing

* Simplify lex scope parsing

* tidy

* docs

* tidy

* ci

* Code simplification

* tidy

* improve type safety

* improve deps graph

* naming

* Improve tests and package structure

* Improve error when resolving a non permission-set

* improve nsid parsing perfs

* benchmark

* Refactor ozone and lexicon into using a common service profile mechanism

* improve perfs

* ci fix (?)

* tidy

* Allow storage of valid lexicons in lexicon store

* Improve handling of lexicon resolution failures

* review comment

* Test both regexp and non regexp based nsid validation

* properly detect presence of port number in https did:web

* Re-enable logging of `safeFetch` requests

* tidy

---------

Co-authored-by: devin ivy <devinivy@gmail.com>
2025-08-29 12:19:19 +02:00

121 lines
4.1 KiB
TypeScript

import {
DEFAULT_FORBIDDEN_DOMAIN_NAMES,
Fetch,
asRequest,
explicitRedirectCheckRequestTransform,
fetchMaxSizeProcessor,
forbiddenDomainNameRequestTransform,
protocolCheckRequestTransform,
requireHostHeaderTransform,
timedFetch,
} from '@atproto-labs/fetch'
import { pipe } from '@atproto-labs/pipe'
import { UnicastFetchWrapOptions, unicastFetchWrap } from './unicast.js'
export type SafeFetchWrapOptions<C> = UnicastFetchWrapOptions<C> & {
responseMaxSize?: number
ssrfProtection?: boolean
allowCustomPort?: boolean
allowData?: boolean
allowHttp?: boolean
allowIpHost?: boolean
allowPrivateIps?: boolean
timeout?: number
forbiddenDomainNames?: Iterable<string>
/**
* When `false`, a {@link RequestInit['redirect']} value must be explicitly
* provided as second argument to the returned function or requests will fail.
*
* @default false
*/
allowImplicitRedirect?: boolean
}
/**
* Wrap a fetch function with safety checks so that it can be safely used
* with user provided input (URL).
*
* @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}
*
* @note When {@link SafeFetchWrapOptions.allowImplicitRedirect} is `false`
* (default), then the returned function **must** be called setting the second
* argument's `redirect` property to one of the allowed values. Otherwise, if
* the returned fetch function is called with a `Request` object (and no
* explicit `redirect` init object), then the verification code will not be able
* to determine if the `redirect` property was explicitly set or based on the
* default value (`follow`), causing it to preventively block the request (throw
* an error). For this reason, unless you set
* {@link SafeFetchWrapOptions.allowImplicitRedirect} to `true`, you should
* **not** wrap the returned function into another function that creates a
* {@link Request} object before passing it to the function (as a e.g. a logging
* function would).
*/
export function safeFetchWrap<C>({
fetch = globalThis.fetch as Fetch<C>,
dangerouslyForceKeepAliveAgent = false,
responseMaxSize = 512 * 1024, // 512kB
ssrfProtection = true,
allowCustomPort = !ssrfProtection,
allowData = false,
allowHttp = !ssrfProtection,
allowIpHost = true,
allowPrivateIps = !ssrfProtection,
timeout = 10e3,
forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES as Iterable<string>,
allowImplicitRedirect = false,
}: SafeFetchWrapOptions<C> = {}) {
return pipe(
/**
* Require explicit {@link RequestInit['redirect']} mode
*/
allowImplicitRedirect ? asRequest : explicitRedirectCheckRequestTransform(),
/**
* Only requests that will be issued with a "Host" header are allowed.
*/
allowIpHost ? asRequest : requireHostHeaderTransform(),
/**
* Prevent using http:, file: or data: protocols.
*/
protocolCheckRequestTransform({
'about:': false,
'data:': allowData,
'file:': false,
'http:': allowHttp && { allowCustomPort },
'https:': { allowCustomPort },
}),
/**
* Disallow fetching from domains we know are not atproto/OIDC client
* implementation. Note that other domains can be blocked by providing a
* custom fetch function combined with another
* forbiddenDomainNameRequestTransform.
*/
forbiddenDomainNameRequestTransform(forbiddenDomainNames),
/**
* Since we will be fetching from the network based on user provided
* input, let's mitigate resource exhaustion attacks by setting a timeout.
*/
timedFetch(
timeout,
/**
* Since we will be fetching from the network based on user provided
* input, we need to make sure that the request is not vulnerable to SSRF
* attacks.
*/
allowPrivateIps
? fetch
: unicastFetchWrap({ fetch, dangerouslyForceKeepAliveAgent }),
),
/**
* Since we will be fetching user owned data, we need to make sure that an
* attacker cannot force us to download a large amounts of data.
*/
fetchMaxSizeProcessor(responseMaxSize),
) satisfies Fetch<unknown>
}