Skip to content

Commit

Permalink
feat(auth): soft delete access tokens and grant accesses (#2837)
Browse files Browse the repository at this point in the history
  • Loading branch information
njlie authored Aug 8, 2024
1 parent aac63c7 commit 4a43281
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.alterTable('accessTokens', function (table) {
table.timestamp('revokedAt')
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.alterTable('accessTokens', function (table) {
table.dropColumn('revokedAt')
})
}
14 changes: 0 additions & 14 deletions packages/auth/src/access/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,4 @@ describe('Access Service', (): void => {
expect(fetchedAccess[0].actions).toEqual(incomingPaymentAccess.actions)
})
})

describe('revoke access', (): void => {
test('revokeByGrantId', async () => {
const access = await Access.query(trx).insert({
grantId: grant.id,
type: 'incoming-payment',
actions: [AccessAction.Create, AccessAction.Read, AccessAction.List]
})

await accessService.revokeByGrantId(grant.id)

expect(Access.query(trx).findById(access.id)).resolves.toBeUndefined()
})
})
})
17 changes: 1 addition & 16 deletions packages/auth/src/access/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export interface AccessService {
trx?: Transaction
): Promise<Access[]>
getByGrant(grantId: string, trx?: Transaction): Promise<Access[]>
revokeByGrantId(grantId: string, trx?: Transaction): Promise<number>
}

interface ServiceDependencies extends BaseService {
Expand All @@ -38,9 +37,7 @@ export async function createAccessService({
trx?: Transaction
) => createAccess(deps, grantId, accessRequests, trx),
getByGrant: (grantId: string, trx?: Transaction) =>
getByGrant(deps, grantId, trx),
revokeByGrantId: (grantId: string, trx?: Transaction) =>
revokeByGrantId(deps, grantId, trx)
getByGrant(deps, grantId, trx)
}
}

Expand All @@ -66,15 +63,3 @@ async function getByGrant(
grantId
})
}

async function revokeByGrantId(
deps: ServiceDependencies,
grantId: string,
trx?: Transaction
): Promise<number> {
return Access.query(trx || deps.knex)
.delete()
.where({
grantId
})
}
1 change: 1 addition & 0 deletions packages/auth/src/accessToken/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class AccessToken extends BaseModel {
public managementId!: string
public grantId!: string
public expiresIn!: number
public revokedAt?: Date
}

