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

159 lines
5.0 KiB
TypeScript

import {
ensureValidNsid,
ensureValidNsidRegex,
InvalidNsidError,
NSID,
} from '../src'
import * as readline from 'readline'
import * as fs from 'fs'
describe('NSID parsing & creation', () => {
it('parses valid NSIDs', () => {
expect(NSID.parse('com.example.foo').authority).toBe('example.com')
expect(NSID.parse('com.example.foo').name).toBe('foo')
expect(NSID.parse('com.example.foo').toString()).toBe('com.example.foo')
expect(NSID.parse('com.long-thing1.cool.fooBarBaz').authority).toBe(
'cool.long-thing1.com',
)
expect(NSID.parse('com.long-thing1.cool.fooBarBaz').name).toBe('fooBarBaz')
expect(NSID.parse('com.long-thing1.cool.fooBarBaz').toString()).toBe(
'com.long-thing1.cool.fooBarBaz',
)
})
it('creates valid NSIDs', () => {
expect(NSID.create('example.com', 'foo').authority).toBe('example.com')
expect(NSID.create('example.com', 'foo').name).toBe('foo')
expect(NSID.create('example.com', 'foo').toString()).toBe('com.example.foo')
expect(NSID.create('cool.long-thing1.com', 'fooBarBaz').authority).toBe(
'cool.long-thing1.com',
)
expect(NSID.create('cool.long-thing1.com', 'fooBarBaz').name).toBe(
'fooBarBaz',
)
expect(NSID.create('cool.long-thing1.com', 'fooBarBaz').toString()).toBe(
'com.long-thing1.cool.fooBarBaz',
)
})
})
describe('NSID validation', () => {
const expectValid = (h: string) => {
ensureValidNsid(h)
ensureValidNsidRegex(h)
}
const expectInvalid = (h: string) => {
expect(() => ensureValidNsid(h)).toThrow(InvalidNsidError)
expect(() => ensureValidNsidRegex(h)).toThrow(InvalidNsidError)
}
it('enforces spec details', () => {
expectValid('com.example.foo')
const longNsid = 'com.' + 'o'.repeat(63) + '.foo'
expectValid(longNsid)
const tooLongNsid = 'com.' + 'o'.repeat(64) + '.foo'
expectInvalid(tooLongNsid)
const longEnd = 'com.example.' + 'o'.repeat(63)
expectValid(longEnd)
const tooLongEnd = 'com.example.' + 'o'.repeat(64)
expectInvalid(tooLongEnd)
const longOverall = 'com.' + 'middle.'.repeat(40) + 'foo'
expect(longOverall.length).toBe(287)
expectValid(longOverall)
const tooLongOverall = 'com.' + 'middle.'.repeat(50) + 'foo'
expect(tooLongOverall.length).toBe(357)
expectInvalid(tooLongOverall)
expectValid('com.example.fooBar')
expectValid('net.users.bob.ping')
expectValid('a.b.c')
expectValid('m.xn--masekowski-d0b.pl')
expectValid('one.two.three')
expectValid('one.two.three.four-and.FiVe')
expectValid('one.2.three')
expectValid('a-0.b-1.c')
expectValid('a0.b1.cc')
expectValid('cn.8.lex.stuff')
expectValid('test.12345.record')
expectValid('a01.thing.record')
expectValid('a.0.c')
expectValid('xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two')
expectInvalid('com.example.foo.*')
expectInvalid('com.example.foo.blah*')
expectInvalid('com.example.foo.*blah')
expectInvalid('com.example.f00')
expectInvalid('com.exa💩ple.thing')
expectInvalid('a-0.b-1.c-3')
expectInvalid('a-0.b-1.c-o')
expectInvalid('a0.b1.c3')
expectInvalid('1.0.0.127.record')
expectInvalid('0two.example.foo')
expectInvalid('example.com')
expectInvalid('com.example')
expectInvalid('a.')
expectInvalid('.one.two.three')
expectInvalid('one.two.three ')
expectInvalid('one.two..three')
expectInvalid('one .two.three')
expectInvalid(' one.two.three')
expectInvalid('com.exa💩ple.thing')
expectInvalid('com.atproto.feed.p@st')
expectInvalid('com.atproto.feed.p_st')
expectInvalid('com.atproto.feed.p*st')
expectInvalid('com.atproto.feed.po#t')
expectInvalid('com.atproto.feed.p!ot')
expectInvalid('com.example-.foo')
})
it('allows onion (Tor) NSIDs', () => {
expectValid('onion.expyuzz4wqqyqhjn.spec.getThing')
expectValid(
'onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
)
})
it('allows starting-with-numeric segments (same as domains)', () => {
expectValid('org.4chan.lex.getThing')
expectValid('cn.8.lex.stuff')
expectValid(
'onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
)
})
it('conforms to interop valid NSIDs', () => {
const lineReader = readline.createInterface({
input: fs.createReadStream(
`${__dirname}/interop-files/nsid_syntax_valid.txt`,
),
terminal: false,
})
lineReader.on('line', (line) => {
if (line.startsWith('#') || line.length === 0) {
return
}
expectValid(line)
})
})
it('conforms to interop invalid NSIDs', () => {
const lineReader = readline.createInterface({
input: fs.createReadStream(
`${__dirname}/interop-files/nsid_syntax_invalid.txt`,
),
terminal: false,
})
lineReader.on('line', (line) => {
if (line.startsWith('#') || line.length === 0) {
return
}
expectInvalid(line)
})
})
})