Skip to content
This repository was archived by the owner on Aug 22, 2024. It is now read-only.

feat: Support for request verification - #31

Open
klcodanr wants to merge 12 commits into
mainfrom
request-verification
Open

klcodanr wants to merge 12 commits into
mainfrom
request-verification

Conversation

@klcodanr

Copy link
Copy Markdown
Contributor

Please ensure your pull request adheres to the following guidelines:

  • make sure to link the related issues in this description
  • when merging / squashing, make sure the fixed issue references are visible in the commits, for easy compilation of release notes

Related Issues

https://trello.com/c/2A4zpMbE/250-create-commons-library-for-creating-verifying-frontegg-backend-tokens

Notes:

  • This includes breaking changes to the SecretsManager and the Router
  • SecretsManager changes: improving the structure of the secrets to better allow for identifying / managing secrets by making the pattern:
    [scope:test,prod]/group:companyId,shared]/[application]/[key]
  • Router - changed the parameters passed to the handler to have the params be the first and an info object containing the request and context as the second. The impetus being that generally you already have the context and request available and in most case I was ignoring these params which made for clunky code
  • This implementation is pretty naive -- we probably should put some caching in place
  • The code for checking roles / permissions doesn't quite work for M2M tokens as there's a different method that we're supposed to use, but it doesn't work the way described in Frontegg's docs (but that's not essential now anyway because the scope of this isn't M2M tokens)

Thanks for contributing!