interface ToOpenPaymentsAccessTokenArgs {
Expand Down
35 changes: 21 additions & 14 deletions packages/auth/src/accessToken/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ describe('Access Token Service', (): void => {
accessTokenService.getByManagementId(accessToken.managementId)
).resolves.toMatchObject({
...accessToken,
revokedAt: null,
grant: retrievedGrant
})
})
Expand Down Expand Up @@ -267,21 +268,19 @@ describe('Access Token Service', (): void => {
describe('Revoke by token id', (): void => {
test('Can revoke un-expired token', async (): Promise<void> => {
await token.$query(trx).patch({ expiresIn: 1000000 })
await expect(accessTokenService.revoke(token.id)).resolves.toEqual(
token
)
await expect(
AccessToken.query(trx).findById(token.id)
).resolves.toBeUndefined()
await expect(accessTokenService.revoke(token.id)).resolves.toEqual({
...token,
revokedAt: expect.any(Date),
updatedAt: expect.any(Date)
})
})
test('Can revoke even if token has already expired', async (): Promise<void> => {
await token.$query(trx).patch({ expiresIn: -1 })
await expect(accessTokenService.revoke(token.id)).resolves.toEqual(
token
)
await expect(
AccessToken.query(trx).findById(token.id)
).resolves.toBeUndefined()
await expect(accessTokenService.revoke(token.id)).resolves.toEqual({
...token,
revokedAt: expect.any(Date),
updatedAt: expect.any(Date)
})
})
test('Can revoke even if token has already been revoked', async (): Promise<void> => {
await token.$query(trx).delete()
Expand Down Expand Up @@ -309,7 +308,11 @@ describe('Access Token Service', (): void => {
).resolves.toEqual(1)
await expect(
AccessToken.query(trx).findById(token.id)
).resolves.toBeUndefined()
).resolves.toEqual({
...token,
revokedAt: expect.any(Date),
updatedAt: expect.any(Date)
})
})
test('Can revoke even if token has already expired', async (): Promise<void> => {
await token.$query(trx).patch({ expiresIn: -1 })
Expand All @@ -318,7 +321,11 @@ describe('Access Token Service', (): void => {
).resolves.toEqual(1)
await expect(
AccessToken.query(trx).findById(token.id)
).resolves.toBeUndefined()
).resolves.toEqual({
...token,
revokedAt: expect.any(Date),
updatedAt: expect.any(Date)
})
})
test('Can revoke even if token has already been revoked', async (): Promise<void> => {
await token.$query(trx).delete()
Expand Down
11 changes: 9 additions & 2 deletions packages/auth/src/accessToken/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ async function getByManagementId(
): Promise<AccessToken | undefined> {
return AccessToken.query()
.findOne('managementId', managementId)
.whereNull('revokedAt')
.withGraphFetched('grant')
}

Expand All @@ -80,6 +81,7 @@ async function introspect(
): Promise<{ grant: Grant; access: Access[] } | undefined> {
const token = await AccessToken.query(deps.knex)
.findOne({ value: tokenValue })
.whereNull('revokedAt')
.withGraphFetched('grant.access')

const foundAccess: Access[] = []
Expand Down Expand Up @@ -117,7 +119,10 @@ async function revoke(
trx?: TransactionOrKnex
): Promise<AccessToken | undefined> {
return AccessToken.query(trx || deps.knex)
.deleteById(id)
.patchAndFetchById(id, {
revokedAt: new Date()
})
.whereNull('revokedAt')
.returning('*')
.first()
}
Expand All @@ -128,7 +133,9 @@ async function revokeByGrantId(
trx?: TransactionOrKnex
): Promise<number> {
return await AccessToken.query(trx || deps.knex)
.delete()
.patch({
revokedAt: new Date()
})
.where('grantId', grantId)
}

Expand Down
18 changes: 14 additions & 4 deletions packages/auth/src/grant/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ describe('Grant Service', (): void => {

describe('grant flow', (): void => {
let grant: Grant
let access: Access
let accessToken: AccessToken

const CLIENT = faker.internet.url({ appendSlash: false })

Expand All @@ -79,13 +81,13 @@ describe('Grant Service', (): void => {
grantId: grant.id
})

await Access.query().insert({
access = await Access.query().insert({
...BASE_GRANT_ACCESS,
type: AccessType.IncomingPayment,
grantId: grant.id
})

await AccessToken.query().insert({
accessToken = await AccessToken.query().insert({
value: generateToken(),
managementId: v4(),
expiresIn: 10_000_000,
Expand Down Expand Up @@ -351,10 +353,18 @@ describe('Grant Service', (): void => {
expect(revokedGrant?.finalizationReason).toEqual(
GrantFinalization.Revoked
)
expect(Access.query().where({ grantId: grant.id })).resolves.toEqual([])
expect(Access.query().where({ grantId: grant.id })).resolves.toEqual([
{ ...access, limits: null }
])
expect(
AccessToken.query().where({ grantId: grant.id })
).resolves.toEqual([])
).resolves.toEqual([
{
...accessToken,
revokedAt: expect.any(Date),
updatedAt: expect.any(Date)
}
])
})

test('Can "revoke" unknown grant', async (): Promise<void> => {
Expand Down
3 changes: 1 addition & 2 deletions packages/auth/src/grant/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ async function revokeGrant(
deps: ServiceDependencies,
grantId: string
): Promise<boolean> {
const { accessTokenService, accessService } = deps
const { accessTokenService } = deps

const trx = await deps.knex.transaction()

Expand All @@ -199,7 +199,6 @@ async function revokeGrant(
}

await accessTokenService.revokeByGrantId(grant.id, trx)
await accessService.revokeByGrantId(grant.id, trx)

await trx.commit()
return true
Expand Down

0 comments on commit 4a43281

Please sign in to comment.