-
Notifications
You must be signed in to change notification settings - Fork 90
feat(backend): tenant signature validation for admin api #3164
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(backend): tenant signature validation for admin api #3164
Conversation
f4d3fb6
to
22d48fe
Compare
22d48fe
to
3805b10
Compare
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.
To clarify, is the idea that we can run Rafiki without an operator tenant?
packages/backend/src/shared/utils.ts
Outdated
} | ||
|
||
const tenantService = await ctx.container.use('tenantService') | ||
const tenantId = headers['tenantid'] |
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.
const tenantId = headers['tenantid'] | |
const tenantId = headers['tenant-id'] |
nit, but maybe kebab case is better for http headers?
|
||
const tenantService = await ctx.container.use('tenantService') | ||
const tenantId = headers['tenantid'] | ||
const tenant = tenantId ? await tenantService.get(tenantId) : undefined |
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.
should we return false if we can't find the tenant?
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.
The idea here is to account for non-tenanted requests (e.g. createTenant, listTenants, other pagination requests). But this is moot since our call where the tenant id is to be expected in any request.
packages/backend/src/shared/utils.ts
Outdated
if ( | ||
verifyApiSignatureDigest( | ||
signature as string, | ||
ctx.request, | ||
config, | ||
config.adminApiSecret as string | ||
) | ||
) { | ||
ctx.tenant = tenant | ||
ctx.isOperator = true | ||
return true | ||
} |
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.
(related to my PR)
If we think that apiSecret
is required in the DB schema, and we are seeding the operator tenant, we could actually avoid this extra check, and just determine whether its the operator by doing tenant.apiSecret = config.adminApiSecret
after verifyingApiSignatureDigest
packages/backend/src/app.ts
Outdated
@@ -384,12 +398,14 @@ export class App { | |||
) | |||
|
|||
if (this.config.adminApiSecret) { |
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.
We can remove this check if this secret is required, right?
packages/backend/src/app.ts
Outdated
if (!verifyApiSignature(ctx, this.config)) { | ||
koa.use( | ||
async (ctx: TenantedHttpSigContext, next: Koa.Next): Promise<void> => { | ||
if (!verifyTenantOrOperatorApiSignature(ctx, this.config)) { |
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.
if (!verifyTenantOrOperatorApiSignature(ctx, this.config)) { | |
if (!(await verifyTenantOrOperatorApiSignature(ctx, this.config))) { |
to mirror the PR just merged into main
|
||
ctx.request.body = requestBody | ||
|
||
const result = await verifyTenantOrOperatorApiSignature(ctx, config) |
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.
Might be good to check that the tenantService.get
was in fact called
signature as string, | ||
ctx.request, | ||
config, | ||
config.adminApiSecret as string |
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.
config.adminApiSecret as string | |
config.adminApiSecret |
Since it should already be typed
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.
nit: but we can pass in either whole of config
or adminApiSecret
+ adminApiSignatureVersion
explicitly
Integration tests should start working again once #3177 is in |
packages/backend/src/shared/utils.ts
Outdated
|
||
if (!(await canApiSignatureBeProcessed(signature, ctx, config))) return false | ||
|
||
// First, try validating with the tenant api secret |
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.
// First, try validating with the tenant api secret |
packages/backend/src/shared/utils.ts
Outdated
Verifies http signatures by first attempting to replicate it with a secret | ||
associated with a tenant id in the headers, then with the configured admin secret. |
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.
Would be good to update this comment
packages/backend/src/app.ts
Outdated
type TenantedHttpSigHeaders = HttpSigHeaders & Record<'tenantId', string> | ||
|
||
type TenantedHttpSigRequest = Omit<HttpSigContext['request'], 'headers'> & { | ||
headers: TenantedHttpSigHeaders | ||
} | ||
|
||
export type TenantedHttpSigContext = HttpSigContext & { | ||
headers: TenantedHttpSigHeaders | ||
request: TenantedHttpSigRequest | ||
tenant?: Tenant | ||
isOperator: boolean | ||
} |
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.
These types seem to be more suited for the Open Payments server. The main thing we need to do here is to add the the tenant object to the GraphQL context (ApolloContext
& lines 409 -> 418), such that we can get the tenant information in the resolvers
602aa6e
to
23b121d
Compare
packages/backend/src/shared/utils.ts
Outdated
@@ -171,6 +173,53 @@ async function canApiSignatureBeProcessed( | |||
return true | |||
} | |||
|
|||
export interface TenantApiSignatureResult { | |||
tenant?: Tenant |
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.
tenant?: Tenant | |
tenant: Tenant |
(same for TenantedApolloContext
)
Changes proposed in this pull request
Context
Fixes #2928.
Checklist
fixes #number
user-docs
label (if necessary)