8000 feat(auth): OAuth sign-in for API and MCP clients by adeelahmad · Pull Request #18 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content

feat(auth): OAuth sign-in for API and MCP clients - #18

Merged
adeelahmad merged 3 commits into
mainfrom
claude/project-thread-icgrqy
Oct 2, 2026
Merged

adeelahmad merged 3 commits into
mainfrom
claude/project-thread-icgrqy

Conversation

@adeelahmad
@adeelahmad adeelahmad commented Oct 2, 2026 •
8000 Copy link
Copy Markdown
Owner

Requested by Adeel · project thread

Description

Before: apps and MCP clients could only reach the Lens API with a hand-made la_… API key.

After: Lens is an OAuth 2.1 authorization server. An app registers itself (RFC 7591), sends the person to the web app's consent page with a PKCE S256 challenge, and swaps the code for tokens that act as that person with their roles, read only or read and write. People see and revoke the apps they allowed under API tokens.

How: app/domain/oauth.py handles clients, one-time codes, grants and rotating refresh tokens, and stores every token as a hash. app/api/v1/routes/oauth.py serves /api/v1/oauth/register, authorize, token, revoke and grants, plus /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource. The bearer path accepts lo_ tokens. An admin's app gets the admin's namespace roles but not the administration. New settings: tokens.oauth_access_minutes and tokens.oauth_refresh_days. Web app: the /oauth/authorize consent page, "Apps with access", the settings fields and a /.well-known proxy.

Review fixes (second commit):

  • A code that comes back after it was swapped revokes the tokens it gave.
  • A rotated refresh token that comes back within the 60 s grace window is refused instead of minting a new pair.
  • Expired codes are purged.
  • A 401 asking for a bearer token carries resource_metadata= (RFC 9728).
  • /oauth/* sends frame-ancestors 'none' and X-Frame-Options: DENY.
  • The web app's backend proxy forwards a browser's own X-Forwarded-Host/-Proto only with TRUST_PROXY_HEADERS=true, and uses Host otherwise.

The Docker fixes commit that sat on the Mac session's branch is not here; it goes in #15.

Motivation and Context

Backlog item #45. The MCP server (#44) needs this first, because its clients expect OAuth discovery.

Steps to reproduce (if appropriate):

cd fastapi_backend && uv run pytest tests/api/test_oauth.py; cd nextjs-frontend && pnpm exec jest __tests__/oauth-* __tests__/backend-proxy.test.ts.

Checked in a cloud container:

  • ruff, ruff format, mypy, eslint, prettier and tsc are clean.
  • All 516 frontend tests pass.
  • next build passes, and next start serves the frame headers on /oauth/authorize.
  • The backend suite has 261 passing and 1 failing. The failing test is test_conversion.py::test_office_and_text_files_become_documents, which fails on main in that container too because of how LibreOffice is installed there.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires documentation updates.
  • I have updated the documentation accordingly.
  • My change requires dependencies updates.
  • I have updated the dependencies accordingly.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HFiySoDnqG9PX5c3WLPz5C

@adeelahmad adeelahmad self-assigned this Oct 2, 2026
Lens is an OAuth 2.1 authorization server: apps register themselves
(RFC 7591), send people to the consent page with a PKCE challenge (S256,
required) and swap the code for tokens that act as the person, with their
roles, read only or read and write. Discovery is at
/.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource,
on the web app's address too. Tokens are random and kept as hashes; refresh
tokens rotate, and one that comes back after it was swapped ends the access.
People see the apps they allowed under API tokens and revoke them there.

API: /api/v1/oauth/register, authorize (GET and POST), token, revoke, grants;
tables oauth_client, oauth_code, oauth_grant, oauth_token; the bearer path
takes lo_ tokens; settings tokens.oauth_access_minutes and
tokens.oauth_refresh_days; audited as oauth.client.register, oauth.grant and
oauth.revoke. An admin's app has the admin's roles in every namespace but
not the administration.

After a security review: redirect addresses with a user name or a backslash
are refused (a loopback match could otherwise be steered to another host),
as are the browser's and the system's own schemes; discovery names the web
app's address only as a trusted proxy reports it, else FRONTEND_URL; tokens
of a revoked grant are dead even if minted during the revoke.

Web app: /oauth/authorize (consent, signing in first), Apps with access on the
API tokens page, the two lifetimes in Settings, and the proxy passes the
browser's Host on. Checked in the browser as a viewer and an admin, in dark
mode and at 390 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zJqF1Ht8p2VjBi4j11jfg
@adeelahmad
adeelahmad force-pushed the claude/project-thread-icgrqy branch from e884d13 to 26f13ea Compare October 2, 2026 12:57
…ming

A code that comes back after it was swapped ends the access it gave, and a
rotated refresh token that comes back within the grace minute is refused
instead of minting another pair. Expired codes are swept when a new one is
made. A 401 that asks for a bearer token names the protected resource
metadata (RFC 9728), as MCP clients expect. The consent page can't be
framed. The web app passes a browser's own X-Forwarded-Host and -Proto on
to the API only with TRUST_PROXY_HEADERS=true; otherwise it reports Host.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HFiySoDnqG9PX5c3WLPz5C
@adeelahmad
adeelahmad force-pushed the claude/project-thread-icgrqy branch from 26f13ea to bc57ea2 Compare October 2, 2026 12:57
@adeelahmad adeelahmad changed the title OAuth sign-in for API and MCP clients feat(auth): OAuth 2.1 sign-in for API and MCP clients Oct 2, 2026
Regenerated openapi.json and the API client from the merged backend.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNbRrpHTCqLewRKdkmd45X
@adeelahmad adeelahmad changed the title feat(auth): OAuth 2.1 sign-in for API and MCP clients feat(auth): OAuth sign-in for API and MCP clients Oct 2, 2026
@adeelahmad
adeelahmad marked this pull request as ready for review October 2, 2026 13:31
@adeelahmad
adeelahmad merged commit 6ef44e2 into main Oct 2, 2026
1 of 11 checks passed
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

0