From ff271e7f182bda721de2c5b909d7ea52119860ca Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 10 Jul 2026 01:56:12 +0300 Subject: [PATCH] feat: do not show oidc consent screen every time --- frontend/src/pages/authorize-page.tsx | 3 +- internal/bootstrap/app_bootstrap.go | 3 +- internal/controller/oidc_controller.go | 45 ++++++++++++++++++++++++++ internal/model/constants.go | 3 +- internal/model/runtime.go | 3 +- internal/service/oidc_service.go | 26 +++++++++++++++ 6 files changed, 75 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/authorize-page.tsx b/frontend/src/pages/authorize-page.tsx index 0f14a5830..773cb9f65 100644 --- a/frontend/src/pages/authorize-page.tsx +++ b/frontend/src/pages/authorize-page.tsx @@ -184,8 +184,7 @@ export const AuthorizePage = () => { diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index f82f8a12f..7f05732a1 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -177,8 +177,7 @@ func (app *BootstrapApp) Setup() error { cookieId := strings.Split(app.runtime.UUID, "-")[0] // first 8 characters of the uuid should be good enough app.runtime.SessionCookieName = fmt.Sprintf("%s-%s", model.SessionCookieName, cookieId) - app.runtime.CSRFCookieName = fmt.Sprintf("%s-%s", model.CSRFCookieName, cookieId) - app.runtime.RedirectCookieName = fmt.Sprintf("%s-%s", model.RedirectCookieName, cookieId) + app.runtime.ScopeCookieName = fmt.Sprintf("%s-%s", model.OIDCScopeCookieName, cookieId) app.runtime.OAuthSessionCookieName = fmt.Sprintf("%s-%s", model.OAuthSessionCookieName, cookieId) // database diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index 0c5ac918d..e7b14c850 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -34,6 +34,7 @@ type OIDCController struct { log *logger.Logger oidc *service.OIDCService runtime *model.RuntimeConfig + config *model.Config } type AuthorizeCallback struct { @@ -87,6 +88,7 @@ type OIDCControllerInput struct { Log *logger.Logger OIDCService *service.OIDCService + Config *model.Config RuntimeConfig *model.RuntimeConfig RouterGroup *gin.RouterGroup `name:"apiRouterGroup"` MainRouter *gin.RouterGroup `name:"mainRouterGroup"` @@ -97,6 +99,7 @@ func NewOIDCController(i OIDCControllerInput) *OIDCController { log: i.Log, oidc: i.OIDCService, runtime: i.RuntimeConfig, + config: i.Config, } i.MainRouter.POST("/authorize", controller.authorize) @@ -241,6 +244,19 @@ func (controller *OIDCController) authorize(c *gin.Context) { } } + cookieId := strings.SplitN(client.ClientID, "-", 2)[0] + cookieName := fmt.Sprintf("%s-%s", controller.runtime.ScopeCookieName, cookieId) + scopeCookie, err := c.Cookie(cookieName) + + if err == nil { + scopes := fmt.Sprintf("scopes=%s;", req.Scope) + if controller.oidc.VerifySignedValue(client.ClientSecret, []byte(scopes), scopeCookie) { + if values.OIDCPrompt != service.OIDCPromptLogin { + values.OIDCPrompt = service.OIDCPromptNone + } + } + } + queries, err := query.Values(values) if err != nil { @@ -319,6 +335,19 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) { return } + // Get the client + client, ok := controller.oidc.GetClient(authorizeReq.ClientID) + + if !ok { + controller.authorizeError(c, authorizeErrorParams{ + err: errors.New("client not found"), + reason: "Client not found", + reasonPublic: "The client is not configured", + json: true, + }) + return + } + // We no longer need the ticket controller.oidc.DeleteAuthorizeRequestTicket(req.Ticket) @@ -361,6 +390,22 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) { return } + // Set a cookie for the consent screen (approved scopes) + cookieId := strings.SplitN(client.ClientID, "-", 2)[0] + cookieName := fmt.Sprintf("%s-%s", controller.runtime.ScopeCookieName, cookieId) + scopes := fmt.Sprintf("scopes=%s;", authorizeReq.Scope) + + cookie := &http.Cookie{ + Name: cookieName, + Value: controller.oidc.CreateSignedValue(client.ClientSecret, []byte(scopes)), + Path: "/", + Secure: controller.config.Auth.SecureCookie, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + } + + http.SetCookie(c.Writer, cookie) + c.JSON(200, gin.H{ "status": 200, "redirect_uri": fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()), diff --git a/internal/model/constants.go b/internal/model/constants.go index ff44a7299..2dd8616bf 100644 --- a/internal/model/constants.go +++ b/internal/model/constants.go @@ -20,8 +20,7 @@ var OverrideProviders = map[string]string{ var ReservedProviderNames = []string{"local", "ldap", "tailscale"} const SessionCookieName = "tinyauth-session" -const CSRFCookieName = "tinyauth-csrf" -const RedirectCookieName = "tinyauth-redirect" const OAuthSessionCookieName = "tinyauth-oauth" +const OIDCScopeCookieName = "tinyauth-scope" const GracefulShutdownTimeout = 5 // seconds diff --git a/internal/model/runtime.go b/internal/model/runtime.go index e1c034d3b..3731550b7 100644 --- a/internal/model/runtime.go +++ b/internal/model/runtime.go @@ -5,8 +5,7 @@ type RuntimeConfig struct { UUID string CookieDomain string SessionCookieName string - CSRFCookieName string - RedirectCookieName string + ScopeCookieName string OAuthSessionCookieName string LocalUsers []LocalUser OAuthProviders map[string]OAuthServiceConfig diff --git a/internal/service/oidc_service.go b/internal/service/oidc_service.go index a3a02400b..b0e8e8380 100644 --- a/internal/service/oidc_service.go +++ b/internal/service/oidc_service.go @@ -3,6 +3,7 @@ package service import ( "context" "crypto" + "crypto/hmac" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -22,6 +23,7 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" "github.com/steveiliop56/ding" "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/repository" @@ -305,6 +307,9 @@ func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) { for id, client := range i.Config.OIDC.Clients { client.ID = id + if err := uuid.Validate(client.ClientID); err != nil { + return nil, fmt.Errorf("invalid client id: %w", err) + } if client.Name == "" { client.Name = utils.Capitalize(client.ID) } @@ -318,6 +323,9 @@ func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) { client.ClientSecret = secret } client.ClientSecretFile = "" + if len(client.ClientSecret) < 32 { + return nil, fmt.Errorf("client secret for client %s is too short, must be >= 32 chars", client.ClientID) + } clients[id] = client i.Log.App.Debug().Str("clientId", client.ClientID).Msg("Loaded OIDC client configuration") } @@ -969,3 +977,21 @@ func (service *OIDCService) GetPrompt(prompt string) []OIDCPrompt { return parsedPromps } + +func (service *OIDCService) CreateSignedValue(key string, data []byte) string { + // create the signature + h := hmac.New(sha256.New, []byte(key)) + h.Write(data) + sig := base64.URLEncoding.EncodeToString(h.Sum(nil)) + + // hash the data + hasher := sha256.New() + hasher.Write(data) + hash := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + + return fmt.Sprintf("%s.%s", hash, sig) +} + +func (service *OIDCService) VerifySignedValue(key string, data []byte, signedValue string) bool { + return service.CreateSignedValue(key, data) == signedValue +}