-
Notifications
You must be signed in to change notification settings - Fork 87
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: bearer token authentication for public APIs #6376
Conversation
src/app/modules/auth/constants.ts
Outdated
export const BEARER_STRING = 'Bearer' | ||
export const BEARER_SEPARATOR = ' ' | ||
export const API_KEY_SEPARATOR = '_' | ||
export const DEFAULT_SALT_ROUNDS = 1 |
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 this be exposed in the code or should we load this in as an env var instead? From the point of view of exposing our protocol, I think loading env vars is slightly more secure. It'll also be easier for us to tweak if we want to change it.
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.
Oh! Also I think we should set more rounds since this is for the API key which is permanent, rather than something ephemeral like our OTP!
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 this be exposed in the code or should we load this in as an env var instead? From the point of view of exposing our protocol, I think loading env vars is slightly more secure. It'll also be easier for us to tweak if we want to change it.
I'm not too worried about this. The strength of the encryption is more important than hiding the number of rounds (i.e. security through obscurity on the algo). Also With bcrypt, the number of rounds is encoded in the hash, so it is visible information to anyone who gets their hand on the hash (though obviously, we should do everything we can to protect against that!).
I also think that changing the number of rounds would be a very infrequent operation, so there's no need to have it a tunable.
I think we should set more rounds since this is for the API key which is permanent, rather than something ephemeral like our OTP!
Agreed. I increased the value to 2, which performs 4 rounds. We could do more, but we need to be mindful of processing cost. In the current POC, we compute the hash on every API call (as opposed to one time on login for session-based access).
At POC staged, I'm not overly worried about this.
Random thought: Is implementing expiring bearer tokens (e.g. after a year) a common practice? Or is that not required here? |
Good comment @kenjin-work ! It's common enough, and it indeed removes risk exposure for keys, especially which are not actively in use. But it's not a must-have practice, and many small SaaS do not have expiry on their token (e.g. BetterUptime or Snyk to name just 2 of our vendors). Removing keys can be disruptive to active automations, and so it typically requires some additional mechanisms to inform users (likely with multiple reminders!) that their key will expire in, say, 60 days, 30 days, 10 days, 2 days, 1 day, BOOM 😅 This API initiative is on POC/MVP mode right now, With the primary goal of showing what the authentication model and API namespace can be. We are not even considering building the UI to generate/revoke/regenerate the API tokens yet 😅 . So we (cc @wanlingt as feature owner) can add requirements as we go to make the feature as robust as can. For example, just like we track lastLoginTime for UI sessions, so too we should track lastUsageTime for the token, and indeed we should add a tokenCreationTime too, so we are ready to add an expiration mechanism later on if we want. |
.status(StatusCodes.UNAUTHORIZED) | ||
.json({ message: 'Invalid API key' }) | ||
} | ||
req.session.user = { _id: user.id } |
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.
Just a note on this: this works because the session middleware is currently running for all routes served by express, and that is sort of wrong.
We should activate the session middleware only for routes which require it (typically under the API router), and for the new "external" APIs, our auth middleware should take responsibility to setup a compatible session object. Something as small as:
req.session.user = { _id: user.id } | |
if (!req.session) req.session = {} | |
req.session.user = { _id: user.id } |
I added some minor changes, but I realized one important thing must be changed: retrieving forms atm returns the entire user object 😱. We need a filter process to make sure any non-intended fields is not returned to the client. The comment applies for both the session base API, and bearer token auth. |
Documentation on base64 and bcrypt encoding https://en.wikipedia.org/wiki/Base64#Applications_not_compatible_with_RFC_4648_Base64
I added the API endpoint to retrieve submission count and submissions themselves (from our One more note: I'm not sure we need to place the public API endpoints within a namespace |
Also to note / possibly something for discussion. The We might need an additional date field |
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.
LGTM. Good enough for demo even in staging / prod.
Important note though: before we start adding keys into users, we MUST change the query that retrieve forms, so the api key hash information NEVER leaves the DB or server.
ATM, the auto fetch of user objects for form->admin and form->collaborators return the api token structure as a user property. We need to return the bare minimum fields for users.
* feat: add user middleware * fix: use api key hash instead of api key * feat: add apiKeySalt as a new config * feat: add getApiKeyHash * chore: add API_KEY_SALT to docker-compose.yml * feat: add separate namespace for external apis * feat: add handleListDashboardForms to external APIs * docs: add comments to api auth functions * fix: add more env vars to config * feat: add rate limit env var * tests: add fake API key salt env var * fix: use salt rounds instead of salt to generate hash * fix: remove more occurrences of apiKeySalt * feat: findUserById instead of by hash * feat: add user ID to req.body.formSg.userId when authenticating with API token * ref: refine error states, move constants into separate file * fix: remove API_KEY_SALT from docker-compose * fix: split API Key in auth middleware * fix: type ReqBody as unknown * fix: update types for req body * ref: remove unused comment * fix: remove unique from apiKeyHash in user model * fix: remove extra test env file * ref: rename external to public * ref: rename external api to public api * fix: check length of api key * docs: add comments * ref: rename external to public * fix: check length * fix: fix typo * feat: use req.session to store user id * feat: Store API key in a structure * feat: increase number of salting round used for API token hashing * refactor: use local regexes to test auth header * chore: add TODO to update lastUseAt time * fix: retrieve user._id (not user.id) * fix: read keyHash from correct location in user * refactor: remove unecessary default value * fix: add dots to match bcrypt\'s base64 alphabet Documentation on base64 and bcrypt encoding https://en.wikipedia.org/wiki/Base64#Applications_not_compatible_with_RFC_4648_Base64 * refactor: rename mapRouteExternalApiError into mapRoutePublicApiError * feat: allow retrieving submissions by api --------- Co-authored-by: Timothee Groleau <[email protected]>
Problem
Closes FRM-906
Solution
This PR allows admins with a valid API token to access public APIs. API tokens are in the format
env_version_userId_token
.Breaking Changes
Features:
/api/public/v1
)Public APIs will not share the same namespace as internal session-based APIs (
/api/v3
) as the external APIs will have different functions (e.g. updating multiple form fields at one go)user
collection asApiKeyHash
/api/public/v1/admin/forms
)Tests
Test API
Test 1: API works
crypto.randomBytes(32).toString("base64")
test_v1_${userId}_${randomString}
(instructions on how to generate the api key and hash are also here)
You should receive a
200 OK
response, with the full list of forms associated with your user.Test 2: Without authorisation header
400 Bad Request
response, with the message "Authorisation header is missing"Test 3: With wrong credentials
401 Unauthorized
responseRegression Test
Test 4: Regression test for
handleListDashboardForms
200 OK
Deploy Notes
New environment variables:
API_KEY_VERSION
: current API versionPUBLIC_API_RATE_LIMIT
: Per-minute, per-IP, per-instance request limit for public APIs