Skip to content

Add OIDC provider functionality with validation setup#548

Closed
shreknel wants to merge 13 commits intotinyauthapp:mainfrom
shreknel:feature/oidc-provider-pr
Closed

Add OIDC provider functionality with validation setup#548
shreknel wants to merge 13 commits intotinyauthapp:mainfrom
shreknel:feature/oidc-provider-pr

Conversation

@shreknel
Copy link
Copy Markdown

@shreknel shreknel commented Dec 30, 2025

This commit adds OpenID Connect (OIDC) provider functionality to tinyauth, allowing it to act as an OIDC identity provider for other applications.

Features:

  • OIDC discovery endpoint at /.well-known/openid-configuration
  • Authorization endpoint for OAuth 2.0 authorization code flow
  • Token endpoint for exchanging authorization codes for tokens
  • ID token generation with JWT signing
  • JWKS endpoint for public key distribution
  • Support for PKCE (code challenge/verifier)
  • Nonce validation for ID tokens
  • Configurable OIDC clients with redirect URIs, scopes, and grant types

Validation:

  • Docker Compose setup for local testing
  • OIDC test client (oidc-whoami) with session management
  • Nginx reverse proxy configuration
  • DNS server (dnsmasq) for custom domain resolution
  • Chrome launch script for easy testing

Configuration:

  • OIDC configuration in config.yaml
  • Example configuration in config.example.yaml
  • Database migrations for OIDC client storage

Summary by CodeRabbit

  • New Features

    • Built-in OpenID Connect provider: discovery, authorize (PKCE), token (access/ID), userinfo and JWKS endpoints.
    • Configurable OIDC: global enable, issuer, token expiries, and multiple client entries (secrets, names, redirect URIs, grant/response types, scopes).
    • Persistent storage for OIDC clients, keys and authorization codes; RSA-signed tokens.
  • Documentation

    • Added Docker Compose validation setup, test OIDC client, browser launch scripts and README to exercise the OIDC flow.

✏️ Tip: You can customize this high-level summary in your review settings.

This commit adds OpenID Connect (OIDC) provider functionality to tinyauth,
allowing it to act as an OIDC identity provider for other applications.

Features:
- OIDC discovery endpoint at /.well-known/openid-configuration
- Authorization endpoint for OAuth 2.0 authorization code flow
- Token endpoint for exchanging authorization codes for tokens
- ID token generation with JWT signing
- JWKS endpoint for public key distribution
- Support for PKCE (code challenge/verifier)
- Nonce validation for ID tokens
- Configurable OIDC clients with redirect URIs, scopes, and grant types

Validation:
- Docker Compose setup for local testing
- OIDC test client (oidc-whoami) with session management
- Nginx reverse proxy configuration
- DNS server (dnsmasq) for custom domain resolution
- Chrome launch script for easy testing

Configuration:
- OIDC configuration in config.yaml
- Example configuration in config.example.yaml
- Database migrations for OIDC client storage
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 30, 2025

📝 Walkthrough

Walkthrough

Adds an in-process OpenID Connect provider: new OIDC config and example, DB migrations and GORM models for clients/keys/authorization-codes, an OIDCService (RSA keys, JWTs, PKCE, client sync), an OIDCController (discovery/authorize/token/userinfo/jwks), bootstrap wiring, dependency, and validation tooling.

Changes

Cohort / File(s) Summary
Configuration & Examples
config.example.yaml, validation/config.yaml, internal/config/config.go
Add top-level oidc config block (enabled, issuer, accessTokenExpiry, idTokenExpiry) and clients map; introduce OIDCConfig and OIDCClientConfig types and example/test configs.
Database Migrations
internal/assets/migrations/000004_oidc_clients.*.sql, internal/assets/migrations/000005_oidc_keys.*.sql, internal/assets/migrations/000006_oidc_authorization_codes.*.sql
Add create/drop migrations for oidc_clients, oidc_keys, and oidc_authorization_codes tables and an index on authorization code expiry.
Data Models
internal/model/oidc_client_model.go, internal/model/oidc_key_model.go, internal/model/oidc_authorization_code_model.go
New GORM models: OIDCClient, OIDCKey, OIDCAuthorizationCode with column mappings and TableName() methods.
OIDC Service
internal/service/oidc_service.go
New OIDCService and OIDCServiceConfig: RSA key persistence/encryption, JWKS, stateless JWT authorization codes, access/ID token generation and validation, PKCE, client validation, and client sync from config.
HTTP Controller
internal/controller/oidc_controller.go
New OIDCController and config: routes and handlers for discovery, authorize, token, userinfo, and jwks; client/auth validation, PKCE handling, code issuance, and token exchange.
Bootstrap / Wiring
internal/bootstrap/service_bootstrap.go, internal/bootstrap/router_bootstrap.go
Add Services.oidcService field; conditionally initialize OIDCService when enabled (issuer defaults to AppURL), attempt non-fatal client sync, and register controller routes when service exists.
Dependencies & Utils
go.mod, internal/utils/app_utils.go, internal/utils/loaders/loader_file.go
Add github.com/golang-jwt/jwt/v5; improve cookie-domain error messages; loader accepts traefik.experimental.configfile or experimental.configfile and logs resolved config path.
Validation / Testing Tooling
validation/Dockerfile, validation/docker-compose.yml, validation/README.md, validation/nginx.conf, validation/oidc_whoami.py, validation/launch-chrome.sh, validation/launch-chrome-host.sh
New docker-compose validation environment with nginx proxy, Python OIDC test client (PKCE), browser launch scripts, and README for local OIDC integration testing.
Misc
.review_trigger
Added a trigger file to initiate automated review.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser (User)
    participant Client as OIDC Client (App)
    participant Controller as OIDCController
    participant Auth as AuthService
    participant OIDC as OIDCService
    participant DB as Database

    rect rgb(200,220,250)
      Note over Browser,DB: Authorization Code Flow (authorize)
    end

    Browser->>Controller: GET /oidc/authorize?client_id=...&redirect_uri=...&response_type=code&state=...
    Controller->>OIDC: GetClient(client_id)
    OIDC->>DB: SELECT oidc_clients WHERE client_id=...
    DB-->>OIDC: client record
    OIDC-->>Controller: client config
    Controller->>OIDC: ValidateRedirectURI/ResponseType/Scope
    Controller->>Auth: Check authentication
    alt not authenticated
        Controller-->>Browser: Redirect /login (preserve original request)
    else authenticated
        Auth-->>Controller: userContext
        Controller->>OIDC: GenerateAuthorizationCode(userContext,...,pkce)
        OIDC-->>Controller: code (JWT)
        Controller-->>Browser: Redirect to redirect_uri?code=...&state=...
    end
