Skip to content

fix(auth): remove dashboard bearer token and add OIDC token verification (#294) - #345

Open
parthrohit22 wants to merge 2 commits into
OWASP:devfrom
parthrohit22:fix/issue-294-oidc-auth-boundary
Open

parthrohit22 wants to merge 2 commits into
OWASP:devfrom
parthrohit22:fix/issue-294-oidc-auth-boundary

Conversation

@parthrohit22

Copy link
Copy Markdown
Collaborator

What does this PR do?

Removes the bearer token built into the dashboard and adds OIDC token verification (JWKS signature, issuer, audience, tenant, IdP app roles) to the API. This is the second containment step for #294, after #320 (token expiry, roles, subscription allowlist).

Type of change

  • New scan rule
  • Remediation playbook
  • Bug fix (security)
  • Dashboard/front-end work
  • API endpoint (authentication middleware)
  • Documentation
  • Compliance mapping

Changes

Frontend: no credential in public JS

  • Removed the VITE_JWT_TOKEN bootstrap and the dev-local-token fallback from App.jsx. Anything in a VITE_* variable is compiled into the public bundle.
  • api.js now keeps the token in memory only (setToken, getToken, clearToken) and deletes any legacy localStorage.jwt_token on load without using it. aiApi.js reads from the same in-memory store.
  • CI guard: the frontend job builds with a canary VITE_JWT_TOKEN and fails if a JWT-shaped value, the canary or dev-local-token appears in frontend/dist. I checked it both ways: the guard catches the canary on current dev code and passes with this change. The canary is assembled at run time, so Gitleaks doesn't flag the workflow file.

API: durable token boundary (api/auth.py)

The auth mode is set with OPENSHIELD_AUTH_MODE:

shared_secret (default, unchanged for local/CI) oidc (new, enterprise)
Signature HS256 with JWT_SECRET Asymmetric only (default RS256) via OIDC_JWKS_URL (5 min key cache)
Required claims exp, sub (new); iss/aud when JWT_ISSUER/JWT_AUDIENCE are set exp, iat, iss, aud, sub
Tenant tid must be in OIDC_ALLOWED_TENANTS when set
Role role claim IdP app roles (roles by default, e.g. OpenShield.Operator), mapped via OIDC_ROLE_MAP; a self-asserted role claim is ignored
  • Algorithm confusion: HS256 and alg: none tokens are refused in oidc mode before key lookup, so a token minted with JWT_SECRET can never pass as an IdP token.
  • Fails closed:
    • an unreachable JWKS endpoint returns 503
    • an identity with no OpenShield role returns 403
    • the API refuses to start if oidc mode is missing issuer, audience or JWKS, uses a non-HTTPS JWKS URL, or configures a symmetric algorithm
  • The API logs a startup warning when shared_secret mode runs in production.
  • The middleware in app.py now calls the verifier. The viewer-can't-write gate and all existing error messages are unchanged.

Docs and scripts

  • New docs/security/authentication.md, covering:
    • choosing a mode
    • Entra ID setup (app roles, required assignment, v2 tokens, env vars)
    • an 8-step containment checklist (stop exposure, suspend, rotate JWT_SECRET, restrict scope, review logs, verify, restore)
    • routine secret rotation
  • generate_demo_jwt.py now defaults to a 1h viewer token, is for API testing only, and no longer tells anyone to set it as VITE_JWT_TOKEN.
  • Updated api-reference.md, FRONTEND_API_TESTING.md, API_ENDPOINTS.txt, scripts/README.md, .env.example and CHANGELOG.md.

Behavior change to note

A dashboard deployment that relied on VITE_JWT_TOKEN will stop sending a token. Reads then work only against an API running with OPENSHIELD_PUBLIC_DEMO=true (non-sensitive data). This is intentional, per #294's containment step "Remove VITE_JWT_TOKEN and the automatic dev-local-token bootstrap". The public API is still suspended (#243).

Operator actions (not code, can't be done from a PR)

  • Delete VITE_JWT_TOKEN from the Vercel environment and rotate JWT_SECRET on the API, following the checklist in docs/security/authentication.md.
  • Before restoring the API with real data, register the Entra app roles and switch to OPENSHIELD_AUTH_MODE=oidc.

Testing

  • tests/test_oidc_auth.py: 34 new tests using a throwaway RSA key and a stub JWKS, with no network access. They cover:
    • a valid principal, highest-role selection and a custom role claim or map
    • expired tokens, wrong issuer, wrong audience, and each missing required claim
    • a tenant outside the allowlist, a missing tenant, and the allowlist being optional
    • no role → 403, and a self-asserted role claim ignored
    • an unknown signing key, an unknown kid, an HS256 token, alg: none, and a garbage token
    • an unreachable JWKS → 503
    • every startup configuration failure
    • shared-secret mode: missing sub, iss/aud checks, and RS256 refused
    • middleware end to end: viewer reads but can't write, operator writes, the HS256 conftest token → 401 in oidc mode, wrong tenant → 401
  • Existing tests/test_auth.py and tests/test_subscription_authorization.py pass unchanged.
  • Full backend suite: 1080 passed. The 2 failures in test_devops_client.py are local-only (azure-devops isn't installed on my machine) and also fail on unmodified dev.
  • Frontend: npm run lint, api.test.mjs (26, including a new memory-only/legacy-purge test), aiApi.test.mjs, usePageData.test.mjs, a11y and i18n checks, and npm run build with the bundle guard.
  • ruff check, ruff format --check and bandit -r api/ -ll are clean.
  • No hardcoded credentials or secrets.

Related issue

Partially addresses #294. Still open under that issue:

  • dashboard sign-in with Authorization Code and PKCE
  • persisted tenant/subscription ownership, with tenant context in every repository query and an RLS evaluation
  • cross-tenant integration tests across scans, findings, compliance, resources, drift, AI and enrichment

Checklist

  • Every commit includes a DCO Signed-off-by trailer
  • Branch name follows the convention: fix/description
  • I have not committed any real credentials

…ion (OWASP#294)

Frontend
- remove the VITE_JWT_TOKEN bootstrap and dev-local-token fallback
- keep bearer tokens in memory only and purge legacy localStorage tokens
- fail CI if a JWT-shaped value reaches the public bundle

API
- move token verification into api/auth.py with two modes:
  shared_secret (HS256, now also requires sub, optional iss/aud) and
  oidc (JWKS-verified asymmetric tokens with issuer, audience, expiry,
  issued-at, subject, tenant allowlist and IdP app-role mapping)
- refuse HS256/none in oidc mode, fail closed with 503 when JWKS is
  unreachable, and refuse to start on incomplete oidc configuration
- warn when shared_secret mode runs in production

Docs
- authentication setup, containment checklist and JWT_SECRET rotation
- demo JWT script is now short-lived and for API testing only

Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
Comment thread api/auth.py Fixed
Comment thread api/auth.py Fixed
Comment thread api/auth.py Fixed
Rename the shared-secret mode constant so the credential scan does not
read it as a hardcoded secret, generate the test signing secret at run
time, and reword rejection log messages flagged by Semgrep.

Signed-off-by: parthrohit22 <parthrohit60@gmail.com>

@m-khan-97 m-khan-97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I reviewed the current authentication head carefully, including the middleware boundary, OIDC configuration validation, JWKS failure behavior, algorithm restrictions, issuer/audience/expiry checks, tenant allowlist, server-side role mapping, and removal of the dashboard token bootstrap. I also ran the focused authentication and subscription suites locally: all 61 tests passed.

Approved as the next containment step. This approval is for the scope stated in the PR, not closure of #294. Before restoring an API that carries real data, the operator actions still need to be completed, and the remaining PKCE sign-in plus persisted tenant ownership/query isolation work needs to stay tracked under #294.

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.

3 participants