atproto/packages/syntax/tests/handle.test.ts
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

239 lines
6.8 KiB
TypeScript

import {
ensureValidHandle,
normalizeAndEnsureValidHandle,
ensureValidHandleRegex,
InvalidHandleError,
} from '../src'
import * as readline from 'readline'
import * as fs from 'fs'
describe('handle validation', () => {
const expectValid = (h: string) => {
ensureValidHandle(h)
ensureValidHandleRegex(h)
}
const expectInvalid = (h: string) => {
expect(() => ensureValidHandle(h)).toThrow(InvalidHandleError)
expect(() => ensureValidHandleRegex(h)).toThrow(InvalidHandleError)
}
it('allows valid handles', () => {
expectValid('A.ISI.EDU')
expectValid('XX.LCS.MIT.EDU')
expectValid('SRI-NIC.ARPA')
expectValid('john.test')
expectValid('jan.test')
expectValid('a234567890123456789.test')
expectValid('john2.test')
expectValid('john-john.test')
expectValid('john.bsky.app')
expectValid('jo.hn')
expectValid('a.co')
expectValid('a.org')
expectValid('joh.n')
expectValid('j0.h0')
const longHandle =
'shoooort' + '.loooooooooooooooooooooooooong'.repeat(8) + '.test'
expect(longHandle.length).toEqual(253)
expectValid(longHandle)
expectValid('short.' + 'o'.repeat(63) + '.test')
expectValid('jaymome-johnber123456.test')
expectValid('jay.mome-johnber123456.test')
expectValid('john.test.bsky.app')
// NOTE: this probably isn't ever going to be a real domain, but my read of
// the RFC is that it would be possible
expectValid('john.t')
})
// NOTE: we may change this at the proto level; currently only disallowed at
// the registration level
it('allows .local and .arpa handles (proto-level)', () => {
expectValid('laptop.local')
expectValid('laptop.arpa')
})
it('allows punycode handles', () => {
expectValid('xn--ls8h.test') // 💩.test
expectValid('xn--bcher-kva.tld') // bücher.tld
expectValid('xn--3jk.com')
expectValid('xn--w3d.com')
expectValid('xn--vqb.com')
expectValid('xn--ppd.com')
expectValid('xn--cs9a.com')
expectValid('xn--8r9a.com')
expectValid('xn--cfd.com')
expectValid('xn--5jk.com')
expectValid('xn--2lb.com')
})
it('allows onion (Tor) handles', () => {
expectValid('expyuzz4wqqyqhjn.onion')
expectValid('friend.expyuzz4wqqyqhjn.onion')
expectValid(
'g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion',
)
expectValid(
'friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion',
)
expectValid(
'friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion',
)
expectValid(
'2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion',
)
expectValid(
'friend.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion',
)
})
it('throws on invalid handles', () => {
expectInvalid('did:thing.test')
expectInvalid('did:thing')
expectInvalid('john-.test')
expectInvalid('john.0')
expectInvalid('john.-')
expectInvalid('short.' + 'o'.repeat(64) + '.test')
expectInvalid('short' + '.loooooooooooooooooooooooong'.repeat(10) + '.test')
const longHandle =
'shooooort' + '.loooooooooooooooooooooooooong'.repeat(8) + '.test'
expect(longHandle.length).toEqual(254)
expectInvalid(longHandle)
expectInvalid('xn--bcher-.tld')
expectInvalid('john..test')
expectInvalid('jo_hn.test')
expectInvalid('-john.test')
expectInvalid('.john.test')
expectInvalid('jo!hn.test')
expectInvalid('jo%hn.test')
expectInvalid('jo&hn.test')
expectInvalid('jo@hn.test')
expectInvalid('jo*hn.test')
expectInvalid('jo|hn.test')
expectInvalid('jo:hn.test')
expectInvalid('jo/hn.test')
expectInvalid('john💩.test')
expectInvalid('bücher.test')
expectInvalid('john .test')
expectInvalid('john.test.')
expectInvalid('john')
expectInvalid('john.')
expectInvalid('.john')
expectInvalid('john.test.')
expectInvalid('.john.test')
expectInvalid(' john.test')
expectInvalid('john.test ')
expectInvalid('joh-.test')
expectInvalid('john.-est')
expectInvalid('john.tes-')
})
it('throws on "dotless" TLD handles', () => {
expectInvalid('org')
expectInvalid('ai')
expectInvalid('gg')
expectInvalid('io')
})
it('correctly validates corner cases (modern vs. old RFCs)', () => {
expectValid('12345.test')
expectValid('8.cn')
expectValid('4chan.org')
expectValid('4chan.o-g')
expectValid('blah.4chan.org')
expectValid('thing.a01')
expectValid('120.0.0.1.com')
expectValid('0john.test')
expectValid('9sta--ck.com')
expectValid('99stack.com')
expectValid('0ohn.test')
expectValid('john.t--t')
expectValid('thing.0aa.thing')
expectInvalid('cn.8')
expectInvalid('thing.0aa')
expectInvalid('thing.0aa')
})
it('does not allow IP addresses as handles', () => {
expectInvalid('127.0.0.1')
expectInvalid('192.168.0.142')
expectInvalid('fe80::7325:8a97:c100:94b')
expectInvalid('2600:3c03::f03c:9100:feb0:af1f')
})
it('is consistent with examples from stackoverflow', () => {
const okStackoverflow = [
'stack.com',
'sta-ck.com',
'sta---ck.com',
'sta--ck9.com',
'stack99.com',
'sta99ck.com',
'google.com.uk',
'google.co.in',
'google.com',
'maselkowski.pl',
'm.maselkowski.pl',
'xn--masekowski-d0b.pl',
'xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s',
'xn--stackoverflow.com',
'stackoverflow.xn--com',
'stackoverflow.co.uk',
'xn--masekowski-d0b.pl',
'xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s',
]
okStackoverflow.forEach(expectValid)
const badStackoverflow = [
'-notvalid.at-all',
'-thing.com',
'www.masełkowski.pl.com',
]
badStackoverflow.forEach(expectInvalid)
})
it('conforms to interop valid handles', () => {
const lineReader = readline.createInterface({
input: fs.createReadStream(
`${__dirname}/interop-files/handle_syntax_valid.txt`,
),
terminal: false,
})
lineReader.on('line', (line) => {
if (line.startsWith('#') || line.length === 0) {
return
}
expectValid(line)
})
})
it('conforms to interop invalid handles', () => {
const lineReader = readline.createInterface({
input: fs.createReadStream(
`${__dirname}/interop-files/handle_syntax_invalid.txt`,
),
terminal: false,
})
lineReader.on('line', (line) => {
if (line.startsWith('#') || line.length === 0) {
return
}
expectInvalid(line)
})
})
})
describe('normalization', () => {
it('normalizes handles', () => {
const normalized = normalizeAndEnsureValidHandle('JoHn.TeST')
expect(normalized).toBe('john.test')
})
it('throws on invalid normalized handles', () => {
expect(() => normalizeAndEnsureValidHandle('JoH!n.TeST')).toThrow(
InvalidHandleError,
)
})
})