Skip to content

Commit

Permalink
fixup! feat(oauth): add oauth provider & client libs
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieusieben committed May 16, 2024
1 parent 1c9d30e commit 5f7493f
Show file tree
Hide file tree
Showing 42 changed files with 769 additions and 611 deletions.
4 changes: 3 additions & 1 deletion packages/did/src/did-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ export const didDocumentSchema = z.object({
z
.array(z.string().url())
.nonempty()
.refine((data) => data[0] === 'https://www.w3.org/ns/did/v1'),
.refine((data) => data[0] === 'https://www.w3.org/ns/did/v1', {
message: 'First @context must be https://www.w3.org/ns/did/v1',
}),
]),
id: didSchema,
controller: didControllerSchema.optional(),
Expand Down
2 changes: 1 addition & 1 deletion packages/did/src/did.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export function checkDid(input: unknown): asserts input is Did {
checkDidMsid(input, idSep + 1, length)
}

export function isDid(input: string): input is Did {
export function isDid(input: unknown): input is Did {
try {
checkDid(input)
return true
Expand Down
6 changes: 3 additions & 3 deletions packages/internal/did-resolver/src/did-cache-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import {

import { DidCache } from './did-cache.js'

export type DidCacheMemoryOptions = SimpleStoreMemoryOptions<Did, DidDocument>
const DEFAULT_TTL = 3600 * 1000 // 1 hour
const DEFAULT_MAX_SIZE = 50 * 1024 * 1024 // ~50MB

export const DEFAULT_TTL = 3600 * 1000 // 1 hour
export const DEFAULT_MAX_SIZE = 50 * 1024 * 1024 // ~50MB
export type DidCacheMemoryOptions = SimpleStoreMemoryOptions<Did, DidDocument>

export class DidCacheMemory
extends SimpleStoreMemory<Did, DidDocument>
Expand Down
37 changes: 23 additions & 14 deletions packages/internal/did-resolver/src/did-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
import { Did, DidDocument } from '@atproto/did'
import { CachedGetter } from '@atproto-labs/simple-store'
import { Did, DidDocument } from '@atproto/did'

import { DidCacheMemory } from './did-cache-memory.js'
import { DidCache } from './did-cache.js'
import { DidMethod, DidMethods, ResolveOptions } from './did-method.js'
import { DidMethod, ResolveOptions } from './did-method.js'
import { DidResolverBase, ResolvedDocument } from './did-resolver-base.js'
import { DidPlcMethod, DidPlcMethodOptions } from './methods/plc.js'
import { DidWebMethod, DidWebMethodOptions } from './methods/web.js'
import { Simplify } from './util.js'

export type { DidMethod, ResolveOptions, ResolvedDocument }

export type DidResolverOptions = {
cache?: DidCache
}
export type MethodsOptions = Simplify<DidPlcMethodOptions & DidWebMethodOptions>
export type DidResolverOptions = Simplify<{ cache?: DidCache } & MethodsOptions>

export class DidResolver<M extends string = string> extends DidResolverBase<M> {
readonly #getter: CachedGetter<Did, DidDocument>
export class DidResolver extends DidResolverBase<'plc' | 'web'> {
private readonly getter: CachedGetter<Did, DidDocument>

constructor(methods: DidMethods<M>, options?: DidResolverOptions) {
super(methods)
constructor({
cache = new DidCacheMemory(),
...methodsOptions
}: DidResolverOptions = {}) {
super({
plc: new DidPlcMethod(methodsOptions),
web: new DidWebMethod(methodsOptions),
})

this.#getter = new CachedGetter<Did, DidDocument>(
this.getter = new CachedGetter<Did, DidDocument>(
(did, options) => super.resolve(did, options),
options?.cache ?? new DidCacheMemory(),
cache,
)
}

async resolve<D extends Did>(
did: D,
options?: ResolveOptions,
): Promise<ResolvedDocument<D, M>>
async resolve(did: Did, options?: ResolveOptions): Promise<DidDocument> {
return this.#getter.get(did, options)
): Promise<ResolvedDocument<D, 'plc' | 'web'>> {
return this.getter.get(did, options) as Promise<
ResolvedDocument<D, 'plc' | 'web'>
>
}
}
1 change: 0 additions & 1 deletion packages/internal/did-resolver/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export * from '@atproto/did'

export * from './isomorphic-did-resolver.js'
export * from './did-cache-memory.js'
export * from './did-cache.js'
export * from './did-method.js'
Expand Down
23 changes: 0 additions & 23 deletions packages/internal/did-resolver/src/isomorphic-did-resolver.ts

This file was deleted.

23 changes: 11 additions & 12 deletions packages/internal/did-resolver/src/methods/plc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Fetch,
fetchFailureHandler,
fetchJsonProcessor,
fetchJsonZodProcessor,
Expand All @@ -20,7 +19,7 @@ export type DidPlcMethodOptions = {
/**
* @default globalThis.fetch
*/
fetch?: Fetch
fetch?: typeof globalThis.fetch

/**
* @default 'https://plc.directory/'
Expand All @@ -29,8 +28,9 @@ export type DidPlcMethodOptions = {
}

export class DidPlcMethod implements DidMethod<'plc'> {
readonly plcDirectoryUrl: URL
protected readonly fetch: Fetch
protected readonly fetch: typeof globalThis.fetch

public readonly plcDirectoryUrl: URL

constructor({
plcDirectoryUrl = 'https://plc.directory/',
Expand All @@ -45,13 +45,12 @@ export class DidPlcMethod implements DidMethod<'plc'> {

const url = new URL(`/${did}`, this.plcDirectoryUrl)

const request = new Request(url, {
redirect: 'error',
headers: { accept: 'application/did+ld+json,application/json' },
signal: options?.signal,
})

const { fetch } = this
return fetch(request).then(fetchSuccessHandler, fetchFailureHandler)
return this.fetch
.call(null, url, {
redirect: 'error',
headers: { accept: 'application/did+ld+json,application/json' },
signal: options?.signal,
})
.then(fetchSuccessHandler, fetchFailureHandler)
}
}
20 changes: 9 additions & 11 deletions packages/internal/did-resolver/src/methods/web.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Fetch,
fetchFailureHandler,
fetchJsonProcessor,
fetchJsonZodProcessor,
Expand All @@ -18,11 +17,11 @@ const fetchSuccessHandler = pipe(
)

export type DidWebMethodOptions = {
fetch?: Fetch
fetch?: typeof globalThis.fetch
}

export class DidWebMethod implements DidMethod<'web'> {
protected readonly fetch: Fetch
protected readonly fetch: typeof globalThis.fetch

constructor({ fetch = globalThis.fetch }: DidWebMethodOptions = {}) {
this.fetch = fetch
Expand All @@ -32,13 +31,12 @@ export class DidWebMethod implements DidMethod<'web'> {
const url = didWebToUrl(did) // Will throw if the DID is invalid
const didDocumentUrl = wellKnownUrl(url, 'did.json')

const request = new Request(didDocumentUrl, {
redirect: 'error',
headers: { accept: 'application/did+ld+json,application/json' },
signal: options?.signal,
})

const { fetch } = this
return fetch(request).then(fetchSuccessHandler, fetchFailureHandler)
return this.fetch
.call(null, didDocumentUrl, {
redirect: 'error',
headers: { accept: 'application/did+ld+json,application/json' },
signal: options?.signal,
})
.then(fetchSuccessHandler, fetchFailureHandler)
}
}
80 changes: 41 additions & 39 deletions packages/internal/fetch-node/src/safe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
DEFAULT_FORBIDDEN_DOMAIN_NAMES,
Fetch,
toGlobalFetch,
fetchMaxSizeProcessor,
forbiddenDomainNameRequestTransform,
protocolCheckRequestTransform,
Expand All @@ -20,55 +20,57 @@ export type SafeFetchWrapOptions = NonNullable<
* with user provided input (URL).
*/
export const safeFetchWrap = ({
fetch = globalThis.fetch as Fetch,
fetch = globalThis.fetch,
responseMaxSize = 512 * 1024, // 512kB
allowHttp = false,
allowData = false,
ssrfProtection = true,
timeout = 10e3 as number,
forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES as Iterable<string>,
} = {}): Fetch =>
pipe(
/**
* Prevent using http:, file: or data: protocols.
*/
protocolCheckRequestTransform(
['https:']
.concat(allowHttp ? ['http:'] : [])
.concat(allowData ? ['data:'] : []),
),

/**
* Only requests that will be issued with a "Host" header are allowed.
*/
requireHostHeaderTranform(),
} = {}) =>
toGlobalFetch(
pipe(
/**
* Prevent using http:, file: or data: protocols.
*/
protocolCheckRequestTransform(
['https:']
.concat(allowHttp ? ['http:'] : [])
.concat(allowData ? ['data:'] : []),
),

/**
* 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),
/**
* Only requests that will be issued with a "Host" header are allowed.
*/
requireHostHeaderTranform(),

/**
* Since we will be fetching from the network based on user provided
* input, let's mitigate resource exhaustion attacks by setting a timeout.
*/
timeoutFetchWrap({
timeout,
/**
* 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, we need to make sure that the request is not vulnerable to SSRF
* attacks.
* input, let's mitigate resource exhaustion attacks by setting a timeout.
*/
fetch: ssrfProtection ? ssrfFetchWrap({ fetch }) : fetch,
}),
timeoutFetchWrap({
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.
*/
fetch: ssrfProtection ? ssrfFetchWrap({ fetch }) : fetch,
}),

/**
* 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),
/**
* 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),
),
)
18 changes: 6 additions & 12 deletions packages/internal/fetch-node/src/ssrf.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import dns, { LookupAddress } from 'node:dns'
import { LookupFunction } from 'node:net'

import { Fetch, FetchError } from '@atproto-labs/fetch'
import { FetchError, toGlobalFetch } from '@atproto-labs/fetch'
import ipaddr from 'ipaddr.js'
import { Agent } from 'undici'

Expand All @@ -19,13 +19,9 @@ export const ssrfAgent = new Agent({ connect: { lookup } })
*/
export const ssrfFetchWrap = ({
allowCustomPort = false,
fetch = globalThis.fetch as (
this: void | null | typeof globalThis,
input: Request,
init?: RequestInit,
) => Promise<Response>,
} = {}): Fetch => {
const ssrfSafeFetch: Fetch = async (request) => {
fetch = globalThis.fetch,
} = {}) => {
return toGlobalFetch(async (request) => {
const { protocol, hostname, port } = new URL(request.url)

if (protocol === 'data:') {
Expand Down Expand Up @@ -130,9 +126,7 @@ export const ssrfFetchWrap = ({

// blob: about: file: all should be rejected
throw new FetchError(400, `Forbidden protocol "${protocol}"`, { request })
}

return ssrfSafeFetch
})
}

function parseIpHostname(
Expand All @@ -149,7 +143,7 @@ function parseIpHostname(
return undefined
}

export function lookup(
function lookup(
hostname: string,
options: dns.LookupOptions,
callback: Parameters<LookupFunction>[2],
Expand Down
Loading

0 comments on commit 5f7493f

Please sign in to comment.