Auth token remains valid after being revoked through the GitHub UI
micahbeech opened this issue · comments
Bug Report
Versions
Version | |
---|---|
Verdaccio | 5.19.1 |
This plugin | 6.0.1 |
Node | 18.12.1 |
Observed behavior
When revoking tokens following the instructions under https://github.com/n4bb12/verdaccio-github-oauth-ui/blob/master/docs/configuration.md#revoking-tokens, it appears as though the token remains valid despite it being revoked.
Once I have revoked a token, I can continue to browse packages and their versions in the Web UI, pull packages via the command line, and publish new packages. It's only once I logout of the Verdaccio UI or clear my local token (i.e. I'm forced to generate a new one) that my requests are rejected and I have to re-authorize the GitHub app.
I have tried invalidating tokens a few ways, namely
- Revoking a single token, by logging into GitHub as the user who owns that token and clicking
Revoke
. - Revoking all tokens, by logging into GitHub as the Org owner and clicking
Revoke all user tokens
. - Rotating the secret for the OAuth app.
- In my eyes, this was a last resort since 1 and 2 are the documented methods (as per the above link), but it seemed like a surefire approach since changing the secret should mean that all JWTs signed with the old secret become invalid. To me, this implies that each request is not being verified against the secret currently provided in the config.
Note: I am seeing requests getting rejected after a token expires. However, I need to maintain long-lasting tokens for CI builds, and therefore need a reliable way to revoke them should they be compromised.
Expected behavior
After revoking a token, it should no longer be able to access or publish packages in Verdaccio.
Steps to reproduce
This can be reproduced via the Web UI or via the command line.
Web UI:
- Log into Verdaccio and authorize the app via the GitHub OAuth flow.
- Verify that you have view the appropriate packages once you are directed back to Verdaccio.
- Go to GitHub
Settings > Applications > Authorized OAuth Apps
and revoke Verdaccio. - Verify that you are still able to browse packages on the Web UI.
Command line:
- Run
npx verdaccio-github-oauth-ui --registry https://my.registry
and authorize Verdaccio. - Publish a package to the registry.
- Go to GitHub
Settings > Applications > Authorized OAuth Apps
and revoke Verdaccio. - Verify that you are still able to publish packages.
Additional context
Dockerfile:
ARG VERDACCIO_TAG
FROM verdaccio/verdaccio:${VERDACCIO_TAG}
USER root
ARG VERDACCIO_GITHUB_OAUTH_VERSION
RUN npm install --global verdaccio-github-oauth-ui@${VERDACCIO_GITHUB_OAUTH_VERSION}
USER verdaccio
Config:
storage: /verdaccio/storage/data
max_body_size: 200mb
web:
title: Verdaccio
middlewares:
github-oauth-ui:
enabled: true
audit:
enabled: true
auth:
github-oauth-ui:
org: my-org
client-id: ${githubClientId}
client-secret: ${githubClientSecret}
token: ${githubToken}
uplinks:
npmjs:
url: https://registry.yarnpkg.com/
cache: true
maxage: 2h
agent_options:
keepAlive: true
maxSockets: 40
maxFreeSockets: 10
packages:
"**":
access: $authenticated
publish: $authenticated
unpublish: $authenticated
proxy: npmjs
security:
api:
legacy: false
jwt:
sign:
expiresIn: 365d
web:
sign:
expiresIn: 365d
user_agent: true
logs: { type: stdout, format: json, level: http }
Hi @micahbeech, thanks for sharing about this.
I believe there are two parts to what you observed.
1. Revoking Registry Owner Tokens
I need to maintain long-lasting tokens for CI builds, and therefore need a reliable way to revoke them should they be compromised.
TLDR
The revocation docs are outdated and refer to the old behavior. I will update them.
You need to revoke the auth.github-oauth-ui.token
that you configured in the Verdaccio config, not the user tokens from the OAuth flow.
You can revoke or regenerate it at https://github.com/settings/tokens where you originally created it.
Why revoking user tokens doesn't do anything
The plugin no longer uses nor requests any permissions from registry users. The only information used from the OAuth token is the GitHub user name which is used to identify the user. Consequentially, revoking user tokens does nothing besides resetting the OAuth screen (users need to re-confirm sharing their public information).
The only token used by the plugin to communicate with GitHub is the auth.github-oauth-ui.token
that you configured in the verdaccio config. This is what you need to protect and revoke.
In the past, the plugin used end-user tokens to request membership information from GitHub. However, this required users to grant access to their private orgs, private repos, and so on, to determine whether they had the required memberships to meet the configured package rules.
This meant that every registry user had to have a high level of trust in the registry and grant access to a lot of private data on GitHub. And there was no way for users to limit how much exactly was shared with the registry. For example, you could only share all your private repos, or none. (I believe this now changed since GitHub introduced more granular tokens, but back then it wasn't possible).
Therefore, I changed the behavior to rely on a single token configured by the registry owner. The registry owner has control over all aspects involved: the Verdaccio config, the package access rules, the orgs, repos, and teams used in the package config, the deployment and secrets management, the token, and the scope of the token. Registry owners therefore only need to trust themselves. Users need to trust no one since they don't need to share anything. This is a much more ideal situation.
2. Revoking Access to the registry
Once I have revoked a token, I can continue to browse packages and their versions in the Web UI, pull packages via the command line, and publish new packages. It's only once I logout of the Verdaccio UI or clear my local token (i.e. I'm forced to generate a new one) that my requests are rejected and I have to re-authorize the GitHub app.
TLDR
Based on your configuration, you set your JWTs to be valid for 365d. Hence, the tokens issued by Verdaccio remain valid for that period of time.
There is no real revocation mechanism for these tokens, which is why they should be relatively short-lived.
How it works
Verdaccio first authenticates the user, then uses the result of the authentication for the duration of the token's validity.
In more detail:
- Authentication
This is where we perform the OAuth flow, determine who the user is, and derive which of the projects, teams, and orgs configured in the Verdaccio config the user has access to. We then encode this information as "username" and "groups" and hand it over to Verdaccio. Verdaccio uses it to issue an API token or UI token. - Access Checks (can access, can publish, can unpublish)
Here, the plugin is not asked to re-authenticate. Verdaccio only presents the previously authenticated user, including the "groups" created earlier. You configured this information to be valid for one year. Hence it is re-used for one year. We only match the "groups" with the attempted action based on the package access config.
Does this sufficiently address your points?
Hey @n4bb12, I appreciate your quick and thorough response. Just a few follow up points to make sure I'm understanding the flow correctly.
The auth flow as I understand it:
- If a user does not have a valid Verdaccio token, they can generate one by initiating the
WebFlow
(via the Web UI's login button) orCliFlow
(vianpx verdaccio-github-oauth-ui --registry https://my.registry
). - The above flows first retrieve a GitHub OAuth token, either by generating a new one if the app has not already been authorized by the GitHub user, or retrieving the existing one if there is already an installed OAuth app for the plugin on the user's account.
- The OAuth token is used to retrieve the username and groups the user belongs to. Once this has been done, the GitHub OAuth token is not needed until the user has to generate a new Verdaccio token.
- The plugin then creates a user with the retrieved name and groups, and gets Verdaccio to encrypt the user as a JWT using the sign options from the config and some secret
S
. - On subsequent calls, Verdaccio verifies the JWT using
S
, and upon success calls the plugin to check access for the requested resource with the already decrypted user. If the token is invalid, the call fails.
Where I'm still confused:
- What is the secret
S
? If I am able to change this, I have a way to invalidate compromised tokens should I need to.- Maybe this is a question for Verdaccio and not you - feel free to redirect me if you don't know the answer.
- What is
Plugin.authenticate
for? The way it is written, it looks like it gets called with a GitHub OAuth token, however based on my understanding of the above Verdaccio never gets an OAuth token to call it with. So when is this flow initiated?
Sure, I will just quote your points and add my thoughts.
If a user does not have a valid Verdaccio token, they can generate one by initiating the
WebFlow
(via the Web UI's login button) orCliFlow
(vianpx verdaccio-github-oauth-ui --registry https://my.registry
).
Yes. I'm not sure I would call it "Verdaccio token" since they are a bit different. One is for use with npm, one is for the UI.
The above flows first retrieve a GitHub OAuth token, either by generating a new one if the app has not already been authorized by the GitHub user, or retrieving the existing one if there is already an installed OAuth app for the plugin on the user's account.
Roughly that's what happens, although a new token is created per redirect flow, not per app installation. The app installation is only there to remember the user confirmation. You will still get a new token each time you sign in.
You can read more about how the flow works here or try out an interactive flow here.
The OAuth token is used to retrieve the username and groups the user belongs to. Once this has been done, the GitHub OAuth token is not needed until the user has to generate a new Verdaccio token.
The OAuth token is only used to determine the user name. If using JWTs, it is discarded. We never use it again. If using AES encryption, we store it in the AES payload and use it to re-identify the user on each API call.
GitHub memberships are determined using the auth.github-oauth-ui.token
that you configured in the verdaccio config. User tokens are never used for this purpose.
The plugin then creates a user with the retrieved name and groups, and gets Verdaccio to encrypt the user as a JWT using the sign options from the config and some secret
S
.
Yes.
On subsequent calls, Verdaccio verifies the JWT using
S
, and upon success calls the plugin to check access for the requested resource with the already decrypted user. If the token is invalid, the call fails.
Yes.
What is the secret
S
?
Verdaccio generates a random secret at first startup and stores it in storage/.verdaccio-db.json
.
If I am able to change this, I have a way to invalidate compromised tokens should I need to.
Yes, and good point.
What is
Plugin.authenticate
for?
In general, the when and how is up to Verdaccio and can change (and has changed in the past). The docs state "on each request" but this refers to the time before Verdaccio used JWTs and is now outdated.
When using JWTs, I would expect Verdaccio to just decode the JWT and there's your user. No need to re-authenticate or re-evaluate memberships. It looks like this is the relevant code.
When not using JWTs, it could be called when using Basic Auth or the "legacy" AES-encrypted tokens. Try disabling the JWT settings in your Verdaccio config to observe this.
For other plugins, the answers might be different. For this plugin, and when using JWTs, plugin.authenticate
is not used.
The way it is written, it looks like it gets called with a GitHub OAuth token, however, based on my understanding of the above Verdaccio never gets an OAuth token to call it with. So when is this flow initiated?
In general, Verdaccio passes the username and some sort of secret/token/password. What this is depends on the plugin and the token mechanism.
In our case, if JWTs are not used, we AES-encrypt the user token from the OAuth flow. See here. This token is then handed back to us in plugin.authenticate
after Verdaccio already decrypted and split it for us.
It is a bit confusing, especially due to old behaviors in both Verdaccio and this plugin. But I hope this helped shed some light.
Very helpful, thank you!
I'll leave this open since I think some docs still need to be updated, but no other questions from me at the moment.