Skip to content

Commit

Permalink
feat: httpsig middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
njlie committed Jul 26, 2022
1 parent 3576261 commit c5147fd
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 73 deletions.
2 changes: 1 addition & 1 deletion packages/auth/src/accessToken/routes.test.ts
Expand Up @@ -138,7 +138,7 @@ describe('Access Token Routes', (): void => {
await expect(accessTokenRoutes.introspect(ctx)).resolves.toBeUndefined()
expect(ctx.status).toBe(404)
expect(ctx.body).toMatchObject({
error: 'invalid_client',
error: 'invalid_request',
message: 'token not found'
})
})
Expand Down
63 changes: 0 additions & 63 deletions packages/auth/src/accessToken/routes.ts
Expand Up @@ -44,39 +44,6 @@ async function introspectToken(
return
}

const sig = ctx.headers['signature']
const sigInput = ctx.headers['signature-input']

if (
!sig ||
!sigInput ||
typeof sig !== 'string' ||
typeof sigInput !== 'string'
) {
ctx.status = 400
ctx.body = {
error: 'invalid_request',
message: 'invalid signature headers'
}
return
}

const verified = await deps.clientService.verifySigFromBoundKey(
sig,
sigInput,
'value',
body['access_token'],
ctx
)
if (!verified.success) {
ctx.status = verified.status || 401
ctx.body = {
error: verified.error || 'request_denied',
message: verified.message || null
}
return
}

const introspectionResult = await deps.accessTokenService.introspect(
body['access_token']
)
Expand Down Expand Up @@ -121,36 +88,6 @@ async function revokeToken(
deps: ServiceDependencies,
ctx: AppContext
): Promise<void> {
const sig = ctx.headers['signature']
const sigInput = ctx.headers['signature-input']

if (
!sig ||
!sigInput ||
typeof sig !== 'string' ||
typeof sigInput !== 'string'
) {
ctx.status = 400
ctx.body = {
error: 'invalid_request'
}
return
}

const verified = await deps.clientService.verifySigFromBoundKey(
sig,
sigInput,
'managementId',
ctx.params['managementId'],
ctx
)
if (!verified.success) {
ctx.status = verified.status || 401
ctx.body = {
error: verified.error || 'invalid_client'
}
}

const revocationError = await deps.accessTokenService.revoke(
ctx.params['managementId']
)
Expand Down
34 changes: 26 additions & 8 deletions packages/auth/src/app.ts
Expand Up @@ -150,6 +150,7 @@ export class App {

const accessTokenRoutes = await this.container.use('accessTokenRoutes')
const grantRoutes = await this.container.use('grantRoutes')
const clientService = await this.container.use('clientService')

const openApi = await this.container.use('openApi')
const toRouterPath = (path: string): string =>
Expand All @@ -167,12 +168,15 @@ export class App {
for (const path in openApi.paths) {
for (const method in openApi.paths[path]) {
if (isHttpMethod(method)) {
let useHttpSigMiddleware = false
let route: (ctx: AppContext) => Promise<void>
if (path.includes('continue')) {
route = grantRoutes[grantMethodToRoute[method]]
} else if (path.includes('token')) {
useHttpSigMiddleware = true
route = accessTokenRoutes[tokenMethodToRoute[method]]
} else if (path.includes('introspect')) {
useHttpSigMiddleware = true
route = accessTokenRoutes.introspect
} else {
if (path === '/' && method === HttpMethod.POST) {
Expand All @@ -183,14 +187,28 @@ export class App {
continue
}
if (route) {
this.publicRouter[method](
toRouterPath(path),
createValidatorMiddleware<ContextType<typeof route>>(openApi, {
path,
method
}),
route
)
if (useHttpSigMiddleware) {
this.publicRouter[method](
toRouterPath(path),
createValidatorMiddleware<ContextType<typeof route>>(openApi, {
path,
method
}),
// TODO: httpsig middleware goes here if applicable
clientService.tokenHttpsigMiddleware,
route
)
} else {
this.publicRouter[method](
toRouterPath(path),
createValidatorMiddleware<ContextType<typeof route>>(openApi, {
path,
method
}),
// TODO: httpsig middleware goes here if applicable
route
)
}
// TODO: remove once all endpoints are implemented
} else {
this.publicRouter[method](
Expand Down
74 changes: 73 additions & 1 deletion packages/auth/src/client/service.ts
Expand Up @@ -74,6 +74,11 @@ export interface ClientService {
validateClientWithRegistry(clientInfo: ClientInfo): Promise<boolean>
getRegistryDataByKid(kid: string): Promise<RegistryData>
sigInputToChallenge(sigInput: string, ctx: AppContext): string | null
tokenHttpsigMiddleware(
ctx: AppContext,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
next: () => Promise<any>
): Promise<void>
}

export async function createClientService({
Expand Down Expand Up @@ -111,7 +116,10 @@ export async function createClientService({
validateClientWithRegistry(deps, clientInfo),
getRegistryDataByKid: (kid: string) => getRegistryDataByKid(deps, kid),
sigInputToChallenge: (sigInput: string, ctx: AppContext) =>
sigInputToChallenge(sigInput, ctx)
sigInputToChallenge(sigInput, ctx),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tokenHttpsigMiddleware: (ctx: AppContext, next: () => Promise<any>) =>
tokenHttpsigMiddleware(deps, ctx, next)
}
}

Expand Down Expand Up @@ -300,3 +308,67 @@ function verifyJwk(jwk: JWKWithRequired, keys: RegistryKey): boolean {
// TODO: update this to reflect eventual shape of response from registry
return !!(!keys.revoked && isJwkViable(keys) && keys.x === jwk.x)
}

// TODO: tests for this
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function tokenHttpsigMiddleware(
deps: ServiceDependencies,
ctx: AppContext,
next: () => Promise<any>
): Promise<void> {
const sig = ctx.headers['signature']
const sigInput = ctx.headers['signature-input']

if (
!sig ||
!sigInput ||
typeof sig !== 'string' ||
typeof sigInput !== 'string'
) {
ctx.status = 400
ctx.body = {
error: 'invalid_request',
message: 'invalid signature headers'
}
next()
return
}

const { body } = ctx.request
let keyName = '',
value = ''
const { path, method } = ctx

if (path.includes('/introspect') && method === 'POST') {
keyName = 'value'
value = body['access_token']
} else if (path.includes('/token') && method === 'DEL') {
keyName = 'managementId'
value = ctx.params['managementId']
} else {
// Is not a route that requires httpsig validation, somehow
next()
return
}

const verified = await verifySigFromBoundKey(
deps,
sig,
sigInput,
keyName,
value,
ctx
)

if (!verified.success) {
ctx.status = verified.status || 401
ctx.body = {
error: verified.error || 'request_denied',
message: verified.message || null
}
next()
return
}

next()
}

0 comments on commit c5147fd

Please sign in to comment.