Loading
sequenceDiagram
    participant Client as OIDC Client (Confidential)
    participant Controller as OIDCController
    participant OIDC as OIDCService
    participant DB as Database

    rect rgb(220,250,200)
      Note over Client,DB: Token Exchange Flow (token)
    end

    Client->>Controller: POST /oidc/token (grant_type=authorization_code, code, redirect_uri, client creds)
    Controller->>Controller: Extract client creds (BasicAuth or form)
    Controller->>OIDC: GetClient(client_id)
    OIDC->>DB: SELECT oidc_clients WHERE client_id=...
    DB-->>OIDC: client record
    Controller->>OIDC: VerifyClientSecret(...)
    Controller->>OIDC: ValidateAuthorizationCode(code, client_id, redirect_uri)
    OIDC-->>Controller: userContext, scopes, nonce
    Controller->>OIDC: GenerateAccessToken(userContext, client_id, scopes)
    OIDC-->>Controller: access_token (JWT)
    Controller->>OIDC: GenerateIDToken(userContext, client_id, nonce)
    OIDC-->>Controller: id_token (JWT)
    Controller-->>Client: JSON { access_token, id_token, token_type, expires_in, scope }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: unified config #533: Modifies internal/config/config.go and related config schema — overlaps with this PR's additions to configuration types for OIDC.

Poem

🐰 I hid some keys in rows of dirt,

I hopped through tables, quiet and pert.
Codes I signed, the clients cheer,
Discovery blooms and tokens appear.
TinyAuth hums — nibble a JWT, dear. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add OIDC provider functionality with validation setup' directly reflects the main changes: introducing OIDC provider features and their validation infrastructure.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (9)
internal/config/config.go (2)

72-88: Consider enforcing default values for OIDC configuration.

The struct field descriptions specify defaults (e.g., grantTypes defaults to ['authorization_code'], scopes defaults to ['openid', 'profile', 'email']), but these are not enforced in code. Consider adding validation or initialization logic to apply these defaults when fields are empty, ensuring consistent behavior.

Example approach for enforcing defaults

You could add a method to normalize/validate the config after loading:

func (c *OIDCClientConfig) ApplyDefaults() {
    if len(c.GrantTypes) == 0 {
        c.GrantTypes = []string{"authorization_code"}
    }
    if len(c.ResponseTypes) == 0 {
        c.ResponseTypes = []string{"code"}
    }
    if len(c.Scopes) == 0 {
        c.Scopes = []string{"openid", "profile", "email"}
    }
}

Call this method after loading the configuration from YAML.


80-82: Document the mutual exclusivity of ClientSecret and ClientSecretFile.

Both ClientSecret and ClientSecretFile fields are present. Based on the pattern in OAuthServiceConfig, these appear to be mutually exclusive. Consider adding a comment or validation to clarify that only one should be set.

validation/Dockerfile (1)

5-5: Consider pinning dependency versions for reproducibility.

The pip install command installs requests and authlib without version constraints. For reproducible builds and to avoid unexpected breakage, consider pinning specific versions.

Example with pinned versions
-RUN pip install --no-cache-dir requests authlib
+RUN pip install --no-cache-dir requests==2.31.0 authlib==1.3.0

Note: Update to current stable versions as appropriate.

internal/assets/migrations/000004_oidc_clients.up.sql (1)

2-2: Remove redundant UNIQUE constraint on PRIMARY KEY.

The client_id column is declared as both PRIMARY KEY and UNIQUE. The PRIMARY KEY constraint already enforces uniqueness, making the UNIQUE keyword redundant.

Suggested fix
-    "client_id" TEXT NOT NULL PRIMARY KEY UNIQUE,
+    "client_id" TEXT NOT NULL PRIMARY KEY,
validation/launch-chrome.sh (1)

22-38: Consider verifying executable status.

The Chrome detection logic covers common paths well. However, line 34 checks file existence with [ -f "$path" ] but doesn't verify if the file is executable. In practice, Chrome binaries should be executable, but for robustness, consider:

🔎 Optional improvement for executable check
 CHROME_CMD=""
 for path in "${CHROME_PATHS[@]}"; do
-    if [ -f "$path" ] || command -v "$(basename "$path")" &> /dev/null; then
+    if [ -x "$path" ] || command -v "$(basename "$path")" &> /dev/null; then
         CHROME_CMD="$(basename "$path")"
         break
     fi
 done

This is validation tooling, so the current implementation is acceptable.

internal/model/oidc_client_model.go (1)

3-13: Consider adding helper methods for JSON array fields.

The model structure is appropriate, but the JSON array fields (lines 7-10) stored as strings will require careful marshaling/unmarshaling in the service layer.

To improve type safety and reduce marshaling errors, consider adding helper methods:

🔎 Optional helper methods
// GetRedirectURIsSlice unmarshals the RedirectURIs JSON string into a slice
func (c *OIDCClient) GetRedirectURIsSlice() ([]string, error) {
    var uris []string
    if err := json.Unmarshal([]byte(c.RedirectURIs), &uris); err != nil {
        return nil, err
    }
    return uris, nil
}

// SetRedirectURIsSlice marshals a slice into the RedirectURIs JSON string
func (c *OIDCClient) SetRedirectURIsSlice(uris []string) error {
    data, err := json.Unmarshal(uris)
    if err != nil {
        return err
    }
    c.RedirectURIs = string(data)
    return nil
}

// Similar methods for GrantTypes, ResponseTypes, and Scopes...

This encapsulates the JSON handling logic within the model, reducing the chance of marshaling errors elsewhere.

validation/README.md (1)

22-22: Consider addressing markdown linting suggestions.

Static analysis identified several minor markdown formatting issues that could improve documentation consistency:

  1. Bare URLs (lines 22, 30, 32, 38, 66, 88, 135, 163, 165): Consider wrapping in angle brackets or markdown links
  2. Missing language specifier (line 84): The /etc/hosts code block should specify a language (e.g., text or hosts)
  3. Space in code span (line 132): Remove spaces inside the backticks in --host-resolver-rules=...

Example fixes:

🔎 Proposed formatting improvements

For bare URLs:

-- **Access:** http://auth.example.com/ (via nginx on port 80)
+- **Access:** <http://auth.example.com/> (via nginx on port 80)

For code block language:

-```
+```text
 127.0.0.1 auth.example.com

