n4bb12 / verdaccio-github-oauth-ui

📦🔐 GitHub OAuth plugin for Verdaccio
https://verdaccio.org
MIT License
71 stars 45 forks source link

Auth token remains valid after being revoked through the GitHub UI #176

Closed micahbeech closed 1 year ago

micahbeech commented 1 year ago

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

  1. Revoking a single token, by logging into GitHub as the user who owns that token and clicking Revoke.
  2. Revoking all tokens, by logging into GitHub as the Org owner and clicking Revoke all user tokens.
  3. 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:

  1. Log into Verdaccio and authorize the app via the GitHub OAuth flow.
  2. Verify that you have view the appropriate packages once you are directed back to Verdaccio.
  3. Go to GitHub Settings > Applications > Authorized OAuth Apps and revoke Verdaccio.
  4. Verify that you are still able to browse packages on the Web UI.

Command line:

  1. Run npx verdaccio-github-oauth-ui --registry https://my.registry and authorize Verdaccio.
  2. Publish a package to the registry.
  3. Go to GitHub Settings > Applications > Authorized OAuth Apps and revoke Verdaccio.
  4. 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 }
n4bb12 commented 1 year ago

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:

  1. 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.
  2. 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?

micahbeech commented 1 year ago

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:

Where I'm still confused:

  1. 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.
  2. 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?
n4bb12 commented 1 year ago

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) or CliFlow (via npx 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.

micahbeech commented 1 year ago

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.