-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(auth): httpsig request validation #387
Changes from all commits
b755923
8cdb650
98fe6bd
0755f7c
0700ff4
d3f7c7c
b6c4b69
6189b27
ff45b69
9821292
66d8942
eff5c1f
d15a329
06927d7
b65208a
ec80231
7518f22
7e5b5c0
ea3c56a
5db1bd1
c7d6fa0
dffed48
b702055
94d2950
91b41d2
17d445d
d3909e2
d0ab02b
2c318ed
c65ccc0
8b6a1b5
6516c7c
1e3d0a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,27 +16,11 @@ import { AccessToken } from './model' | |
import { Access } from '../access/model' | ||
import { AccessTokenRoutes } from './routes' | ||
import { createContext } from '../tests/context' | ||
|
||
const KEY_REGISTRY_ORIGIN = 'https://openpayments.network' | ||
const TEST_KID_PATH = '/keys/test-key' | ||
const TEST_JWK = { | ||
kid: KEY_REGISTRY_ORIGIN + TEST_KID_PATH, | ||
x: 'test-public-key', | ||
kty: 'OKP', | ||
alg: 'EdDSA', | ||
crv: 'Ed25519', | ||
use: 'sig' | ||
} | ||
const TEST_CLIENT_KEY = { | ||
client: { | ||
id: v4(), | ||
name: 'Bob', | ||
email: 'bob@bob.com', | ||
image: 'a link to an image', | ||
uri: 'https://bob.com' | ||
}, | ||
...TEST_JWK | ||
} | ||
import { | ||
KID_PATH, | ||
KEY_REGISTRY_ORIGIN, | ||
TEST_CLIENT_KEY | ||
} from '../grant/routes.test' | ||
|
||
describe('Access Token Routes', (): void => { | ||
let deps: IocContract<AppServices> | ||
|
@@ -71,7 +55,7 @@ describe('Access Token Routes', (): void => { | |
finishMethod: FinishMethod.Redirect, | ||
finishUri: 'https://example.com/finish', | ||
clientNonce: crypto.randomBytes(8).toString('hex').toUpperCase(), | ||
clientKeyId: KEY_REGISTRY_ORIGIN + TEST_KID_PATH, | ||
clientKeyId: KEY_REGISTRY_ORIGIN + KID_PATH, | ||
interactId: v4(), | ||
interactRef: crypto.randomBytes(8).toString('hex').toUpperCase(), | ||
interactNonce: crypto.randomBytes(8).toString('hex').toUpperCase() | ||
|
@@ -92,14 +76,18 @@ describe('Access Token Routes', (): void => { | |
|
||
const BASE_TOKEN = { | ||
value: crypto.randomBytes(8).toString('hex').toUpperCase(), | ||
managementId: 'https://example.com/manage/12345', | ||
managementId: v4(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does that not have a path? Or is that already the parsed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is supposed to represent a row in the |
||
expiresIn: 3600 | ||
} | ||
|
||
describe('Introspect', (): void => { | ||
let grant: Grant | ||
let access: Access | ||
let token: AccessToken | ||
|
||
const url = '/introspect' | ||
const method = 'POST' | ||
|
||
beforeEach(async (): Promise<void> => { | ||
grant = await Grant.query(trx).insertAndFetch({ | ||
...BASE_GRANT | ||
|
@@ -116,61 +104,48 @@ describe('Access Token Routes', (): void => { | |
test('Cannot introspect fake token', async (): Promise<void> => { | ||
const ctx = createContext( | ||
{ | ||
headers: { Accept: 'application/json' }, | ||
url: '/introspect', | ||
method: 'POST' | ||
headers: { | ||
Accept: 'application/json' | ||
}, | ||
url, | ||
method | ||
}, | ||
{} | ||
) | ||
ctx.request.body = { | ||
access_token: v4(), | ||
proof: 'httpsig', | ||
resource_server: 'test' | ||
} | ||
await expect(accessTokenRoutes.introspect(ctx)).rejects.toMatchObject({ | ||
status: 404, | ||
await expect(accessTokenRoutes.introspect(ctx)).resolves.toBeUndefined() | ||
expect(ctx.status).toBe(404) | ||
expect(ctx.body).toMatchObject({ | ||
error: 'invalid_request', | ||
message: 'token not found' | ||
}) | ||
}) | ||
|
||
test('Cannot introspect if no token passed', async (): Promise<void> => { | ||
const ctx = createContext( | ||
{ | ||
headers: { Accept: 'application/json' }, | ||
url: '/introspect', | ||
method: 'POST' | ||
}, | ||
{} | ||
) | ||
ctx.request.body = { | ||
proof: 'httpsig', | ||
resource_server: 'test' | ||
} | ||
await expect(accessTokenRoutes.introspect(ctx)).rejects.toMatchObject({ | ||
status: 400, | ||
message: 'invalid introspection request' | ||
}) | ||
}) | ||
|
||
test('Successfully introspects valid token', async (): Promise<void> => { | ||
const clientId = crypto | ||
.createHash('sha256') | ||
.update(TEST_CLIENT_KEY.client.id) | ||
.update(TEST_CLIENT_KEY.jwk.client.id) | ||
.digest('hex') | ||
const scope = nock(KEY_REGISTRY_ORIGIN) | ||
.get(TEST_KID_PATH) | ||
.reply(200, TEST_CLIENT_KEY) | ||
.get(KID_PATH) | ||
.reply(200, TEST_CLIENT_KEY.jwk) | ||
|
||
const ctx = createContext( | ||
{ | ||
headers: { Accept: 'application/json' }, | ||
headers: { | ||
Accept: 'application/json' | ||
}, | ||
url: '/introspect', | ||
method: 'POST' | ||
}, | ||
{} | ||
) | ||
|
||
ctx.request.body = { | ||
access_token: token.value, | ||
proof: 'httpsig', | ||
resource_server: 'test' | ||
} | ||
await expect(accessTokenRoutes.introspect(ctx)).resolves.toBeUndefined() | ||
|
@@ -179,6 +154,9 @@ describe('Access Token Routes', (): void => { | |
expect(ctx.response.get('Content-Type')).toBe( | ||
'application/json; charset=utf-8' | ||
) | ||
|
||
const testKeyWithoutClient = TEST_CLIENT_KEY.jwk | ||
delete testKeyWithoutClient.client | ||
expect(ctx.body).toEqual({ | ||
active: true, | ||
grant: grant.id, | ||
|
@@ -189,27 +167,41 @@ describe('Access Token Routes', (): void => { | |
limits: access.limits | ||
} | ||
], | ||
key: { proof: 'httpsig', jwk: TEST_JWK }, | ||
key: { | ||
proof: 'httpsig', | ||
jwk: { | ||
...testKeyWithoutClient, | ||
exp: expect.any(Number), | ||
nbf: expect.any(Number), | ||
revoked: false | ||
} | ||
}, | ||
client_id: clientId | ||
}) | ||
scope.isDone() | ||
}) | ||
|
||
test('Successfully introspects expired token', async (): Promise<void> => { | ||
const scope = nock(KEY_REGISTRY_ORIGIN) | ||
.get(KID_PATH) | ||
.reply(200, TEST_CLIENT_KEY.jwk) | ||
const now = new Date(new Date().getTime() + 4000) | ||
jest.useFakeTimers() | ||
jest.setSystemTime(now) | ||
|
||
const ctx = createContext( | ||
{ | ||
headers: { Accept: 'application/json' }, | ||
headers: { | ||
Accept: 'application/json' | ||
}, | ||
url: '/introspect', | ||
method: 'POST' | ||
}, | ||
{} | ||
) | ||
|
||
ctx.request.body = { | ||
access_token: token.value, | ||
proof: 'httpsig', | ||
resource_server: 'test' | ||
} | ||
await expect(accessTokenRoutes.introspect(ctx)).resolves.toBeUndefined() | ||
|
@@ -221,13 +213,18 @@ describe('Access Token Routes', (): void => { | |
expect(ctx.body).toEqual({ | ||
active: false | ||
}) | ||
|
||
scope.isDone() | ||
}) | ||
}) | ||
|
||
describe('Revocation', (): void => { | ||
let grant: Grant | ||
let token: AccessToken | ||
let id: string | ||
let managementId: string | ||
let url: string | ||
|
||
const method = 'DELETE' | ||
|
||
beforeEach(async (): Promise<void> => { | ||
grant = await Grant.query(trx).insertAndFetch({ | ||
|
@@ -237,52 +234,79 @@ describe('Access Token Routes', (): void => { | |
grantId: grant.id, | ||
...BASE_TOKEN | ||
}) | ||
id = token.id | ||
managementId = token.managementId | ||
url = `/token/${managementId}` | ||
}) | ||
|
||
test('Returns status 204 even if token does not exist', async (): Promise<void> => { | ||
id = v4() | ||
managementId = v4() | ||
const ctx = createContext( | ||
{ | ||
headers: { Accept: 'application/json' }, | ||
url: `/token/${id}`, | ||
method: 'DELETE' | ||
headers: { | ||
Accept: 'application/json' | ||
}, | ||
url: `/token/${managementId}`, | ||
method | ||
}, | ||
{ id } | ||
{ id: managementId } | ||
) | ||
|
||
await accessTokenRoutes.revoke(ctx) | ||
expect(ctx.response.status).toBe(204) | ||
}) | ||
|
||
test('Returns status 204 if token has not expired', async (): Promise<void> => { | ||
const scope = nock(KEY_REGISTRY_ORIGIN) | ||
.get(KID_PATH) | ||
.reply(200, TEST_CLIENT_KEY.jwk) | ||
|
||
const ctx = createContext( | ||
{ | ||
headers: { Accept: 'application/json' }, | ||
url: `/token/${id}`, | ||
method: 'DELETE' | ||
headers: { | ||
Accept: 'application/json' | ||
}, | ||
url, | ||
method | ||
}, | ||
{ id } | ||
{ id: managementId } | ||
) | ||
|
||
ctx.request.body = { | ||
access_token: token.value, | ||
proof: 'httpsig', | ||
resource_server: 'test' | ||
} | ||
await token.$query(trx).patch({ expiresIn: 10000 }) | ||
await accessTokenRoutes.revoke(ctx) | ||
expect(ctx.response.status).toBe(204) | ||
scope.isDone() | ||
}) | ||
|
||
test('Returns status 204 if token has expired', async (): Promise<void> => { | ||
const scope = nock(KEY_REGISTRY_ORIGIN) | ||
.get(KID_PATH) | ||
.reply(200, TEST_CLIENT_KEY.jwk) | ||
|
||
const ctx = createContext( | ||
{ | ||
headers: { Accept: 'application/json' }, | ||
url: `/token/${id}`, | ||
method: 'DELETE' | ||
headers: { | ||
Accept: 'application/json' | ||
}, | ||
url, | ||
method | ||
}, | ||
{ id } | ||
{ id: managementId } | ||
) | ||
|
||
ctx.request.body = { | ||
access_token: token.value, | ||
proof: 'httpsig', | ||
resource_server: 'test' | ||
} | ||
await token.$query(trx).patch({ expiresIn: -1 }) | ||
await accessTokenRoutes.revoke(ctx) | ||
expect(ctx.response.status).toBe(204) | ||
scope.isDone() | ||
}) | ||
}) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still getting the warning and so is GitHub Actions
#387 (comment)
https://github.com/interledger/rafiki/runs/7682195245?check_suite_focus=true
I wonder if typescript should be updated in the root
package.json
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll try it and see what happens
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bumping typescript breaks a lot of stuff
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't make any sense because
^4.2.4
actually installs4.7.2
currently as the caret means any non breaking changes.