From a47ba059acd7303b4ebfaa9c7c4191e0517ce5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Guldmund?= Date: Thu, 9 Jul 2026 22:09:35 +0200 Subject: [PATCH 1/5] feat: add SkipAuthorization option for OIDC clients --- internal/controller/oidc_controller.go | 31 ++++++++++++++++++++++++++ internal/model/config.go | 5 +++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index 0c5ac918..4056665a 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -206,6 +206,37 @@ func (controller *OIDCController) authorize(c *gin.Context) { ticket := controller.oidc.CreateAuthorizeRequestTicket(*req) + if client.SkipAuthorization && userContext != nil && userContext.Authenticated { + controller.oidc.DeleteAuthorizeRequestTicket(ticket) + sub := controller.oidc.CreateSub(*userContext, req.ClientID) + if err := controller.oidc.DeleteOldSession(c, sub); err != nil { + controller.authorizeError(c, authorizeErrorParams{ + err: err, + reason: "Failed to delete old sessions", + reasonPublic: "Failed to delete old sessions", + callback: req.RedirectURI, + callbackError: "server_error", + state: req.State, + }) + return + } + code := controller.oidc.CreateCode(*req, *userContext) + queries, err := query.Values(AuthorizeCallback{Code: code, State: req.State}) + if err != nil { + controller.authorizeError(c, authorizeErrorParams{ + err: err, + reason: "Failed to build query", + reasonPublic: "Failed to build query", + callback: req.RedirectURI, + callbackError: "server_error", + state: req.State, + }) + return + } + c.Redirect(http.StatusFound, fmt.Sprintf("%s?%s", req.RedirectURI, queries.Encode())) + return + } + values := AuthorizeScreenParams{ LoginFor: FrontendLoginForOIDC, OIDCTicket: ticket, diff --git a/internal/model/config.go b/internal/model/config.go index 97c92510..fc4a9f4d 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -271,8 +271,9 @@ type OIDCClientConfig struct { ClientID string `description:"OIDC client ID." yaml:"clientId,omitempty"` ClientSecret string `description:"OIDC client secret." yaml:"clientSecret,omitempty"` ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile,omitempty"` - TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris,omitempty"` - Name string `description:"Client name in UI." yaml:"name,omitempty"` + TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris,omitempty"` + Name string `description:"Client name in UI." yaml:"name,omitempty"` + SkipAuthorization bool `description:"Skip the authorization consent screen for this client." yaml:"skipAuthorization,omitempty"` } type ACLsConfig struct { From 70ede6b16eb566424f02d7c3a6b2a28cf5e362e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Guldmund?= Date: Thu, 9 Jul 2026 22:20:48 +0200 Subject: [PATCH 2/5] fix: remove accidental alignment padding from OIDCClientConfig --- internal/model/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/model/config.go b/internal/model/config.go index fc4a9f4d..64690fd2 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -271,9 +271,9 @@ type OIDCClientConfig struct { ClientID string `description:"OIDC client ID." yaml:"clientId,omitempty"` ClientSecret string `description:"OIDC client secret." yaml:"clientSecret,omitempty"` ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile,omitempty"` - TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris,omitempty"` - Name string `description:"Client name in UI." yaml:"name,omitempty"` - SkipAuthorization bool `description:"Skip the authorization consent screen for this client." yaml:"skipAuthorization,omitempty"` + TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris,omitempty"` + Name string `description:"Client name in UI." yaml:"name,omitempty"` + SkipAuthorization bool `description:"Skip the authorization consent screen for this client." yaml:"skipAuthorization,omitempty"` } type ACLsConfig struct { From d7b7396d829cced4638b3ba1b12e16a68c83fbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Guldmund?= Date: Thu, 9 Jul 2026 22:23:48 +0200 Subject: [PATCH 3/5] fix: skip authorization only after prompt and max_age checks --- internal/controller/oidc_controller.go | 62 +++++++++++++------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index 4056665a..771c50c9 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -206,37 +206,6 @@ func (controller *OIDCController) authorize(c *gin.Context) { ticket := controller.oidc.CreateAuthorizeRequestTicket(*req) - if client.SkipAuthorization && userContext != nil && userContext.Authenticated { - controller.oidc.DeleteAuthorizeRequestTicket(ticket) - sub := controller.oidc.CreateSub(*userContext, req.ClientID) - if err := controller.oidc.DeleteOldSession(c, sub); err != nil { - controller.authorizeError(c, authorizeErrorParams{ - err: err, - reason: "Failed to delete old sessions", - reasonPublic: "Failed to delete old sessions", - callback: req.RedirectURI, - callbackError: "server_error", - state: req.State, - }) - return - } - code := controller.oidc.CreateCode(*req, *userContext) - queries, err := query.Values(AuthorizeCallback{Code: code, State: req.State}) - if err != nil { - controller.authorizeError(c, authorizeErrorParams{ - err: err, - reason: "Failed to build query", - reasonPublic: "Failed to build query", - callback: req.RedirectURI, - callbackError: "server_error", - state: req.State, - }) - return - } - c.Redirect(http.StatusFound, fmt.Sprintf("%s?%s", req.RedirectURI, queries.Encode())) - return - } - values := AuthorizeScreenParams{ LoginFor: FrontendLoginForOIDC, OIDCTicket: ticket, @@ -272,6 +241,37 @@ func (controller *OIDCController) authorize(c *gin.Context) { } } + if client.SkipAuthorization && values.OIDCPrompt != service.OIDCPromptLogin && userContext != nil && userContext.Authenticated { + controller.oidc.DeleteAuthorizeRequestTicket(ticket) + sub := controller.oidc.CreateSub(*userContext, req.ClientID) + if err := controller.oidc.DeleteOldSession(c, sub); err != nil { + controller.authorizeError(c, authorizeErrorParams{ + err: err, + reason: "Failed to delete old sessions", + reasonPublic: "Failed to delete old sessions", + callback: req.RedirectURI, + callbackError: "server_error", + state: req.State, + }) + return + } + code := controller.oidc.CreateCode(*req, *userContext) + queries, err := query.Values(AuthorizeCallback{Code: code, State: req.State}) + if err != nil { + controller.authorizeError(c, authorizeErrorParams{ + err: err, + reason: "Failed to build query", + reasonPublic: "Failed to build query", + callback: req.RedirectURI, + callbackError: "server_error", + state: req.State, + }) + return + } + c.Redirect(http.StatusFound, fmt.Sprintf("%s?%s", req.RedirectURI, queries.Encode())) + return + } + queries, err := query.Values(values) if err != nil { From d0eb33b8d0aced079938939b43c757f3d4d48a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Guldmund?= Date: Thu, 9 Jul 2026 22:25:56 +0200 Subject: [PATCH 4/5] fix: use url.Parse to safely merge callback query params --- internal/controller/oidc_controller.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index 771c50c9..35c028bd 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "slices" "strconv" "strings" @@ -256,19 +257,23 @@ func (controller *OIDCController) authorize(c *gin.Context) { return } code := controller.oidc.CreateCode(*req, *userContext) - queries, err := query.Values(AuthorizeCallback{Code: code, State: req.State}) + redirectURL, err := url.Parse(req.RedirectURI) if err != nil { controller.authorizeError(c, authorizeErrorParams{ err: err, - reason: "Failed to build query", - reasonPublic: "Failed to build query", + reason: "Failed to build callback URL", + reasonPublic: "Failed to build callback URL", callback: req.RedirectURI, callbackError: "server_error", state: req.State, }) return } - c.Redirect(http.StatusFound, fmt.Sprintf("%s?%s", req.RedirectURI, queries.Encode())) + redirectQueries := redirectURL.Query() + redirectQueries.Set("code", code) + redirectQueries.Set("state", req.State) + redirectURL.RawQuery = redirectQueries.Encode() + c.Redirect(http.StatusFound, redirectURL.String()) return } From dfb81ce2c07d9968c8073638fabef8506ebcadb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Guldmund?= Date: Thu, 9 Jul 2026 22:26:55 +0200 Subject: [PATCH 5/5] fix: compute forceLogin before ticket creation and skip branch --- internal/controller/oidc_controller.go | 34 ++++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index 35c028bd..f075f909 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -205,20 +205,7 @@ func (controller *OIDCController) authorize(c *gin.Context) { return } - ticket := controller.oidc.CreateAuthorizeRequestTicket(*req) - - values := AuthorizeScreenParams{ - LoginFor: FrontendLoginForOIDC, - OIDCTicket: ticket, - OIDCScope: req.Scope, - OIDCName: client.Name, - } - - if slices.Contains(prompts, service.OIDCPromptLogin) { - values.OIDCPrompt = service.OIDCPromptLogin - } else if slices.Contains(prompts, service.OIDCPromptNone) { - values.OIDCPrompt = service.OIDCPromptNone - } + forceLogin := slices.Contains(prompts, service.OIDCPromptLogin) if req.MaxAge != "" && userContext != nil { maxAge, err := strconv.Atoi(req.MaxAge) @@ -237,12 +224,14 @@ func (controller *OIDCController) authorize(c *gin.Context) { if userContext.Authenticated { authTime := time.Unix(userContext.AuthTime, 0) if authTime.Add(time.Duration(maxAge) * time.Second).Before(time.Now()) { - values.OIDCPrompt = service.OIDCPromptLogin + forceLogin = true } } } - if client.SkipAuthorization && values.OIDCPrompt != service.OIDCPromptLogin && userContext != nil && userContext.Authenticated { + ticket := controller.oidc.CreateAuthorizeRequestTicket(*req) + + if client.SkipAuthorization && userContext != nil && userContext.Authenticated && !forceLogin { controller.oidc.DeleteAuthorizeRequestTicket(ticket) sub := controller.oidc.CreateSub(*userContext, req.ClientID) if err := controller.oidc.DeleteOldSession(c, sub); err != nil { @@ -277,6 +266,19 @@ func (controller *OIDCController) authorize(c *gin.Context) { return } + values := AuthorizeScreenParams{ + LoginFor: FrontendLoginForOIDC, + OIDCTicket: ticket, + OIDCScope: req.Scope, + OIDCName: client.Name, + } + + if forceLogin { + values.OIDCPrompt = service.OIDCPromptLogin + } else if slices.Contains(prompts, service.OIDCPromptNone) { + values.OIDCPrompt = service.OIDCPromptNone + } + queries, err := query.Values(values) if err != nil {