Skip to content

Personal access tokens for programatic API access#176

Merged
piffio merged 2 commits intopiffio:mainfrom
mfcarroll:feat/api-keys
Mar 26, 2026
Merged

Personal access tokens for programatic API access#176
piffio merged 2 commits intopiffio:mainfrom
mfcarroll:feat/api-keys

Conversation

@mfcarroll
Copy link
Copy Markdown
Contributor

Feature: Personal Access Tokens (API Keys)

Description

This PR implements a secure Personal Access Token (PAT) system, allowing users to interact with the Rushomon API programmatically.

Tokens follow the industry-standard "Prefix + Secret" format (ro_pat_...) and are stored using SHA-256 hashing. This ensures that even in the event of a database leak, raw tokens remain unrecoverable. Users are shown the raw token exactly once upon creation.

Key Changes

🔑 Backend (Rust)

  • Database Migration: Added api_keys table with support for names, expiration, and last-used tracking.
  • Security: Implemented SHA-256 hashing for token storage.
  • Middleware: Updated the authentication stack to detect Authorization: Bearer ro_pat_... headers, validate against the database, and automatically update the last_used_at timestamp.
  • API Endpoints: Created handlers for Create, List, and Revoke operations.

🎨 Frontend (SvelteKit)

  • Developer Settings: Added a new section in the Account Settings page to manage keys.
  • Secure UX: Implemented a "reveal once" modal for newly generated tokens with clear security warnings.
  • Validation: Added accessibility-friendly forms and a11y-compliant labels for the key management UI.

🧪 Testing

  • Integration Tests: Added tests/api_keys_test.rs covering the full lifecycle:
    • Successful creation and "once-only" raw token visibility.
    • Programmatic authentication via Bearer token.
    • Authentication failure after revocation.
    • Rejection of invalid/malformed tokens.

Security Checklist

  • Raw tokens are never stored in the database (hashed only).
  • Raw tokens are only returned in the API response during the initial POST request.
  • Tokens are scoped to the user_id and org_id of the creator.
  • Middleware includes protection against timing attacks via database lookups.

@piffio piffio self-assigned this Mar 17, 2026
@piffio
Copy link
Copy Markdown
Owner

piffio commented Mar 21, 2026

Hello @mfcarroll

I'd like to get this feature included in the upcoming version 0.7.

In order for me to fully review it, I'd need you to do a couple of things first:

  1. Please rebase it against the latest main
  2. Bump the migration version, as 0022 has already been used.
  3. Fix the FMT/Clippy errors. For this purpose I recommend you set up the Git pre-commit hook included in the repo. you can install it via ./repo-config/scripts/setup.sh

I promise I'll take a look as soon as you do that to avoid having to to additional rebases / version bumps on the migrations.

Thanks in advance

@mfcarroll
Copy link
Copy Markdown
Contributor Author

Hi @piffio. I think that's all cleaned up now. I bumped to 026 as that was open, I assume you'd left that open intentionally for this feature as I don't see it in any other branches. If not feel free to rename too, the branch is open to edits by maintainers. Thanks again.

Copy link
Copy Markdown
Owner

@piffio piffio left a comment

Choose a reason for hiding this comment

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

Thanks for your contribution, really appreciated!

I've left a few comments here and there but nothing blocking.
There will be time to review some of those decisions in the future, none of them is a one-way door

Comment thread src/api/keys.rs
stmt.bind(&[
key_id.clone().into(),
user_ctx.user_id.into(),
user_ctx.org_id.into(),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I'll need to review this logic later as a user can in theory belong to multiple organizations.

So I guess we should scope the API key to specific organization(s) upon creation, depending on which orgs the user has access to.

The scoping should be editable.

Nothing blocking here, more of a note to self to evolve the logic in the near future

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.

Yes, that makes sense, just wasn't needed for my use (self hosted for a nonprofit organization).

Comment thread src/auth/middleware.rs

// 5. Update the 'last_used_at' timestamp inline
let _ =
crate::db::queries::update_api_key_last_used(&db, &api_key.id, now_timestamp()).await;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I understand why you're doing this, but not sure it's worth it as it means a new DB write for every API request, even when it's just fetching data.

Similarly, I'll keep it for now but I might review the decusion in the future.

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.

Very good point. Maybe the usage timestamp isn't truly needed, especially in high-traffic environments.

Comment thread .gitignore
# Environment variables and secrets
.env
.dev.vars
repo-config/config/user.sh
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Good catch :)

@piffio piffio merged commit 22201b4 into piffio:main Mar 26, 2026
6 checks passed
@mfcarroll mfcarroll deleted the feat/api-keys branch March 27, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants