Skip to content

feat(auth): pluggable auth with static-token and OIDC/JWT modes#32

Open
mickamy wants to merge 3 commits into
mainfrom
feat/auth-authenticator
Open

feat(auth): pluggable auth with static-token and OIDC/JWT modes#32
mickamy wants to merge 3 commits into
mainfrom
feat/auth-authenticator

Conversation

@mickamy

@mickamy mickamy commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Introduces a pluggable authentication layer so adms can identify who is calling, not just whether a shared secret is present. Authentication is now selected by auth.mode, with three modes: none (open, the default), static (a shared bearer token), and oidc (validate JWTs against an OIDC issuer). Every request carries a Principal (subject, roles, claims) on its context, which lays the groundwork for role-based authorization.

Config

auth:
  mode: oidc            # none | static | oidc  (default: none)
  static:
    token_env: ADMS_TOKEN
  oidc:
    issuer: https://your-tenant.auth0.com/
    audience: adms
    roles_claim: https://adms/roles   # optional

The previous top-level auth_token_env key is removed in favor of auth.static.token_env; auth.mode: none keeps the API fully open as before.

What changed

Authenticator is a small interface (Authenticate(r) (Principal, error)) behind the authenticate middleware. The middleware stores the resolved Principal on the request context and renders a 401 (with the appropriate WWW-Authenticate challenge) when an authenticator rejects a request. /healthz stays open for every mode.

  • noneAuth — anonymous principal, fully open.
  • staticTokenAuth — the existing shared-token check, now behind the interface (SHA-256 constant-time compare preserved).
  • oidcAuth — verifies the bearer JWT's signature (against the issuer's JWKS), issuer, audience, and expiry via github.com/coreos/go-oidc/v3. sub becomes the principal subject, roles_claim (a JSON array or a space-separated string) becomes its roles, and the full validated claim set is retained.

The OIDC issuer is discovered at startup, so a misconfigured or unreachable issuer fails startup rather than every request. The JWKS is refreshed automatically on key rotation; the discovery round-trip and later key fetches are bounded by an HTTP client timeout rather than a cancelable context, so key rotation keeps working for the process lifetime.

Security

  • Fail-closed: auth.mode: static requires static.token_env, and oidc requires issuer and audience; missing values are a startup error.
  • The static-token comparison remains constant-time.
  • JWT validation checks signature, issuer, audience, and expiry; a token failing any check is rejected with 401.

Testing

  • Behavior-invariance tests for the none / static paths (status codes, WWW-Authenticate headers, /healthz bypass) plus a check that the Principal reaches the handler context.
  • OIDC verification is exercised against a mock issuer (discovery + JWKS served over httptest, RS256 tokens signed in-test): valid tokens, expired tokens, audience mismatch, tampered signatures, and a missing header, plus role extraction from both array and space-separated claims.
  • Config validation tests for each mode.

go build ./..., go test ./..., gofmt, go vet, and golangci-lint all pass.

Dependency

Adds github.com/coreos/go-oidc/v3 (with go-jose/v4 and golang.org/x/oauth2 as indirect dependencies) for OIDC discovery and JWKS verification. The static-token path stays on the standard library.

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.97080% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.87%. Comparing base (b1b00d9) to head (d93295e).

Files with missing lines Patch % Lines
internal/server/server.go 57.14% 5 Missing and 1 partial ⚠️
internal/server/oidc.go 90.69% 3 Missing and 1 partial ⚠️
internal/server/auth.go 96.96% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #32      +/-   ##
==========================================
+ Coverage   87.80%   87.87%   +0.07%     
==========================================
  Files          38       39       +1     
  Lines        2582     2688     +106     
==========================================
+ Hits         2267     2362      +95     
- Misses        209      218       +9     
- Partials      106      108       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the authentication system of adms to support multiple authentication modes: none, static (shared bearer token), and oidc (OIDC/JWT validation). It replaces the previous auth_token_env configuration with a structured auth block, introduces an Authenticator interface with context-based Principal storage, and adds OIDC token validation using the go-oidc library. Feedback on these changes suggests using the standard library's slices.Equal helper in tests instead of a manual slice comparison function. Additionally, a potential issue was raised regarding synchronous network I/O (OIDC provider discovery) during server instantiation without a cancelable context, which could cause the startup process to hang if the issuer is unreachable.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +301 to +313
func equalStringSlice(a, b []string) bool {
if len(a) != len(b) {
return false
}

for i := range a {
if a[i] != b[i] {
return false
}
}

return true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Instead of implementing a manual slice comparison helper, prefer using the standard library's slices.Equal helper to write more idiomatic and clean Go code. Note: You will need to import the slices package.

func equalStringSlice(a, b []string) bool {
	return slices.Equal(a, b)
}
References
  1. Prefer using standard library slices helpers instead of manual slice operations to write more idiomatic and clean Go code.

Comment thread internal/server/server.go
Comment on lines +126 to +141
func newAuthenticator(auth config.Auth) (Authenticator, error) {
switch auth.Mode {
case config.AuthModeNone, "":
// The empty zero value means "unset", which is open — same as none.
return noneAuth{}, nil
case config.AuthModeStatic:
return newStaticTokenAuth(auth.Token), nil
case config.AuthModeOIDC:
return newOIDCAuth(auth.OIDC)
default:
// config.buildAuth rejects unknown modes, so this is unreachable via
// the config path. Fail closed rather than silently serving an open
// API if a caller ever constructs an unexpected mode directly.
return nil, fmt.Errorf("unknown auth mode %q", auth.Mode)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Performing synchronous network I/O (OIDC provider discovery) during server instantiation (newServer / New) without a cancelable context can cause the startup process to hang (up to 10 seconds) if the OIDC issuer is unreachable, ignoring any interrupt signals (like Ctrl+C). Consider initializing the OIDC authenticator lazily or during the Prepare(ctx) phase where a cancelable context is available.

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.

1 participant