VYPR
Moderate severityNVD Advisory· Published Mar 12, 2026· Updated Mar 12, 2026

Tinyauth's OIDC authorization codes are not bound to client on token exchange

CVE-2026-32245

Description

Tinyauth is an authentication and authorization server. Prior to 5.0.3, the OIDC token endpoint does not verify that the client exchanging an authorization code is the same client the code was issued to. A malicious OIDC client operator can exchange another client's authorization code using their own client credentials, obtaining tokens for users who never authorized their application. This violates RFC 6749 Section 4.1.3. This vulnerability is fixed in 5.0.3.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2026-32245 is an OIDC authorization code substitution vulnerability in Tinyauth before 5.0.3, allowing a malicious client to exchange another client's authorization code.

Vulnerability

Overview

In Tinyauth versions prior to 5.0.3, the OIDC token endpoint fails to verify that the client exchanging an authorization code is the same client the code was issued to [1][3]. When an authorization code is created, the system correctly stores the ClientID alongside the code hash in the database. However, during token exchange, the handler retrieves the code entry and validates the redirect_uri, but never compares entry.ClientID against the requesting client's ID (creds.ClientID) [3]. The code proceeds directly to token generation, bypassing the intended client binding check.

Attack

Vector and Exploitation

An attacker must control an OIDC client registered with the same Tinyauth instance and have at least one overlapping redirect URI with the victim client, or be able to intercept the authorization code from the victim client's redirect (e.g., via referrer leak, browser history, or log access) [3]. The attacker initiates a normal user login, then authorizes with the victim client A to obtain an authorization code. Instead of using client A's credentials, the attacker submits the stolen authorization code to the token endpoint using their own client B's credentials, successfully exchanging the code for tokens belonging to the victim user [3].

Impact

This violates RFC 6749 Section 4.1.3, which requires that the authorization code be bound to the client to which it was issued [1][3]. A successful exploit allows a malicious OIDC client operator to obtain access and ID tokens for users who never authorized their application, potentially gaining unauthorized access to resources protected by the victim client.

Mitigation

The vulnerability is fixed in Tinyauth version 5.0.3 [1][2]. The fix adds the missing client ID validation during authorization code exchange, ensuring that the authorization code can only be exchanged by the client it was originally issued to [2]. Users should upgrade to 5.0.3 immediately; no workaround is available.

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/steveiliop56/tinyauthGo
< 1.0.1-20260311144920-9eb2d33064b71.0.1-20260311144920-9eb2d33064b7

Affected products

2

Patches

1
b2a1bfb1f532

fix: validate client id on oidc token endpoint

