6d308b857b
* ✨ 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>
315 lines
9.3 KiB
TypeScript
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',
|
|
})
|
|
})
|
|
})
|