atproto/packages/pds/tests/auth.test.ts
Foysal Ahamed 6d308b857b
Allow appeals on takendown account (#3251)
*  Allow appeals on takendown account

*  Update snapshot

*  Remove duplicate test

*  Respond with takendown token from createSession for takendown accounts

* 🧹 cleanup appeal account action stuff

* 📝 Add description to new field

* ♻️ Refactor authscope formatter and add test for create record with takendown token

*  Update snapshot

* add createReport route

* changeset

---------

Co-authored-by: dholms <dtholmgren@gmail.com>
2024-12-20 13:52:20 -06:00

315 lines
9.3 KiB
TypeScript

import * as jose from 'jose'
import { AtpAgent } from '@atproto/api'
import { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'
import { createRefreshToken } from '../src/account-manager/helpers/auth'
describe('auth', () => {
let network: TestNetworkNoAppView
let agent: AtpAgent
beforeAll(async () => {
network = await TestNetworkNoAppView.create({
dbPostgresSchema: 'auth',
})
agent = network.pds.getClient()
})
afterAll(async () => {
await network.close()
})
const createAccount = async (info) => {
const { data } = await agent.com.atproto.server.createAccount(info)
return data
}
const getSession = async (jwt) => {
const { data } = await agent.com.atproto.server.getSession(
{},
{
headers: SeedClient.getHeaders(jwt),
},
)
return data
}
const createSession = async (info) => {
const { data } = await agent.com.atproto.server.createSession(info)
return data
}
const deleteSession = async (jwt) => {
await agent.com.atproto.server.deleteSession(undefined, {
headers: SeedClient.getHeaders(jwt),
})
}
const refreshSession = async (jwt: string) => {
const { data } = await agent.com.atproto.server.refreshSession(undefined, {
headers: SeedClient.getHeaders(jwt),
})
return data
}
it('provides valid access and refresh token on account creation.', async () => {
const email = 'alice@test.com'
const account = await createAccount({
handle: 'alice.test',
email,
password: 'password',
})
// Valid access token
const sessionInfo = await getSession(account.accessJwt)
expect(sessionInfo).toEqual({
did: account.did,
handle: account.handle,
email,
emailConfirmed: false,
active: true,
})
// Valid refresh token
const nextSession = await refreshSession(account.refreshJwt)
expect(nextSession).toEqual(
expect.objectContaining({
did: account.did,
handle: account.handle,
}),
)
})
it('provides valid access and refresh token on session creation.', async () => {
const email = 'bob@test.com'
await createAccount({
handle: 'bob.test',
email,
password: 'password',
})
const session = await createSession({
identifier: 'bob.test',
password: 'password',
})
// Valid access token
const sessionInfo = await getSession(session.accessJwt)
expect(sessionInfo).toEqual({
did: session.did,
handle: session.handle,
email,
emailConfirmed: false,
active: true,
})
// Valid refresh token
const nextSession = await refreshSession(session.refreshJwt)
expect(nextSession).toEqual(
expect.objectContaining({
did: session.did,
handle: session.handle,
}),
)
})
it('allows session creation using email address.', async () => {
const session = await createSession({
identifier: 'bob@TEST.com',
password: 'password',
})
expect(session.handle).toEqual('bob.test')
})
it('fails on session creation with a bad password.', async () => {
const sessionPromise = createSession({
identifier: 'bob.test',
password: 'wrong-pass',
})
await expect(sessionPromise).rejects.toThrow(
'Invalid identifier or password',
)
})
it('provides valid access and refresh token on session refresh.', async () => {
const email = 'carol@test.com'
const account = await createAccount({
handle: 'carol.test',
password: 'password',
email,
})
const session = await refreshSession(account.refreshJwt)
// Valid access token
const sessionInfo = await getSession(session.accessJwt)
expect(sessionInfo).toEqual({
did: session.did,
handle: session.handle,
email,
emailConfirmed: false,
active: true,
})
// Valid refresh token
const nextSession = await refreshSession(session.refreshJwt)
expect(nextSession).toEqual(
expect.objectContaining({
did: session.did,
handle: session.handle,
}),
)
})
it('handles racing refreshes', async () => {
const email = 'dan@test.com'
const account = await createAccount({
handle: 'dan.test',
password: 'password',
email,
})
const tokenIdPromises: Promise<string>[] = []
const doRefresh = async () => {
const res = await refreshSession(account.refreshJwt)
const decoded = jose.decodeJwt(res.refreshJwt)
if (!decoded?.jti) {
throw new Error('undefined jti on refresh token')
}
return decoded.jti
}
for (let i = 0; i < 10; i++) {
tokenIdPromises.push(doRefresh())
}
const tokenIds = await Promise.all(tokenIdPromises)
for (let i = 0; i < 10; i++) {
expect(tokenIds[i]).toEqual(tokenIds[0])
}
})
it('refresh token provides new token with same id on multiple uses during grace period.', async () => {
const account = await createAccount({
handle: 'eve.test',
email: 'eve@test.com',
password: 'password',
})
const refresh1 = await refreshSession(account.refreshJwt)
const refresh2 = await refreshSession(account.refreshJwt)
const token0 = jose.decodeJwt(account.refreshJwt)
const token1 = jose.decodeJwt(refresh1.refreshJwt)
const token2 = jose.decodeJwt(refresh2.refreshJwt)
expect(typeof token1?.jti).toEqual('string')
expect(token1?.jti).toEqual(token2?.jti)
expect(token1?.jti).not.toEqual(token0?.jti)
expect(token2?.jti).not.toEqual(token0?.jti)
})
it('refresh token is revoked after grace period completes.', async () => {
const { db } = network.pds.ctx.accountManager
const account = await createAccount({
handle: 'evan.test',
email: 'evan@test.com',
password: 'password',
})
await refreshSession(account.refreshJwt)
const token = jose.decodeJwt(account.refreshJwt)
// Update expiration (i.e. grace period) to end immediately
const refreshUpdated = await db.db
.updateTable('refresh_token')
.set({ expiresAt: new Date().toISOString() })
.where('id', '=', token?.jti ?? '')
.executeTakeFirst()
expect(Number(refreshUpdated.numUpdatedRows)).toEqual(1)
// Token can no longer be used
const refreshAgain = refreshSession(account.refreshJwt)
await expect(refreshAgain).rejects.toThrow('Token has been revoked')
// Ensure that token was cleaned-up
const refreshInfo = await db.db
.selectFrom('refresh_token')
.selectAll()
.where('id', '=', token?.jti ?? '')
.executeTakeFirst()
expect(refreshInfo).toBeUndefined()
})
it('refresh token is revoked when session is deleted.', async () => {
const account = await createAccount({
handle: 'finn.test',
email: 'finn@test.com',
password: 'password',
})
await deleteSession(account.refreshJwt)
const refreshDeleted = refreshSession(account.refreshJwt)
await expect(refreshDeleted).rejects.toThrow('Token has been revoked')
await deleteSession(account.refreshJwt) // No problem double-revoking a token
})
it('access token cannot be used to refresh a session.', async () => {
const account = await createAccount({
handle: 'gordon.test',
email: 'gordon@test.com',
password: 'password',
})
const refreshWithAccess = refreshSession(account.accessJwt)
await expect(refreshWithAccess).rejects.toThrow('Invalid token type')
})
it('expired refresh token cannot be used to refresh a session.', async () => {
const account = await createAccount({
handle: 'holga.test',
email: 'holga@test.com',
password: 'password',
})
const refreshJwt = await createRefreshToken({
did: account.did,
jwtKey: network.pds.jwtSecretKey(),
serviceDid: network.pds.ctx.cfg.service.did,
expiresIn: -1,
})
const refreshExpired = refreshSession(refreshJwt)
await expect(refreshExpired).rejects.toThrow('Token has expired')
await deleteSession(refreshJwt) // No problem revoking an expired token
})
it('actor takedown disallows fresh session.', async () => {
const account = await createAccount({
handle: 'iris.test',
email: 'iris@test.com',
password: 'password',
})
await agent.com.atproto.admin.updateSubjectStatus(
{
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: account.did,
},
takedown: { applied: true },
},
{
encoding: 'application/json',
headers: { authorization: network.pds.adminAuth() },
},
)
await expect(
createSession({ identifier: 'iris.test', password: 'password' }),
).rejects.toMatchObject({
error: 'AccountTakedown',
})
})
it('actor takedown disallows refresh session.', async () => {
const account = await createAccount({
handle: 'jared.test',
email: 'jared@test.com',
password: 'password',
})
await agent.com.atproto.admin.updateSubjectStatus(
{
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: account.did,
},
takedown: { applied: true },
},
{
encoding: 'application/json',
headers: { authorization: network.pds.adminAuth() },
},
)
await expect(refreshSession(account.refreshJwt)).rejects.toMatchObject({
error: 'AccountTakedown',
})
})
})