For code span spacing:
```diff
-   - In "Target" field, append: ` --host-resolver-rules="MAP auth.example.com 127.0.0.1"`
+   - In "Target" field, append: `--host-resolver-rules="MAP auth.example.com 127.0.0.1"`

Based on static analysis hints. These are purely cosmetic improvements.

Also applies to: 30-30, 32-32, 38-38, 66-66, 84-86, 88-88, 132-132, 135-135, 163-163, 165-165

validation/oidc_whoami.py (1)

263-267: Consider adding SameSite attribute to session cookie.

The id_token cookie is set with HttpOnly but lacks SameSite attribute. For better CSRF protection, consider adding SameSite=Lax. Since this is a test client over HTTP, the Secure flag would break functionality.

🔎 Proposed fix
-        self.send_header("Set-Cookie", f"id_token={token['id_token']}; Path=/; Max-Age={max_age}; HttpOnly")
+        self.send_header("Set-Cookie", f"id_token={token['id_token']}; Path=/; Max-Age={max_age}; HttpOnly; SameSite=Lax")
internal/service/oidc_service.go (1)

332-343: JWKS exponent encoding may include unnecessary leading zeros.

The exponent is always encoded as 4 bytes, but the JWK spec requires minimal encoding (no leading zeros). The common RSA exponent 65537 (0x010001) should be encoded as 3 bytes, not 4.

🔎 Proposed fix: Strip leading zero bytes
-	eBytes := make([]byte, 4)
-	eBytes[0] = byte(e >> 24)
-	eBytes[1] = byte(e >> 16)
-	eBytes[2] = byte(e >> 8)
-	eBytes[3] = byte(e)
+	// Encode exponent with minimal bytes (strip leading zeros)
+	eBytes := make([]byte, 0, 4)
+	for shift := 24; shift >= 0; shift -= 8 {
+		b := byte(e >> shift)
+		if len(eBytes) > 0 || b != 0 {
+			eBytes = append(eBytes, b)
+		}
+	}
+	if len(eBytes) == 0 {
+		eBytes = []byte{0}
+	}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 986ac88 and 020fcb9.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (20)
  • config.example.yaml
  • go.mod
  • internal/assets/migrations/000004_oidc_clients.down.sql
  • internal/assets/migrations/000004_oidc_clients.up.sql
  • internal/bootstrap/router_bootstrap.go
  • internal/bootstrap/service_bootstrap.go
  • internal/config/config.go
  • internal/controller/oidc_controller.go
  • internal/model/oidc_client_model.go
  • internal/service/oidc_service.go
  • internal/utils/app_utils.go
  • internal/utils/loaders/loader_file.go
  • validation/Dockerfile
  • validation/README.md
  • validation/config.yaml
  • validation/docker-compose.yml
  • validation/launch-chrome-host.sh
  • validation/launch-chrome.sh
  • validation/nginx.conf
  • validation/oidc_whoami.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-27T11:02:49.689Z
Learnt from: steveiliop56
Repo: steveiliop56/tinyauth PR: 326
File: internal/service/database_service.go:7-11
Timestamp: 2025-08-27T11:02:49.689Z
Learning: In internal/service/database_service.go, using glebarez/sqlite (modernc) for GORM with sqlite3 (mattn) driver for golang-migrate is a valid pattern that works correctly, despite appearing to be a driver mismatch. The golang-migrate sqlite driver causes panics when used with modernc.org/sqlite backend, so the mixed approach should be maintained.

Applied to files:

  • go.mod
🧬 Code graph analysis (2)
internal/bootstrap/router_bootstrap.go (1)
internal/controller/oidc_controller.go (2)
  • NewOIDCController (31-38)
  • OIDCControllerConfig (19-22)
internal/bootstrap/service_bootstrap.go (1)
internal/service/oidc_service.go (3)
  • OIDCService (33-37)
  • NewOIDCService (39-43)
  • OIDCServiceConfig (25-31)
🪛 markdownlint-cli2 (0.18.1)
validation/README.md

22-22: Bare URL used

(MD034, no-bare-urls)


30-30: Bare URL used

(MD034, no-bare-urls)


32-32: Bare URL used

(MD034, no-bare-urls)


38-38: Bare URL used

(MD034, no-bare-urls)


66-66: Bare URL used

(MD034, no-bare-urls)


84-84: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


88-88: Bare URL used

(MD034, no-bare-urls)


132-132: Spaces inside code span elements

(MD038, no-space-in-code)


135-135: Bare URL used

(MD034, no-bare-urls)


163-163: Bare URL used

(MD034, no-bare-urls)


165-165: Bare URL used

(MD034, no-bare-urls)

🪛 Ruff (0.14.10)
validation/oidc_whoami.py

42-42: Avoid specifying long messages outside the exception class

(TRY003)


72-72: Probable use of requests call without timeout

(S113)


99-99: Do not catch blind exception: Exception

(BLE001)


293-293: Do not catch blind exception: Exception

(BLE001)


297-297: Possible binding to all interfaces

(S104)

🪛 Shellcheck (0.11.0)
validation/launch-chrome-host.sh

[warning] 31-31: Quote this to prevent word splitting.

(SC2046)

🔇 Additional comments (31)
internal/utils/app_utils.go (1)

5-5: LGTM! Enhanced error messages improve debuggability.

The addition of contextual information (URLs, IP addresses, part counts) to error messages using fmt.Errorf is a good practice that will significantly help troubleshooting configuration issues without changing the validation logic.

Also applies to: 26-26, 32-32, 40-40

config.example.yaml (1)

66-100: Well-documented OIDC configuration example.

The OIDC configuration block is clearly documented with helpful comments and aligns well with the struct definitions in internal/config/config.go. The placeholder values make it easy for users to understand what needs to be configured.

internal/assets/migrations/000004_oidc_clients.down.sql (1)

1-1: LGTM!

The down migration correctly uses DROP TABLE IF EXISTS for safe idempotent rollback.

internal/bootstrap/router_bootstrap.go (1)

105-113: Good defensive initialization pattern.

The conditional OIDC controller setup properly checks both the config flag (app.config.OIDC.Enabled) and service availability (app.services.oidcService != nil), preventing initialization errors if the service failed to start.

validation/config.yaml (1)

1-36: Validation configuration looks appropriate for local testing.

The configuration correctly sets secureCookie: false and uses HTTP URLs, which is appropriate for local Docker-based testing. The test credentials and client secrets are clearly for development/validation purposes.

go.mod (1)

14-14: No action required — v5.3.0 is the current, latest stable release with all known security patches applied.

The version github.com/golang-jwt/jwt/v5 v5.3.0 (released July 30, 2025) is the latest stable release. It includes fixes for CVE-2025-30204 (DoS via excessive memory allocation in parse.ParseUnverified) and CVE-2024-51744 (error handling in ParseWithClaims), which were patched in v5.2.2 and later. No further upgrades are needed.

internal/assets/migrations/000004_oidc_clients.up.sql (1)

5-8: Array fields are properly serialized as JSON with error handling in place.

All four columns (redirect_uris, grant_types, response_types, scopes) are correctly stored as JSON arrays. The OIDCClient model includes // JSON array documentation, and the OIDCService implements proper serialization with json.Marshal() when writing to the database and deserialization with json.Unmarshal() when reading, complete with error logging for both operations.

internal/utils/loaders/loader_file.go (2)

28-37: LGTM! Good error handling and logging.

The empty path check, contextual logging, and error handling are all appropriate. Including the config file path in both warning and error messages aids troubleshooting.


19-26: Code looks good. The dual-check pattern for flag keys (traefik.experimental.configfile and experimental.configfile) appropriately handles both namespaced and direct flag formats. The comment about paerser converting flags to lowercase is accurate—the flag keys checked are consistently lowercase, matching the paerser library's normalization behavior.

validation/nginx.conf (3)

1-9: LGTM! Appropriate Docker DNS configuration.

The resolver configuration correctly uses Docker's built-in DNS (127.0.0.11) for service name resolution. The 10-second cache validity and 5-second timeout are reasonable for a test environment.


11-25: LGTM! Good use of dynamic backend resolution.

The dynamic backend variable pattern enables nginx to resolve the backend at request time, which is appropriate for Docker service discovery. All standard proxy headers are properly forwarded.


27-41: LGTM! Consistent configuration pattern.

The client.example.com server block follows the same reliable pattern as auth.example.com, ensuring consistent proxy behavior across both services.

internal/bootstrap/service_bootstrap.go (2)

16-16: LGTM! Field addition follows existing pattern.

The oidcService field addition is consistent with other service fields in the struct.


100-132: LGTM! Well-structured graceful degradation pattern.

The OIDC service initialization follows a robust pattern:

  • Conditional initialization gated by Enabled flag
  • Sensible default for issuer (AppURL)
  • Non-fatal error handling with appropriate logging
  • Client sync attempts with success/failure logging

This pattern is consistent with the LDAP service initialization approach (lines 43-49), ensuring optional features don't break bootstrap.

validation/launch-chrome.sh (3)

8-20: LGTM! Appropriate service readiness check.

The 30-second timeout with polling is a good balance for waiting on nginx startup in a container environment.


40-51: LGTM! User-friendly fallback handling.

The manual instructions fallback is well-structured and exits gracefully with status 0, which is appropriate since Chrome is expected to be on the host, not in the container.


57-67: LGTM! Appropriate Chrome configuration for testing.

The host-resolver-rules approach is ideal for testing, avoiding system-wide DNS changes. The background execution and helpful output URLs provide a good user experience.

validation/launch-chrome-host.sh (3)

9-21: LGTM! Appropriate host-side Chrome detection.

The Chrome detection logic properly errors when Chrome isn't found, which is appropriate for a host-side script where Chrome installation is expected.


23-26: LGTM! Clear user feedback.

The pre-launch output provides helpful context about which Chrome binary is being used and the target URL.


36-39: LGTM! Helpful post-launch output.

The success message and test URLs provide clear next steps for the user.

validation/docker-compose.yml (4)

4-21: LGTM! Well-configured DNS service.

The dnsmasq configuration appropriately maps test domains to the nginx proxy IP and uses Google's public DNS as upstream resolvers. The NET_ADMIN capability is required for dnsmasq operations.


23-38: LGTM! Appropriate reverse proxy setup.

The nginx service correctly depends on all upstream services and properly exposes port 80 for host access. The comments clarify the DNS resolution strategy.


40-59: LGTM! Proper OIDC provider configuration.

The tinyauth service is well-configured with:

  • Experimental config file flag (consistent with loader_file.go changes)
  • Appropriate volume mounts for data persistence and config
  • Healthcheck for service readiness
  • Internal networking without host port exposure

85-90: LGTM! Appropriate network configuration.

The custom bridge network with a /16 subnet provides ample IP space for the test environment and allows static IP assignments for the services.

internal/model/oidc_client_model.go (1)

15-17: LGTM! Standard GORM table name implementation.

The TableName method correctly specifies the database table name following GORM conventions.

validation/README.md (1)

1-181: Excellent comprehensive documentation!

The validation README provides thorough documentation covering:

  • Step-by-step setup instructions
  • Multiple browser access methods (hosts file, Chrome flags, system DNS)
  • Platform-specific guidance (Linux, macOS, Windows)
  • Clear configuration details
  • Helpful notes about networking and cookie domains

The documentation will significantly aid developers and testers in working with the OIDC setup.

validation/oidc_whoami.py (1)

44-61: Single-session limitation: state and nonce are shared globally.

The state and nonce are generated once at module startup and shared across all requests. This means only one concurrent OIDC flow is supported. This is acceptable for a single-user test client but worth noting.

internal/controller/oidc_controller.go (2)

52-72: LGTM: Discovery document is well-formed.

The OpenID Connect discovery document includes all required fields and correctly advertises the supported capabilities including PKCE support.


171-274: Token handler structure is correct.

The token endpoint properly implements the authorization code flow: validates grant type, extracts credentials, verifies client, validates authorization code, and generates tokens. Error handling follows OAuth 2.0 error response format.

internal/service/oidc_service.go (2)

365-467: Client sync logic is well-implemented.

The SyncClientsFromConfig method properly handles both creating new clients and updating existing ones, with sensible defaults for missing configuration values and comprehensive error logging.


471-504: LGTM: Helper functions are clean and correct.

The helper functions (splitScopes, joinScopes, contains, getStringClaim) are simple, focused, and correctly implemented.

Comment thread internal/controller/oidc_controller.go
Comment thread internal/controller/oidc_controller.go
Comment thread internal/controller/oidc_controller.go Outdated
Comment thread internal/controller/oidc_controller.go
Comment thread internal/service/oidc_service.go
Comment thread internal/service/oidc_service.go Outdated
Comment thread validation/docker-compose.yml
Comment thread validation/launch-chrome-host.sh
Comment thread validation/oidc_whoami.py
Comment thread validation/oidc_whoami.py Outdated
…idation

The validateAccessToken method was only decoding the JWT payload without
verifying the signature, allowing attackers to forge tokens. This fix:

- Adds ValidateAccessToken method to OIDCService that properly verifies
  JWT signature using RSA public key
- Validates issuer, expiration, and required claims
- Updates controller to use the secure validation method
- Removes insecure manual JWT parsing code
PKCE was advertised in the discovery document but not actually implemented.
This commit adds full PKCE support:

- Store code_challenge and code_challenge_method in authorization code JWT
- Accept code_verifier parameter in token endpoint
- Validate code_verifier against stored code_challenge
- Support both S256 (SHA256) and plain code challenge methods
- PKCE validation is required when code_challenge is present

This prevents authorization code interception attacks by requiring
the client to prove possession of the code_verifier that was used
to generate the code_challenge.
Per OAuth 2.0 RFC 6749 §4.1.2.1, errors should NOT redirect to
unvalidated redirect_uri values. This fix:

- Returns JSON errors for failures before redirect_uri validation
  (missing parameters, invalid client)
- Only redirects to redirect_uri after it has been validated
  against registered client URIs
- Prevents open redirect attacks where malicious redirect_uri
  values could be used to redirect users to attacker-controlled sites
The discovery document only advertises client_secret_basic and
client_secret_post as supported authentication methods. Query parameters
are insecure because they are:
- Logged in access logs
- Stored in browser history
- Exposed in referrer headers

This fix removes the query parameter fallback, ensuring client secrets
are only accepted via:
- Authorization header (client_secret_basic)
- POST form body (client_secret_post)

This aligns the implementation with the advertised capabilities and
prevents client secret exposure through query strings.
User claims from ID tokens (username, name, email) were directly
interpolated into HTML without escaping, allowing XSS attacks if
malicious content was present in claims.

This fix:
- Imports html module for escaping
- Escapes all user-controlled data before rendering in HTML
- Escapes JSON output in pre tags as well
- Prevents execution of malicious scripts in browser
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
internal/service/oidc_service.go (5)

46-58: RSA keys are ephemeral: all tokens invalidate on restart.

This issue was previously flagged and remains unresolved. A new RSA key pair is generated on every service initialization, invalidating all existing access tokens, ID tokens, and authorization codes when the service restarts.


72-74: Client secrets are stored and compared in plaintext.

This issue was previously flagged and remains unresolved. Client secrets should be hashed (e.g., bcrypt) for storage and compared using constant-time comparison to prevent timing attacks.


142-153: Logic flaw: "openid" scope added even if not allowed for client.

This issue was previously flagged and remains unresolved. If openid is requested but not in the client's allowedScopes, it's still added to validScopes, bypassing the client's configured scope restrictions.


163-171: Remove dead code: authCode map is never used.

This issue was previously flagged and remains unresolved. The authCode map is constructed but only suppressed with _ = authCode. Remove this dead code.

Also applies to: 208-208


421-424: Remove unused pubKeyPEM variable.

This issue was previously flagged and remains unresolved. The PEM-encoded public key is computed but never used. Remove this dead code.

Also applies to: 446-446

🧹 Nitpick comments (3)
internal/service/oidc_service.go (3)

272-301: Consider constant-time comparison for PKCE validation.

Lines 288 and 293 use standard string equality (!=) to compare the code challenge/verifier. While PKCE is primarily designed to prevent MITM attacks rather than timing attacks, using subtle.ConstantTimeCompare is a defense-in-depth best practice to prevent potential timing side-channels.

🔎 Proposed fix using constant-time comparison
+import "crypto/subtle"
+
 func (oidc *OIDCService) ValidatePKCE(codeChallenge string, codeChallengeMethod string, codeVerifier string) error {
 	// ... existing checks ...
 	
 	switch codeChallengeMethod {
 	case "S256":
 		hash := sha256.Sum256([]byte(codeVerifier))
 		computedChallenge := base64.RawURLEncoding.EncodeToString(hash[:])
-		if computedChallenge != codeChallenge {
+		if subtle.ConstantTimeCompare([]byte(computedChallenge), []byte(codeChallenge)) != 1 {
 			return errors.New("code_verifier does not match code_challenge")
 		}
 	case "plain":
-		if codeVerifier != codeChallenge {
+		if subtle.ConstantTimeCompare([]byte(codeVerifier), []byte(codeChallenge)) != 1 {
 			return errors.New("code_verifier does not match code_challenge")
 		}

303-327: Add key ID (kid) to JWT headers for JWKS compatibility.

The generated JWTs (access tokens and ID tokens) do not explicitly set a kid (key ID) header, but the JWKS endpoint returns a key with kid: "default". Without the kid header in tokens, clients cannot reliably match tokens to the correct public key, especially when key rotation is implemented in the future.

🔎 Proposed fix: Set kid header when creating tokens
 func (oidc *OIDCService) GenerateAccessToken(userContext *config.UserContext, clientID string, scopes []string) (string, error) {
 	// ... existing claims setup ...
 	
-	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
+	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
+	token.Header["kid"] = "default"
 	accessToken, err := token.SignedString(oidc.privateKey)

Apply the same change to GenerateIDToken (line 406) and GenerateAuthorizationCode (line 202).

Also applies to: 383-413


431-435: Consider using minimum-octet encoding for RSA exponent in JWK.

The RSA exponent is manually encoded as a fixed 4-byte big-endian value. RFC 7518 Section 6.3.1 recommends using the minimum number of octets needed to represent the value. For the standard exponent 65537 (0x010001), this would be 3 bytes.

While most OIDC clients tolerate the extra leading zero byte, using a standards-compliant approach improves interoperability.

🔎 Proposed fix: Use math/big for minimum-octet encoding
 	// Extract modulus and exponent from public key
 	n := oidc.publicKey.N
 	e := oidc.publicKey.E
 
-	nBytes := n.Bytes()
-	eBytes := make([]byte, 4)
-	eBytes[0] = byte(e >> 24)
-	eBytes[1] = byte(e >> 16)
-	eBytes[2] = byte(e >> 8)
-	eBytes[3] = byte(e)
+	nBytes := n.Bytes()
+	eBig := big.NewInt(int64(e))
+	eBytes := eBig.Bytes()

This automatically uses the minimum number of bytes needed.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 020fcb9 and 672914c.

📒 Files selected for processing (2)
  • internal/controller/oidc_controller.go
  • internal/service/oidc_service.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/controller/oidc_controller.go
🧰 Additional context used
🧬 Code graph analysis (1)
internal/service/oidc_service.go (4)
internal/model/oidc_client_model.go (2)
  • OIDCClient (3-13)
  • OIDCClient (15-17)
internal/config/config.go (1)
  • UserContext (151-163)
internal/controller/context_controller.go (1)
  • Provider (40-44)
internal/utils/security_utils.go (1)
  • GetSecret (13-28)

Comment thread internal/service/oidc_service.go
The variable 'html' was being assigned to store HTML content, which
caused Python to treat 'html' as a local variable throughout the
function. This prevented access to the 'html' module (imported at
the top) within f-strings that referenced html.escape().

Renamed the HTML content variable to 'html_content' to avoid the
naming conflict with the html module.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
validation/oidc_whoami.py (1)

73-73: Add timeout to JWKS fetch request (duplicate concern).

The requests.get() call lacks a timeout parameter, which could cause the client to hang if the JWKS endpoint becomes unresponsive. This issue was previously flagged.

🧹 Nitpick comments (2)
validation/oidc_whoami.py (1)

104-277: LGTM: Well-implemented OIDC callback handler.

The request handler correctly implements the OIDC flow with proper security measures:

  • State validation to prevent CSRF attacks (line 223)
  • PKCE code verifier for token exchange (line 235)
  • Claims validation with issuer and audience checks (lines 243-246)
  • HttpOnly cookie flag prevents JavaScript access (line 267)
  • HTML escaping prevents XSS (lines 173-175, 179)
  • Cookie expiration aligned with token expiry (lines 261-262)
💡 Optional: Consider additional cookie security flags

For enhanced security (though less critical in a test client), consider adding SameSite and Secure flags to the cookie:

-        self.send_header("Set-Cookie", f"id_token={token['id_token']}; Path=/; Max-Age={max_age}; HttpOnly")
+        self.send_header("Set-Cookie", f"id_token={token['id_token']}; Path=/; Max-Age={max_age}; HttpOnly; SameSite=Lax")

Note: Secure flag would require HTTPS, which may not be practical for local testing.

internal/model/oidc_key_model.go (1)

6-7: Consider adding GORM auto-timestamp tags.

The CreatedAt and UpdatedAt fields would benefit from GORM's automatic timestamp management to ensure consistency.

🔎 Proposed enhancement
-	CreatedAt  int64  `gorm:"column:created_at"`
-	UpdatedAt  int64  `gorm:"column:updated_at"`
+	CreatedAt  int64  `gorm:"column:created_at;autoCreateTime"`
+	UpdatedAt  int64  `gorm:"column:updated_at;autoUpdateTime"`
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b5799a and cd068d1.

📒 Files selected for processing (5)
  • internal/assets/migrations/000005_oidc_keys.down.sql
  • internal/assets/migrations/000005_oidc_keys.up.sql
  • internal/model/oidc_key_model.go
  • internal/service/oidc_service.go
  • validation/oidc_whoami.py
✅ Files skipped from review due to trivial changes (1)
  • internal/assets/migrations/000005_oidc_keys.up.sql
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-04-30T15:03:09.620Z
Learnt from: steveiliop56
Repo: steveiliop56/tinyauth PR: 122
File: internal/handlers/handlers.go:75-81
Timestamp: 2025-04-30T15:03:09.620Z
Learning: Values from identity providers and user inputs should be sanitized before being set as HTTP headers in TinyAuth to prevent CRLF injection attacks, even if they come from seemingly trusted sources like OAuth providers.

Applied to files:

  • validation/oidc_whoami.py
🧬 Code graph analysis (2)
internal/model/oidc_key_model.go (1)
internal/model/session_model.go (1)
  • UUID (3-13)
internal/service/oidc_service.go (5)
internal/model/oidc_key_model.go (2)
  • OIDCKey (3-8)
  • OIDCKey (10-12)
internal/model/oidc_client_model.go (2)
  • OIDCClient (3-13)
  • OIDCClient (15-17)
internal/config/config.go (3)
  • UserContext (151-163)
  • Claims (100-106)
  • OIDCClientConfig (80-88)
internal/controller/context_controller.go (1)
  • Provider (40-44)
internal/utils/security_utils.go (1)
  • GetSecret (13-28)
🪛 Ruff (0.14.10)
validation/oidc_whoami.py

43-43: Avoid specifying long messages outside the exception class

(TRY003)


73-73: Probable use of requests call without timeout

(S113)


100-100: Do not catch blind exception: Exception

(BLE001)


294-294: Do not catch blind exception: Exception

(BLE001)


298-298: Possible binding to all interfaces

(S104)

🔇 Additional comments (9)
validation/oidc_whoami.py (3)

18-24: LGTM: Clean environment-based configuration.

The configuration setup correctly loads OIDC parameters from environment variables and appropriately marks CLIENT_SECRET as optional to support public clients.


45-63: LGTM: Proper PKCE and OIDC flow setup.

The authorization flow correctly implements PKCE with S256 challenge method and securely generates state and nonce parameters using secrets.token_urlsafe.


278-298: LGTM: Server startup is appropriate for containerized testing.

The server startup logic is well-suited for the validation environment:

  • Clear usage instructions for manual testing
  • Graceful handling of browser auto-open failures (lines 292-296)
  • Binding to 0.0.0.0:8765 is necessary and correct for Docker containerization, allowing the test client to be accessed from outside the container via the nginx reverse proxy

Note: The static analysis warning about binding to all interfaces (S104) is a false positive in this validation/testing context.

internal/assets/migrations/000005_oidc_keys.down.sql (1)

1-1: LGTM!

The down-migration correctly drops the oidc_keys table with proper IF EXISTS guard.

internal/service/oidc_service.go (5)

331-360: LGTM!

The PKCE validation correctly implements both S256 (SHA256 + base64url) and plain methods per RFC 7636.


362-472: LGTM!

The token generation and validation methods correctly implement OAuth 2.0 and OIDC standards, with proper expiry handling, issuer validation, and nonce support.


485-509: LGTM!

The JWK generation correctly extracts the RSA modulus and exponent, encodes them as base64url, and constructs a proper JWK structure for RS256.


523-625: LGTM!

The client sync logic correctly handles defaults, JSON serialization for array fields, and performs proper upsert operations while preserving creation timestamps.


627-662: LGTM!

The helper functions are well-implemented with proper error handling and defensive programming (whitespace trimming, empty checks).

Comment thread internal/service/oidc_service.go
Comment thread internal/service/oidc_service.go Outdated
Authorization codes were implemented as stateless JWTs with no tracking,
allowing the same code to be exchanged for tokens multiple times. This
violates OAuth 2.0 RFC 6749 Section 4.1.2 which mandates that authorization
codes MUST be single-use.

This change:
- Adds oidc_authorization_codes table to track code usage
- Stores authorization codes in database when generated
- Validates code exists and hasn't been used before exchange
- Marks code as used immediately after validation
- Prevents replay attacks where intercepted codes could be reused

Security impact:
- Prevents attackers from reusing intercepted authorization codes
- Ensures compliance with OAuth 2.0 security requirements
- Adds database-backed single-use enforcement
Security improvements:

1. Client secret hashing:
   - Replace plaintext comparison with bcrypt.CompareHashAndPassword
   - Provides constant-time comparison to prevent timing attacks
   - Hash secrets with bcrypt before storing in database
   - Update SyncClientsFromConfig to hash incoming plaintext secrets

2. Deterministic RSA key loading:
   - Load most recently created key using ORDER BY created_at DESC
   - Add warning if multiple keys detected in database
   - Ensures consistent key selection on startup

3. Optional RSA key encryption:
   - Encrypt private keys with AES-256-GCM when OIDC_RSA_MASTER_KEY is set
   - Master key derived via SHA256 from environment variable
   - Backward compatible: stores plaintext if no master key set
   - Automatic detection of encrypted vs plaintext on load

All changes maintain backward compatibility with existing deployments.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
internal/service/oidc_service.go (1)

316-327: Logic flaw: "openid" scope bypass still present.

The special case for "openid" (lines 316-327) still allows adding "openid" to validScopes even when it's not in the client's allowedScopes. This bypasses the client's configured scope restrictions. If "openid" should always be available, add it to the default scopes in SyncClientsFromConfig (line 700); otherwise, enforce the client's allowed scopes consistently.

🔎 Proposed fix: Remove special case
-	// Always include "openid" if it was requested
-	hasOpenID := false
-	for _, scope := range validScopes {
-		if scope == "openid" {
-			hasOpenID = true
-			break
-		}
-	}
-
-	if !hasOpenID && contains(requestedScopesList, "openid") {
-		validScopes = append(validScopes, "openid")
-	}
-
 	return validScopes, nil

If "openid" must always be allowed, add it to default scopes at line 700 instead.

Based on learnings, past reviews flagged this same issue.

🧹 Nitpick comments (2)
internal/model/oidc_authorization_code_model.go (1)

7-7: Consider clarifying the GORM default tag usage.

The default:false tag in the GORM struct doesn't set the SQL DEFAULT constraint; that's defined in the migration file (line 5 of 000006_oidc_authorization_codes.up.sql). The GORM tag primarily affects behavior when creating records programmatically. Since the migration already sets DEFAULT 0, this works correctly, but the tag could be removed to avoid confusion about where defaults are actually enforced.

🔎 Optional: Remove redundant default tag
-	Used        bool   `gorm:"column:used;default:false"`
+	Used        bool   `gorm:"column:used"`

This makes it clearer that the SQL default is the source of truth.

internal/service/oidc_service.go (1)

128-130: Consider enforcing minimum key length for production security.

The code warns when OIDC_RSA_MASTER_KEY is shorter than 32 bytes but continues execution. A weak master key significantly reduces the security of encrypted RSA private keys. For production deployments, consider rejecting keys that don't meet minimum entropy requirements.

🔎 Proposed fix to enforce minimum key length
 		if len(oidc.masterKey) < 32 {
-			log.Warn().Msg("OIDC_RSA_MASTER_KEY is shorter than 32 bytes, consider using a longer key for better security")
+			return fmt.Errorf("OIDC_RSA_MASTER_KEY must be at least 32 bytes for production use (got %d bytes)", len(oidc.masterKey))
 		}

Alternatively, keep the warning for flexibility but document the security implications clearly.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd068d1 and ca74534.

📒 Files selected for processing (4)
  • internal/assets/migrations/000006_oidc_authorization_codes.down.sql
  • internal/assets/migrations/000006_oidc_authorization_codes.up.sql
  • internal/model/oidc_authorization_code_model.go
  • internal/service/oidc_service.go
🔇 Additional comments (8)
internal/assets/migrations/000006_oidc_authorization_codes.down.sql (1)

1-2: LGTM: Down migration is correct.

The migration properly drops the index before the table and uses IF EXISTS for safety.

internal/assets/migrations/000006_oidc_authorization_codes.up.sql (1)

1-10: LGTM: Authorization code table schema is well-designed.

The table structure supports single-use enforcement with the used flag and includes an index on expires_at for efficient cleanup queries. The schema aligns with the model defined in internal/model/oidc_authorization_code_model.go.

internal/service/oidc_service.go (6)

240-248: LGTM: Client secret verification now uses bcrypt.

This properly addresses the previous security concern about plaintext comparison. The use of bcrypt.CompareHashAndPassword provides constant-time comparison and proper cryptographic hashing.


332-389: LGTM: Authorization code generation now includes replay protection.

The database-backed storage (lines 338-349) properly addresses the previous critical security issue. Authorization codes are now tracked in the database, enabling single-use enforcement in ValidateAuthorizationCode. The cleanup on JWT signing failure (lines 383-385) is good defensive programming.


391-484: LGTM: Authorization code validation implements proper replay protection.

The implementation correctly:

  • Verifies the code exists in the database (lines 418-426)
  • Checks the Used flag to prevent replay attacks (lines 428-431)
  • Validates expiry (lines 433-436)
  • Atomically marks the code as used (lines 456-460)

This fully addresses the previous critical security concern about replay attacks. The dual validation of client_id and redirect_uri (both from JWT and DB) is defensive programming and doesn't hurt.


486-515: LGTM: PKCE validation correctly implements RFC 7636.

The implementation properly supports both S256 (SHA256 hash) and plain code challenge methods, with correct base64url encoding for S256.


728-733: LGTM: Client secrets are now hashed with bcrypt before storage.

This properly addresses the previous security concern about storing plaintext secrets. The use of bcrypt.GenerateFromPassword with DefaultCost provides secure one-way hashing.


777-812: LGTM: Helper functions are straightforward and correct.

The utility functions provide clean abstractions for scope manipulation, slice membership checks, and safe JWT claim extraction.

Comment thread internal/service/oidc_service.go Outdated
Security improvements:

1. HKDF key derivation:
   - Replace raw sha256.Sum256() with proper HKDF (HMAC-based KDF)
   - Uses domain-separated label 'oidc-aes-256-key-v1' for key derivation
   - Applied to both encryptPrivateKey and decryptPrivateKey
   - Provides better security properties than raw hash

2. Scope validation fix:
   - Only add 'openid' scope if it's both requested AND in client's
     allowedScopes
   - Prevents bypassing client scope restrictions
   - Respects configured allowedScopes

Both changes improve security posture while maintaining backward
compatibility.
The special case for adding 'openid' scope was redundant and could
potentially bypass client scope restrictions. The main loop already
correctly adds 'openid' to validScopes if it's in both requestedScopes
and allowedScopes.

Since 'openid' is already in the default scopes during client
configuration (SyncClientsFromConfig), it will be available for
clients that don't explicitly configure scopes. Clients can include
or exclude 'openid' in their allowedScopes as needed.

This ensures consistent enforcement of client scope restrictions
with no special-case bypasses.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/service/oidc_service.go (2)

483-512: Consider deprecating PKCE "plain" method.

While the PKCE implementation is correct, the "plain" method (lines 502-506) is discouraged by RFC 7636 which recommends S256 for security. Consider logging a warning when plain is used or only supporting S256.

🔎 Optional: Add warning for plain PKCE
 	case "plain":
+		log.Warn().Msg("PKCE 'plain' method is deprecated and insecure, consider using 'S256'")
 		// Direct comparison
 		if codeVerifier != codeChallenge {
 			return errors.New("code_verifier does not match code_challenge")

638-645: Consider adding dynamic key identifiers for future key rotation.

Line 641: The key ID is hardcoded as "default". For proper key rotation support, consider:

  • Using a key fingerprint or database ID as the kid
  • Adding kid to JWT headers when signing tokens
  • Supporting multiple active keys in JWKS

This isn't critical for the current implementation but will be necessary if key rotation is implemented later.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca74534 and 5ec9989.

📒 Files selected for processing (1)
  • internal/service/oidc_service.go
🧰 Additional context used
🧬 Code graph analysis (1)
internal/service/oidc_service.go (4)
internal/model/oidc_key_model.go (2)
  • OIDCKey (3-8)
  • OIDCKey (10-12)
internal/model/oidc_client_model.go (2)
  • OIDCClient (3-13)
  • OIDCClient (15-17)
internal/config/config.go (2)
  • UserContext (151-163)
  • OIDCClientConfig (80-88)
internal/model/oidc_authorization_code_model.go (2)
  • OIDCAuthorizationCode (3-10)
  • OIDCAuthorizationCode (12-14)
🔇 Additional comments (7)
internal/service/oidc_service.go (7)

54-131: LGTM: Strong cryptographic implementation.

The HKDF-based key derivation with domain separation and AES-GCM encryption provides robust protection for private keys. The backward compatibility handling for plaintext keys is well-designed.


133-236: LGTM: Robust key initialization with proper encryption.

The Init method correctly handles RSA key loading/generation with encryption, deterministic key selection when multiple keys exist, and comprehensive error handling. The PKCS1/PKCS8 fallback parsing increases compatibility.


238-327: LGTM: Client validation methods are secure and correct.

The client validation methods properly use bcrypt for secret comparison, correctly validate redirect URIs/grant types/response types, and enforce scope restrictions without bypassing client configurations.


388-481: LGTM: Robust authorization code validation with replay protection.

The authorization code validation correctly implements replay protection by tracking usage in the database, validates JWT signatures and claims, and ensures client_id and redirect_uri consistency.


663-772: LGTM: Client synchronization correctly handles configuration-driven updates.

The SyncClientsFromConfig method properly handles client lifecycle with bcrypt-hashed secrets, JSON serialization of arrays, and appropriate defaults. The approach of always updating secrets from config (line 726-740) makes the configuration file the authoritative source, which is a reasonable design choice.


774-809: LGTM: Helper functions are correct and well-implemented.

The helper functions properly handle edge cases (empty strings, whitespace, type safety) and provide clean abstractions for common operations.


329-386: No changes required. The OIDCAuthorizationCode model does not have a DeletedAt field, so it does not use GORM soft delete. The Delete(&authCodeRecord) call on line 381 performs a hard delete, permanently removing the record as intended. The replay protection mechanism works correctly.

Likely an incorrect or invalid review comment.

Comment thread internal/service/oidc_service.go
Comment thread internal/service/oidc_service.go
Access tokens include an 'aud' (audience) claim set to the client ID,
but this was never validated during token validation. This allowed
tokens issued for one client to be used by another client, violating
the OAuth 2.0 security model.

Changes:
- Add ValidateAccessTokenForClient method that validates audience
  if expectedClientID is provided
- Update ValidateAccessToken to call ValidateAccessTokenForClient
  (backward compatible, no audience check if not specified)
- Update userinfo endpoint to accept optional client_id parameter
  and validate token audience matches it

Security impact:
- Prevents token reuse across different clients
- Ensures tokens are scoped to specific clients as intended
- Prevents attackers from using tokens issued for one client to
  access resources protected by another client
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
internal/controller/oidc_controller.go (1)

480-489: Remove unused method to reduce code clutter.

The validateAccessToken method is not called anywhere in the controller. The userinfoHandler directly uses controller.oidc.ValidateAccessTokenForClient instead (line 353). Consider removing this unused helper method.

🔎 Proposed cleanup
-// validateAccessToken validates an access token and extracts user context.
-// Verifies the JWT signature using the OIDC service's public key, checks the
-// issuer, and validates expiration. Returns the user context if valid, or an
-// error if validation fails.
-func (controller *OIDCController) validateAccessToken(accessToken string) (*config.UserContext, error) {
-	// Validate the JWT token using the OIDC service's public key
-	// This properly verifies the signature, issuer, and expiration
-	// Note: This method does not validate audience - use ValidateAccessTokenForClient for that
-	return controller.oidc.ValidateAccessToken(accessToken)
-}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ec9989 and 3f2f813.

📒 Files selected for processing (3)
  • .review_trigger
  • internal/controller/oidc_controller.go
  • internal/service/oidc_service.go
✅ Files skipped from review due to trivial changes (1)
  • .review_trigger
🧰 Additional context used
🧬 Code graph analysis (2)
internal/controller/oidc_controller.go (4)
internal/service/oidc_service.go (1)
  • OIDCService (41-46)
internal/service/auth_service.go (1)
  • AuthService (39-46)
internal/utils/app_utils.go (1)
  • GetContext (70-84)
internal/config/config.go (1)
  • UserContext (151-163)
internal/service/oidc_service.go (6)
internal/model/oidc_key_model.go (2)
  • OIDCKey (3-8)
  • OIDCKey (10-12)
internal/model/oidc_client_model.go (2)
  • OIDCClient (3-13)
  • OIDCClient (15-17)
internal/config/config.go (3)
  • UserContext (151-163)
  • Claims (100-106)
  • OIDCClientConfig (80-88)
internal/model/oidc_authorization_code_model.go (2)
  • OIDCAuthorizationCode (3-10)
  • OIDCAuthorizationCode (12-14)
internal/controller/context_controller.go (1)
  • Provider (40-44)
internal/utils/security_utils.go (1)
  • GetSecret (13-28)
🔇 Additional comments (17)
internal/controller/oidc_controller.go (7)

1-61: LGTM! Clean controller structure and comprehensive OIDC endpoint setup.

The controller is well-structured with proper dependency injection and all standard OIDC endpoints (discovery, authorize, token, userinfo, jwks) correctly registered.


63-87: LGTM! Compliant OIDC discovery document.

The discovery handler correctly implements RFC 8414 with all required metadata and accurately advertises supported features.


89-204: LGTM! Authorization endpoint properly implements OAuth 2.0 authorization code flow with security fixes applied.

The handler correctly validates all parameters before any redirects (preventing open redirect), includes PKCE support, preserves authentication state through login redirects, and enforces TOTP requirements.


206-329: LGTM! Token endpoint correctly implements authorization code exchange with PKCE validation.

The handler properly validates all inputs, verifies client credentials via secure methods, validates PKCE code_verifier, and issues tokens following OIDC specifications.


331-372: LGTM! Userinfo endpoint with optional audience validation for backward compatibility.

The handler correctly validates access tokens with optional audience checking when client_id is provided. The optional nature maintains backward compatibility while allowing clients to opt into stronger validation by supplying client_id in the request.


374-388: LGTM! JWKS endpoint correctly delegates to service layer.

Clean implementation with appropriate error handling.


390-478: LGTM! Helper functions implement secure credential handling.

The credential extraction methods correctly support only client_secret_basic and client_secret_post authentication methods, properly excluding query parameters for secrets to prevent exposure in logs and referrer headers.

internal/service/oidc_service.go (10)

1-52: LGTM! Well-structured service foundation.

Clean type definitions with appropriate dependencies including cryptographic libraries (bcrypt, hkdf) for secure key and secret management.


54-132: LGTM! Secure key encryption with proper KDF.

The encryption implementation correctly uses HKDF for key derivation (addressing previous security concerns) with domain separation, and AES-256-GCM for authenticated encryption. Backward compatibility for plaintext keys is appropriately maintained.


134-237: LGTM! Robust key initialization with security improvements applied.

The initialization correctly addresses previous concerns: deterministic key selection via ORDER BY created_at DESC, warnings for multiple keys, and optional master key encryption with length validation.


239-328: LGTM! Client validation methods with security fixes applied.

Client secret verification now uses bcrypt for constant-time comparison (preventing timing attacks), and scope validation correctly enforces client-configured scopes without special-case bypasses.


330-482: LGTM! Authorization code flow with comprehensive replay protection.

Critical security improvements have been applied: authorization codes are now tracked in the database with single-use enforcement, PKCE parameters are properly stored and validated, and all security checks (expiration, client/redirect URI matching) are correctly implemented.


484-513: LGTM! PKCE validation correctly implements RFC 7636.

The method properly supports both S256 and plain challenge methods with correct cryptographic operations.


515-608: LGTM! Access token validation with audience checking for cross-client protection.

The new ValidateAccessTokenForClient method correctly implements optional audience validation to prevent token reuse across clients. The design maintains backward compatibility by making audience checking opt-in via the expectedClientID parameter.


610-663: LGTM! ID token generation and RFC 7517-compliant JWKS.

ID tokens include all required OIDC claims, and the JWKS encoding correctly uses minimal-octet representation for the RSA exponent (addressing previous RFC compliance concerns).


665-785: LGTM! Client synchronization with secure secret hashing.

The client sync correctly hashes secrets with bcrypt before storage (addressing previous security concerns), applies sensible defaults, and properly handles both new client creation and existing client updates.


787-822: LGTM! Clean utility functions.

The helper methods provide straightforward string and slice operations with appropriate edge case handling.

@shreknel
Copy link
Copy Markdown
Author

As far as I can tell, all relevant actions have been taken to add OIDC provider capability in a secure manner to tinyauth. Even if this particular approach isn't deemed suitable, I hope the capability makes it into tinyauth, as this would be a killer feature in such a small package.

@steveiliop56
Copy link
Copy Markdown
Member

@shreknel thank you so much for this pull request. I will have to go through the spec myself to ensure that everything is implemented correctly before merging. Would you mind if I took over the pull request? I immediately see some things I would like to do differently and I believe that doing it through conversations would take a significant amount of time.

@shreknel
Copy link
Copy Markdown
Author

@steveiliop56 I don't mind at all. Cheers!

@steveiliop56
Copy link
Copy Markdown
Member

Alright, closing in favor of #550.

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