@klcodanr
klcodanr requested review from jdelbick and mattvryan May 25, 2023 13:21
Comment thread test/router.test.js
const response = await router.handle(
new Request('https://localhost/', {
headers: {
authorization: 'Bearer yourbearerhere',

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "Bearer yourbearerhere" is used as [authorization header](1).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-_-

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤣

Comment thread test/security.test.js

it('rejects non-bearer authorization header', async () => {
const request = createRequest('test-space', token);
request.headers.set('authorization', 'basic YWRtaW46YWRtaW4=');

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "basic YWRtaW46YWRtaW4=" is used as [authorization header](1).

@jdelbick jdelbick left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly questions, but so far LGTM

Comment thread docs/API.md
| spaceId | <code>string</code> | the space for which the token will be generated |
| roleKeys | <code>Array.&lt;string&gt;</code> | the role keys for which the token should be generated |
| generator | <code>string</code> | provides attribution for the key in the description |
| [expiresInMinutes] | <code>number</code> | the number of minutes before the token expires (or 14 days) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qs: why isn't the companyId needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not used for validating the token, but it does make sense that we need to validate that as well as the space id.

Comment thread src/router.js
Comment thread src/router.js
Comment thread src/security.js Outdated
Comment thread src/security.js Outdated
Comment thread src/security.js
const spaceId = this.#getRequiredHeader(req, 'x-space-id');
const payload = jwt.decode(token);

// first validate that the tenant is correct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qs: is there a way to validate the company id is a valid company id too/first?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question. cc/ @alexkli

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the record, I am working on getting the company id (parent tenant id) into the JWT. this is possible using the jwt generation frontegg prehook. adding this new hook over in adobe/content-lake-frontegg-service.

Comment thread src/security.js
* @param {Array<string>} roleIds
* @param {number | undefined} expiresInMinutes
*/
async #generateAccessToken(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the process through the UI -> backend, the request will already have this access token from frontegg right?

so token generation is only needed in situations where the endpoint is called from outside frontegg login screen (ie extractors or manual requests)

@klcodanr klcodanr May 25, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, the only real case right now is to support the IT, however, this could be used for backend calls (for example Dropbox Extractor calling Ingestor)

Comment thread test/router.test.js
const response = await router.handle(
new Request('https://localhost/', {
headers: {
authorization: 'Bearer yourbearerhere',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-_-

Comment thread it/security.test.js
Comment thread it/security.test.js
@github-actions

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

Comment thread it/security.test.js
});

it('will not authorize invalid token', async () => {
await assert.rejects(() => security.authorize(createRequest('not a valid token')));

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "not a valid token" is used as [authorization header](1).
@jdelbick

Copy link
Copy Markdown
Contributor

fyi 2 of the security tests are failing:

 2 failing

  1) Security Unit Tests
       authorization
         rejects malformed bearer token:
     AssertionError [ERR_ASSERTION]: undefined == 401
      at assertFailsWithStatus (file:///home/circleci/project/test/security.test.js:42:10)
      at async Context.<anonymous> (file:///home/circleci/project/test/security.test.js:75:7)

  2) Security Unit Tests
       authorization
         rejects tokens from other issuers:
     AssertionError [ERR_ASSERTION]: undefined == 401
      at assertFailsWithStatus (file:///home/circleci/project/test/security.test.js:42:10)
      at async Context.<anonymous> (file:///home/circleci/project/test/security.test.js:86:7)

@klcodanr

klcodanr commented Jun 1, 2023

Copy link
Copy Markdown
Contributor Author

Tests are fixed, thanks @jdelbick

@codecov

codecov Bot commented Jun 1, 2023

Copy link
Copy Markdown

Codecov Report

Merging #31 (22a4a02) into main (f04f985) will decrease coverage by 0.59%.
The diff coverage is 98.39%.

@@            Coverage Diff             @@
##             main      #31      +/-   ##
==========================================
- Coverage   97.14%   96.56%   -0.59%     
==========================================
  Files          11       12       +1     
  Lines        1263     1688     +425     
==========================================
+ Hits         1227     1630     +403     
- Misses         36       58      +22     
Impacted Files Coverage Δ
src/schema-validator.js 80.23% <90.90%> (-19.77%) ⬇️
src/security.js 98.52% <98.52%> (ø)
src/cloud-search-index-storage.js 100.00% <100.00%> (ø)
src/context.js 100.00% <100.00%> (ø)
src/index.js 100.00% <100.00%> (ø)
src/router.js 100.00% <100.00%> (ø)
src/secret.js 94.26% <100.00%> (+0.40%) ⬆️

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@alexkli

alexkli commented Jun 2, 2023

Copy link
Copy Markdown

@klcodanr If you don't mind, I would approach it a bit differently. Instead of adding it to our custom Router mechanism, I would like to be more Franklin/helix compatible and plug this in as a wrapper function, like for example helix-shared-body-data.

This wrapper would respond with a 401 or 403 if authN or (basic tenant) authZ fails. If successful, it would set the necessary tenant and user information on the context object, say as context.user and context.space with the various ids and the parsed token (including roles & permissions), so it is easily available to any downstream code. Fine grained authZ can happen in the main function later by looking at the context object - as that might be different depending on what API/resource is being accessed and thus is probably best right in the code that has that information.

This would make it easy to plug this into our function, without also having to depend on the Router and everything that comes with it. And we don't introduce any new dependencies on code as they only need to look at context.* - we did a similar thing in Nui (nui/auth - you can ignore that this is its own serverless function, Nui has its own params payload that is passed through various functions in its execution flow).

@klcodanr

klcodanr commented Jun 2, 2023

Copy link
Copy Markdown
Contributor Author

That makes sense as long as the function has the same authorization/authentication requirements for any invocation. The Extractor doesn't as neither the webhook or callback endpoints require any form of auth but I think the Extractor is an anomaly and could be handled by just calling the functions underlying the auth wrapper inside the handler functions.

@jdelbick would you agree with that or are there other cases where having more fine-grained control at the router level would be helpful?

@alexkli

alexkli commented Jun 5, 2023

Copy link
Copy Markdown

@klcodanr I am not sure I see the implication - if there is a different authorization requirement, then it's different logic either way - a different wrapper function OR Router "plugin". The wrapper is not forced onto all our functions, you would add it to the index.js file of each lambda that needs it.

Or are you saying we already have lambdas (eg. in the extractor) which handle multiple sub apis/routes (/one, /two, /three) where each might have different authN & authZ requirements? If yes we could make this configurable. Or maybe the (new) library exports a function just for the auth logic that could be used and directly called from anywhere (e.g. inside an api/route like /one) – and separately it exports a wrapper that happens to call this very function, basically to make it easy and convenient to use for (most?) lambdas that need the same authN for all their requests.

@klcodanr

klcodanr commented Jun 5, 2023

Copy link
Copy Markdown
Contributor Author

Yes, I'm referring to the second case and I agree with that approach. Make it easier for the 90% and expose a function (or functions) for checking more involved use cases.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants