Skip to content

feat(auth): optional IA S3 auth plugin for patron borrow requests#184

Closed
mekarpeles wants to merge 1 commit into
mainfrom
183/ia-patron-auth
Closed

feat(auth): optional IA S3 auth plugin for patron borrow requests#184
mekarpeles wants to merge 1 commit into
mainfrom
183/ia-patron-auth

Conversation

@mekarpeles

@mekarpeles mekarpeles commented Jun 2, 2026

Copy link
Copy Markdown
Member

Summary

Closes #183

Adds an optional IA auth plugin that allows a logged-out patron to borrow a book by supplying their Internet Archive S3 credentials as an Authorization: LOW <access>:<secret> HTTP header. When valid, Lenny creates the loan and sets a session cookie — bypassing the OTP flow entirely.

Opt-in: only Lenny instances that set IA_AUTH_ENABLED=true in their environment will use this plugin. All other instances are unaffected.

Changes

lenny/configs/__init__.py

  • New IA_AUTH_ENABLED env var (default false)

lenny/core/auth.py

  • New async verify_ia_s3_keys(access, secret) -> Optional[str] — validates credentials using internetarchive.get_session() against https://s3.us.archive.org?check_auth=1 (the same auth stack as the ia CLI; no privileged access required). Returns {username}@archive.org or None. The blocking network call is offloaded via asyncio.to_thread.

lenny/routes/api.py

  • In borrow_item(), before the session-cookie check: if IA_AUTH_ENABLED and the request carries Authorization: LOW access:secret (parsed case-insensitively, any whitespace), validate via verify_ia_s3_keys(). On success, create loan + IP-bound session cookie in one step.

Flow

POST /v1/api/items/{id}/borrow
  Authorization: LOW <ia_access>:<ia_secret>
  ↓
verify_ia_s3_keys() → internetarchive session → s3.us.archive.org?check_auth=1
  ↓ success
patron email = {username}@archive.org
  ↓
item.borrow(email) → loan created
  ↓
response: loan publication + Set-Cookie: session=... (IP-bound)

Why This Matters

The Internet Archive Labs Lenny instance hosts purchased EPUBs for OL patrons. IA patrons already have S3 keys. This plugin lets them borrow directly without going through the OTP email flow, while keeping the OTP path as the default for all other Lenny instances.

Companion PR

Test Plan

  • IA_AUTH_ENABLED=false (default): borrow endpoint behaviour unchanged, LOW header ignored
  • IA_AUTH_ENABLED=true, no Authorization header: falls through to OTP flow as before
  • IA_AUTH_ENABLED=true, Authorization: LOW valid:keys: loan created, session cookie set in response
  • IA_AUTH_ENABLED=true, Authorization: LOW bad:keys: verify_ia_s3_keys() returns None, falls through to OTP
  • Existing session cookie auth still works when IA_AUTH_ENABLED=true
  • Authorization: low valid:keys (lowercase scheme): accepted identically to LOW

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an opt-in Internet Archive (IA) patron authentication path for the borrow endpoint, allowing logged-out patrons to authenticate using IA S3 keys via an Authorization: LOW <access>:<secret> header and receive a Lenny session cookie, bypassing the OTP flow when enabled.

Changes:

  • Introduces IA_AUTH_ENABLED configuration flag (default disabled) to gate the new behavior.
  • Adds verify_ia_s3_keys() to validate IA S3 credentials against archive.org/services/xauthn/?op=s3auth and derive a patron identity.
  • Updates borrow_item() to accept LOW credentials when enabled, borrow the item, and set a session cookie on success.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
lenny/routes/api.py Adds the IA LOW Authorization-header borrow path and sets a session cookie on successful IA auth.
lenny/core/auth.py Adds async IA S3 key verification via the IA xauthn endpoint.
lenny/configs/__init__.py Adds IA_AUTH_ENABLED env flag and exports it from the configs module.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lenny/routes/api.py Outdated
Comment thread lenny/routes/api.py Outdated
Comment thread lenny/routes/api.py Outdated
Comment thread lenny/core/auth.py Outdated
mekarpeles added a commit that referenced this pull request Jun 2, 2026
…add tests

- borrow_item(): split on any whitespace and compare scheme case-insensitively
  to handle 'low', 'Low', or tabs/extra spaces between scheme and keys
- borrow_item(): validate both access and secret are non-empty after stripping
- borrow_item(): pass client IP to create_session_cookie() so IA-auth sessions
  are IP-bound, matching the OTP session cookie behaviour
- tests/test_auth.py: TestVerifyIaS3Keys with mocked httpx — success,
  screenname fallback, non-200, missing success field, network error
- tests/test_direct_auth_mock.py: four tests for IA_AUTH_ENABLED paths —
  disabled flag, valid keys create loan+cookie, invalid falls through, lowercase scheme

Addresses Copilot review threads on #184
Comment thread lenny/core/auth.py Outdated
@mekarpeles mekarpeles force-pushed the 183/ia-patron-auth branch from 017206f to 1a7952d Compare June 4, 2026 04:38
@mekarpeles

Copy link
Copy Markdown
Member Author

@copilot review

Copilot AI commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@copilot review

Reviewed. Code review and CodeQL security scan both pass with no issues.

One small follow-up was included in the latest commit: the two IA-auth tests that exercise the valid-key path now mock create_session_cookie so they don't silently rely on auth.SERIALIZER being initialised by an earlier test in the suite. Everything else — the internetarchive-based verify_ia_s3_keys(), case-insensitive LOW header parsing, IP-bound session cookie, and all four IA-auth integration tests — is unchanged.

Adds an opt-in IA_AUTH_ENABLED flag (default: false). When enabled,
borrow requests may authenticate via Authorization: LOW access:secret
headers, bypassing the OTP flow. Only Lenny instances that explicitly
set IA_AUTH_ENABLED=true use this path.

Changes:
- configs/__init__.py: add IA_AUTH_ENABLED env var
- core/auth.py: add verify_ia_s3_keys() — validates S3 keys using
  internetarchive.get_session() against s3.us.archive.org?check_auth=1
  (same library as ol_bootstrap.py; wrapped in asyncio.to_thread so the
  event loop is not blocked)
- routes/api.py: in borrow_item(), before the OTP flow, check for a LOW
  auth header (case-insensitive, any whitespace); validate both parts
  non-empty; call verify_ia_s3_keys(); create an IP-bound session cookie
  on success
- tests/test_auth.py: TestVerifyIaS3Keys — mocks internetarchive.get_session,
  covers authorized/unauthorized/non-200/error/screenname-fallback cases
- tests/test_direct_auth_mock.py: four borrow_item tests via monkeypatch +
  AsyncMock — disabled flag ignores header, valid keys create loan+cookie,
  invalid keys fall through to 401, lowercase scheme accepted

Closes #183
@ronibhakta1

Copy link
Copy Markdown
Collaborator

This feature is added in #185

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.

feat(auth): Optional IA auth plugin — allow patron borrow via IA S3 credentials

4 participants