We need to add support for PATs (personal access tokens) to our security app.
The idea is to use these tokens for access to the API without having to log in to get an authorization token. One of the most obvious use cases is the Headless CMS (which is currently in development).
PATs are in no way related to authentication providers (like Cognito). They are handled by Webiny itself, so Webiny is responsible for the generation and storage of PATs as well as their validation on each request to the API.
~In the admin app, we need to expand our User form to allow creation of multiple PATs. Each PAT can be assigned scopes just as any other user. Users who have permissions to create PAT's for other users can assign all system scopes to the PAT they create (this also allows us to create tokens for anyone, without giving them the login to the system itself).~
~Users who create PATs for themselves can only create a PAT with scopes that they already own (in this case we only need PAT's name - scopes will be auto-assigned).~
Update: we decided to go with a simple NPM-style implementation. The user will have the ability to generate a token and that's it - no additional configuration required. If a client uses a token, he will have the same permissions as the user to which the token is linked with.
~I suggest we use a regular JWT token for PATs for their reliability and ease of validation without contacting other services (so it would work the same way our regular Authorization token does).~
Update: PATs, unfortunately, cannot be JWT tokens, because they contain permissions. And if they are changed from the admin side for a particular user, we would have to generate a new token, which would contain a new set of permissions. An even bigger issue is the fact that you cannot deactivate tokens since the user already has a full JWT, with all of the permissions written in it.
Because of this, I think it's better we generate our own token, save it to the database, and every time a client is making a request with a token, check it against the database. Might not be super performant, but I don't see the other way of doing it at this very moment.
Check the implementation details for some ideas on how to make it as performant as possible.
On the UI side, the story begins here: packages/app-security/src/admin/views/Account.tsx. This form provides the form-submission functionality. The way the inputs are rendered is defined in the security-view-user-account-form plugin.
The default plugin that specifies rendering can be found here: packages/app-plugin-security-cognito-theme/src/admin/index.ts.
So, you will need to add token generation UI to both. The first one will be responsible for providing the actual token-generation component, and the second one will decide where to put it (probably at the bottom of all fields).
In general, the "Create Token" UI should have the "Generate" button, and a list of all generated tokens, with the "Delete" button on the side of each token, so the users can actually delete the tokens. Once the user clicks on any of these buttons, in order to save the changes, he'll need to submit the form.
You can be creative with this UI. Check out how others are doing it if you run out of ideas (e.g. NPM). If possible, it would be nice to have a copy/paste button, so the users don't have to manually select the token.
SecurityModel modelThe SecurityModel (packages/api-security/src/plugins/models/securityUser.model.ts) needs an additional field, that will hold all of the saved tokens.
The field can be a simple string({ list: true }) field, but we could make a bit smarter, and maybe turn it into a fields({ list: true, instanceOf: PersonalAccessTokenModel }) field, where PersonalAccessTokenModel could be another model which not only stores the actual token, but also createdOn field.
Example (actually I wrote the PersonalAccessTokenModel directly in the instanceOf field here):
personalAccessTokens: fields({
list: true,
instanceOf: withFields({
token: string(),
createdOn: date()
}})()
}),
Additionally, we'll need to add logic for checking the received PAT, in here: packages/api-security/src/plugins/authentication/authenticate.ts.
So, you'll need to detect if the "Authorization" header contains a JWT or a PAT. If it's a JWT, you don't need to do anything additional. On the other hand, if we've received a PAT, then we'll need to call the Security service and ask it to give us user data for received PAT. Once the data is retrieved, we just populate the context the same way we already do when a regular JWT was received.
~How to retrieve the data for PAT? Well, you could make a GQL call. We already had a few situations like this one, e.g. packages/api-i18n/src/plugins/service.ts. Here, the service (e.g. form builder) is calling the I18N service to get a list of all I18N Locales. As you can see, we are sending the process.env.I18N_API_URL to the client, because it contains the URL to the I18N GraphQL server.~
~And how did that env variable got there? Well, we simply put it in the serverless.yml:
~
~This basically means that ALL services that are using security (there is no service that's not using it) will have to have a similar SECURITY_API_URL env variable available, otherwise, the authentication plugin won't be able to check the PAT because it wouldn't know where to initiate the request.~
~#### Can we optimize?~
After a short discussion, we agreed that it might be an overkill to call the whole GraphQL server to make a simple findByPAT query, so we decided that for this case, we should actually make a separate function for this, which the other services would invoke to check the received PAT.
authenticateByPat functionSo, the only purpose of this authenticateByPat function would be to search the user via passed PAT, and to return basic user details, which are then going to be stored in the GraphQL context. Check the details here packages/api-security/src/plugins/graphql/userResolvers/loginUsingIdToken.ts:25- you can see here all of the data that needs to go into the context. The data should be requested from the already mentioned packages/api-security/src/plugins/authentication/authenticate.ts.
Right now we're using the @webiny/serverless-apollo-service component to deploy the Security service. But since the service would now need to deploy an additional function, and not just the Apollo Gateway, we should upgrade this, by making a separate @webiny/serverless-security component. There are other services that work like this already, for example, @webiny/serverless-form-builder or @webiny/serverless-page -builder.
Note that you will probably have to utilize some AST in order to modify the authenticateByPat function code, because, for example, the commodo driver has to be set via plugins and injected into the code. The same thing, for example, happens in the @webiny/serverless-apollo-service component, these plugins get injected via AST:

The transformations start here: components/serverless-apollo-service/serverless.js:122
So, you will have to inject necessary plugins into the function too, and use them with Commodo.
In order to get a better overview of what exactly gets generated before the actual deployment, as a good example, you can check the examples/api/.webiny/formBuilder/handler.js file.
Inside of this function, you could create a simpler "fragment" version of the SecurityUser model in it, which would only have a few fields that are needed for the whole function. This fragment will definitely need to have fields like the access and personalAccessTokens field too. The SecurityUser model is located here: packages/api-security/src/plugins/models/securityUser.model.ts:74
If you want, you can extract the relevant fields into a separate withBaseUserFields HOF (you can choose a different name), so that you don't just c/p the code from SecurityUser.
I added more details to the issue.
The most significant change is the fact that PATs cannot be JWTs, but can be any string and must be checked against the Security service.
Please take a look and let me know if you have any other ideas.
I read the entire issue, it looks fine to me. I'm sure there will be more questions along the way but let's get this going and we'll work on it as we progress.
Cool.
But still, I will add more info regarding the standalone function implementation tomorrow. Didn't manage to finish the issue in time. Will notify when the issues is updated.
Something to discuss... @Pavel910
@Fsalker and I talked about PATs, and we came to the conclusion that if all PATs just inherit the permissions of the user, there isn't a strong value in enabling the user to create multiple PATs. Every PAT will have the same permissions.
With that being said, I would still enable multiple PATs creation, just in case one day we decide to add per-PAT-permissions, in which case the user will be able to assign all of the permissions that he currently owns (I believe this was the initial idea). This would also enable us to maybe implement usage tracking of each PAT.
So, right now, I would just follow the upper model structure, because it will be easily upgradeable with new fields (if needed). And also, in the short term, users can create multiple users, with different permissions, and create PATs for them. This will "sort of" mimic the "multiple PATs with different permissions" functionality.
@doitadrian @Fsalker I agree. We need to make this upgradeable. The short term solution is ok, but we do know that more advanced and fine-grained control of tokens will be necessary, so let's plan with those requirements in mind.
@Fsalker You can check the issue, and let me know the questions.
Most helpful comment
@doitadrian @Fsalker I agree. We need to make this upgradeable. The short term solution is ok, but we do know that more advanced and fine-grained control of tokens will be necessary, so let's plan with those requirements in mind.