https://github.com/steveiliop56/tinyauthStavrosMar 11, 2026via ghsa
5 files changed · +27 54
  • internal/controller/oidc_controller.go+8 1 modified
    @@ -270,7 +270,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
     
     	switch req.GrantType {
     	case "authorization_code":
    -		entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code))
    +		entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code), client.ClientID)
     		if err != nil {
     			if errors.Is(err, service.ErrCodeNotFound) {
     				tlog.App.Warn().Msg("Code not found")
    @@ -286,6 +286,13 @@ func (controller *OIDCController) Token(c *gin.Context) {
     				})
     				return
     			}
    +			if errors.Is(err, service.ErrInvalidClient) {
    +				tlog.App.Warn().Msg("Invalid client ID")
    +				c.JSON(400, gin.H{
    +					"error": "invalid_client",
    +				})
    +				return
    +			}
     			tlog.App.Warn().Err(err).Msg("Failed to get OIDC code entry")
     			c.JSON(400, gin.H{
     				"error": "server_error",
    
  • internal/controller/proxy_controller.go+0 5 modified
    @@ -185,11 +185,6 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
     
     	tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
     
    -	if userContext.IsBasicAuth && userContext.TotpEnabled {
    -		tlog.App.Debug().Msg("User has TOTP enabled, denying basic auth access")
    -		userContext.IsLoggedIn = false
    -	}
    -
     	if userContext.IsLoggedIn {
     		userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
     
    
  • internal/controller/proxy_controller_test.go+9 46 modified
    @@ -59,6 +59,11 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
     				Username: "testuser",
     				Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
     			},
    +			{
    +				Username:   "totpuser",
    +				Password:   "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.",
    +				TotpSecret: "foo",
    +			},
     		},
     		OauthWhitelist:     []string{},
     		SessionExpiry:      3600,
    @@ -79,9 +84,11 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
     	return router, recorder, authService
     }
     
    +// TODO: Needs tests for context middleware
    +
     func TestProxyHandler(t *testing.T) {
     	// Setup
    -	router, recorder, authService := setupProxyController(t, nil)
    +	router, recorder, _ := setupProxyController(t, nil)
     
     	// Test invalid proxy
     	req := httptest.NewRequest("GET", "/api/auth/invalidproxy", nil)
    @@ -144,21 +151,6 @@ func TestProxyHandler(t *testing.T) {
     	assert.Equal(t, 401, recorder.Code)
     
     	// Test logged in user
    -	c := gin.CreateTestContextOnly(recorder, router)
    -
    -	err := authService.CreateSessionCookie(c, &repository.Session{
    -		Username:    "testuser",
    -		Name:        "testuser",
    -		Email:       "testuser@example.com",
    -		Provider:    "local",
    -		TotpPending: false,
    -		OAuthGroups: "",
    -	})
    -
    -	assert.NilError(t, err)
    -
    -	cookie := c.Writer.Header().Get("Set-Cookie")
    -
     	router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{
     		func(c *gin.Context) {
     			c.Set("context", &config.UserContext{
    @@ -177,44 +169,15 @@ func TestProxyHandler(t *testing.T) {
     	})
     
     	req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
    -	req.Header.Set("Cookie", cookie)
     	req.Header.Set("X-Forwarded-Proto", "https")
     	req.Header.Set("X-Forwarded-Host", "example.com")
     	req.Header.Set("X-Forwarded-Uri", "/somepath")
     	req.Header.Set("Accept", "text/html")
    -	router.ServeHTTP(recorder, req)
     
    +	router.ServeHTTP(recorder, req)
     	assert.Equal(t, 200, recorder.Code)
     
     	assert.Equal(t, "testuser", recorder.Header().Get("Remote-User"))
     	assert.Equal(t, "testuser", recorder.Header().Get("Remote-Name"))
     	assert.Equal(t, "testuser@example.com", recorder.Header().Get("Remote-Email"))
    -
    -	// Ensure basic auth is disabled for TOTP enabled users
    -	router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{
    -		func(c *gin.Context) {
    -			c.Set("context", &config.UserContext{
    -				Username:    "testuser",
    -				Name:        "testuser",
    -				Email:       "testuser@example.com",
    -				IsLoggedIn:  true,
    -				IsBasicAuth: true,
    -				OAuth:       false,
    -				Provider:    "local",
    -				TotpPending: false,
    -				OAuthGroups: "",
    -				TotpEnabled: true,
    -			})
    -			c.Next()
    -		},
    -	})
    -
    -	req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
    -	req.Header.Set("X-Forwarded-Proto", "https")
    -	req.Header.Set("X-Forwarded-Host", "example.com")
    -	req.Header.Set("X-Forwarded-Uri", "/somepath")
    -	req.SetBasicAuth("testuser", "test")
    -	router.ServeHTTP(recorder, req)
    -
    -	assert.Equal(t, 401, recorder.Code)
     }
    
  • internal/middleware/context_middleware.go+5 1 modified
    @@ -182,13 +182,17 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
     
     			user := m.auth.GetLocalUser(basic.Username)
     
    +			if user.TotpSecret != "" {
    +				tlog.App.Debug().Msg("User with TOTP not allowed to login via basic auth")
    +				return
    +			}
    +
     			c.Set("context", &config.UserContext{
     				Username:    user.Username,
     				Name:        utils.Capitalize(user.Username),
     				Email:       utils.CompileUserEmail(user.Username, m.config.CookieDomain),
     				Provider:    "local",
     				IsLoggedIn:  true,
    -				TotpEnabled: user.TotpSecret != "",
     				IsBasicAuth: true,
     			})
     			c.Next()
    
  • internal/service/oidc_service.go+5 1 modified
    @@ -352,7 +352,7 @@ func (service *OIDCService) ValidateGrantType(grantType string) error {
     	return nil
     }
     
    -func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string) (repository.OidcCode, error) {
    +func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string, clientId string) (repository.OidcCode, error) {
     	oidcCode, err := service.queries.GetOidcCode(c, codeHash)
     
     	if err != nil {
    @@ -374,6 +374,10 @@ func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string) (repos
     		return repository.OidcCode{}, ErrCodeExpired
     	}
     
    +	if oidcCode.ClientID != clientId {
    +		return repository.OidcCode{}, ErrInvalidClient
    +	}
    +
     	return oidcCode, nil
     }
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.