6c1ec149cf
* accept entryway session tokens * extra check + tests * build * build * pr feedback --------- Co-authored-by: Devin Ivy <devinivy@gmail.com>
175 lines
5.8 KiB
TypeScript
175 lines
5.8 KiB
TypeScript
import * as nodeCrypto from 'node:crypto'
|
|
import KeyEncoder from 'key-encoder'
|
|
import * as ui8 from 'uint8arrays'
|
|
import * as jose from 'jose'
|
|
import * as crypto from '@atproto/crypto'
|
|
import { AtpAgent, AtUri } from '@atproto/api'
|
|
import { basicSeed, SeedClient, TestNetwork } from '@atproto/dev-env'
|
|
import assert from 'node:assert'
|
|
import { MINUTE } from '@atproto/common'
|
|
|
|
const keyEncoder = new KeyEncoder('secp256k1')
|
|
|
|
const derivePrivKey = async (
|
|
keypair: crypto.ExportableKeypair,
|
|
): Promise<nodeCrypto.KeyObject> => {
|
|
const privKeyRaw = await keypair.export()
|
|
const privKeyEncoded = keyEncoder.encodePrivate(
|
|
ui8.toString(privKeyRaw, 'hex'),
|
|
'raw',
|
|
'pem',
|
|
)
|
|
return nodeCrypto.createPrivateKey(privKeyEncoded)
|
|
}
|
|
|
|
// @NOTE temporary measure, see note on entrywaySession in bsky/src/auth-verifier.ts
|
|
describe('entryway auth', () => {
|
|
let network: TestNetwork
|
|
let agent: AtpAgent
|
|
let sc: SeedClient
|
|
let alice: string
|
|
let jwtPrivKey: nodeCrypto.KeyObject
|
|
|
|
beforeAll(async () => {
|
|
const keypair = await crypto.Secp256k1Keypair.create({ exportable: true })
|
|
jwtPrivKey = await derivePrivKey(keypair)
|
|
const entrywayJwtPublicKeyHex = ui8.toString(
|
|
keypair.publicKeyBytes(),
|
|
'hex',
|
|
)
|
|
|
|
network = await TestNetwork.create({
|
|
dbPostgresSchema: 'bsky_entryway_auth',
|
|
bsky: {
|
|
entrywayJwtPublicKeyHex,
|
|
},
|
|
})
|
|
agent = network.bsky.getClient()
|
|
sc = network.getSeedClient()
|
|
await basicSeed(sc)
|
|
await network.processAll()
|
|
alice = sc.dids.alice
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await network.close()
|
|
})
|
|
|
|
it('works', async () => {
|
|
const signer = new jose.SignJWT({ scope: 'com.atproto.access' })
|
|
.setSubject(alice)
|
|
.setIssuedAt()
|
|
.setExpirationTime('60mins')
|
|
.setAudience('did:web:fake.server.bsky.network')
|
|
.setProtectedHeader({
|
|
typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html
|
|
alg: 'ES256K',
|
|
})
|
|
const token = await signer.sign(jwtPrivKey)
|
|
const res = await agent.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.bob },
|
|
{ headers: { authorization: `Bearer ${token}` } },
|
|
)
|
|
expect(res.data.did).toEqual(sc.dids.bob)
|
|
// ensure this request is personalized for alice
|
|
const followingUri = res.data.viewer?.following
|
|
assert(followingUri)
|
|
const parsed = new AtUri(followingUri)
|
|
expect(parsed.hostname).toEqual(alice)
|
|
})
|
|
|
|
it('does not work on bad scopes', async () => {
|
|
const signer = new jose.SignJWT({ scope: 'com.atproto.refresh' })
|
|
.setSubject(alice)
|
|
.setIssuedAt()
|
|
.setExpirationTime('60mins')
|
|
.setAudience('did:web:fake.server.bsky.network')
|
|
.setProtectedHeader({
|
|
typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html
|
|
alg: 'ES256K',
|
|
})
|
|
const token = await signer.sign(jwtPrivKey)
|
|
const attempt = agent.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.bob },
|
|
{ headers: { authorization: `Bearer ${token}` } },
|
|
)
|
|
await expect(attempt).rejects.toThrow('Bad token scope')
|
|
})
|
|
|
|
it('does not work on expired tokens', async () => {
|
|
const time = Math.floor((Date.now() - 5 * MINUTE) / 1000)
|
|
const signer = new jose.SignJWT({ scope: 'com.atproto.access' })
|
|
.setSubject(alice)
|
|
.setIssuedAt()
|
|
.setExpirationTime(time)
|
|
.setAudience('did:web:fake.server.bsky.network')
|
|
.setProtectedHeader({
|
|
typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html
|
|
alg: 'ES256K',
|
|
})
|
|
const token = await signer.sign(jwtPrivKey)
|
|
const attempt = agent.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.bob },
|
|
{ headers: { authorization: `Bearer ${token}` } },
|
|
)
|
|
await expect(attempt).rejects.toThrow('Token has expired')
|
|
})
|
|
|
|
it('does not work on bad auds', async () => {
|
|
const signer = new jose.SignJWT({ scope: 'com.atproto.access' })
|
|
.setSubject(alice)
|
|
.setIssuedAt()
|
|
.setExpirationTime('60mins')
|
|
.setAudience('did:web:my.personal.pds.com')
|
|
.setProtectedHeader({
|
|
typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html
|
|
alg: 'ES256K',
|
|
})
|
|
const token = await signer.sign(jwtPrivKey)
|
|
const attempt = agent.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.bob },
|
|
{ headers: { authorization: `Bearer ${token}` } },
|
|
)
|
|
await expect(attempt).rejects.toThrow('Bad token aud')
|
|
})
|
|
|
|
it('does not work with bad signatures', async () => {
|
|
const fakeKey = await crypto.Secp256k1Keypair.create({ exportable: true })
|
|
const fakeJwtKey = await derivePrivKey(fakeKey)
|
|
const signer = new jose.SignJWT({ scope: 'com.atproto.access' })
|
|
.setSubject(alice)
|
|
.setIssuedAt()
|
|
.setExpirationTime('60mins')
|
|
.setAudience('did:web:my.personal.pds.com')
|
|
.setProtectedHeader({
|
|
typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html
|
|
alg: 'ES256K',
|
|
})
|
|
const token = await signer.sign(fakeJwtKey)
|
|
const attempt = agent.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.bob },
|
|
{ headers: { authorization: `Bearer ${token}` } },
|
|
)
|
|
await expect(attempt).rejects.toThrow('Token could not be verified')
|
|
})
|
|
|
|
it('does not work on flexible aud routes', async () => {
|
|
const signer = new jose.SignJWT({ scope: 'com.atproto.access' })
|
|
.setSubject(alice)
|
|
.setIssuedAt()
|
|
.setExpirationTime('60mins')
|
|
.setAudience('did:web:fake.server.bsky.network')
|
|
.setProtectedHeader({
|
|
typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html
|
|
alg: 'ES256K',
|
|
})
|
|
const token = await signer.sign(jwtPrivKey)
|
|
const feedUri = AtUri.make(alice, 'app.bsky.feed.generator', 'fake-feed')
|
|
const attempt = agent.app.bsky.feed.getFeed(
|
|
{ feed: feedUri.toString() },
|
|
{ headers: { authorization: `Bearer ${token}` } },
|
|
)
|
|
await expect(attempt).rejects.toThrow('Malformed token')
|
|
})
|
|
})
|