feat: add backend for oidc consent

This commit is contained in:
Stavros
2026-06-11 18:09:14 +03:00
parent e4c5f14d8c
commit 24f166551e
17 changed files with 262 additions and 64 deletions
+25 -10
View File
@@ -24,6 +24,7 @@ type OAuthController struct {
log *logger.Logger
config model.Config
runtime model.RuntimeConfig
helpers model.RuntimeHelpers
auth *service.AuthService
}
@@ -31,6 +32,7 @@ func NewOAuthController(
log *logger.Logger,
config model.Config,
runtimeConfig model.RuntimeConfig,
helpers model.RuntimeHelpers,
router *gin.RouterGroup,
auth *service.AuthService,
) *OAuthController {
@@ -38,6 +40,7 @@ func NewOAuthController(
log: log,
config: config,
runtime: runtimeConfig,
helpers: helpers,
auth: auth,
}
@@ -105,7 +108,18 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
return
}
c.SetCookie(controller.runtime.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.Auth.SecureCookie, true)
cookieDomain, err := controller.helpers.GetCookieDomain(c, c.RemoteIP())
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to determine cookie domain")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
c.SetCookie(controller.runtime.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", cookieDomain, controller.config.Auth.SecureCookie, true)
c.JSON(200, gin.H{
"status": 200,
@@ -135,7 +149,15 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
return
}
c.SetCookie(controller.runtime.OAuthSessionCookieName, "", -1, "/", controller.getCookieDomain(), controller.config.Auth.SecureCookie, true)
cookieDomain, err := controller.helpers.GetCookieDomain(c, c.RemoteIP())
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to determine cookie domain")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
return
}
c.SetCookie(controller.runtime.OAuthSessionCookieName, "", -1, "/", cookieDomain, controller.config.Auth.SecureCookie, true)
oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
@@ -252,7 +274,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
controller.log.App.Debug().Msg("Creating session cookie for user")
cookie, err := controller.auth.CreateSession(c, sessionCookie)
cookie, err := controller.auth.CreateSession(c, sessionCookie, c.RemoteIP())
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to create session cookie")
@@ -298,10 +320,3 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackParams) bool {
return params.LoginFor == string(FrontendLoginForOIDC)
}
func (controller *OAuthController) getCookieDomain() string {
if controller.config.Auth.SubdomainsEnabled {
return "." + controller.runtime.CookieDomain
}
return controller.runtime.CookieDomain
}
+64 -8
View File
@@ -1,12 +1,14 @@
package controller
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"slices"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
@@ -31,6 +33,8 @@ type OIDCController struct {
log *logger.Logger
oidc *service.OIDCService
runtime model.RuntimeConfig
helpers model.RuntimeHelpers
config model.Config
}
type AuthorizeCallback struct {
@@ -68,10 +72,11 @@ type ClientCredentials struct {
}
type AuthorizeScreenParams struct {
LoginFor FrontendLoginFor `url:"login_for"`
OIDCTicket string `url:"oidc_ticket"`
OIDCScope string `url:"oidc_scope"`
OIDCName string `url:"oidc_name"`
LoginFor FrontendLoginFor `url:"login_for"`
OIDCTicket string `url:"oidc_ticket"`
OIDCScope string `url:"oidc_scope"`
OIDCName string `url:"oidc_name"`
OIDCShowConsent bool `url:"oidc_show_consent"`
}
type AuthorizeCompleteRequest struct {
@@ -82,12 +87,16 @@ func NewOIDCController(
log *logger.Logger,
oidcService *service.OIDCService,
runtimeConfig model.RuntimeConfig,
helpers model.RuntimeHelpers,
config model.Config,
router *gin.RouterGroup,
mainRouter *gin.RouterGroup) *OIDCController {
controller := &OIDCController{
log: log,
oidc: oidcService,
runtime: runtimeConfig,
helpers: helpers,
config: config,
}
mainRouter.POST("/authorize", controller.authorize)
@@ -163,11 +172,31 @@ func (controller *OIDCController) authorize(c *gin.Context) {
ticket := controller.oidc.CreateAuthorizeRequestTicket(*req)
// Check if we have consented before for this client and scope
consnetCookie, err := c.Cookie(controller.runtime.ConsentCookieName)
showConsent := true
if err == nil {
consentEntry, err := controller.oidc.GetConsentEntry(c, consnetCookie)
if err == nil && consentEntry != nil {
if consentEntry.ClientID == req.ClientID && consentEntry.Scopes == req.Scope {
showConsent = false
}
} else {
if !errors.Is(err, sql.ErrNoRows) {
controller.log.App.Error().Err(err).Msg("Failed to get consent entry for consent cookie")
}
}
}
queries, err := query.Values(AuthorizeScreenParams{
LoginFor: FrontendLoginForOIDC,
OIDCTicket: ticket,
OIDCScope: req.Scope,
OIDCName: client.Name,
LoginFor: FrontendLoginForOIDC,
OIDCTicket: ticket,
OIDCScope: req.Scope,
OIDCName: client.Name,
OIDCShowConsent: showConsent,
})
if err != nil {
@@ -289,6 +318,33 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
return
}
// Just before returning let's set the consent cookie
consnetUUID, err := controller.oidc.CreateConsentEntry(c, authorizeReq.ClientID, authorizeReq.Scope)
// If we fail to create the consent entry, we don't want to block the authorization flow,
// but we log the error and move on without setting the cookie
if err == nil {
cookieDomain, err := controller.helpers.GetCookieDomain(c.Request.Context(), c.RemoteIP())
if err == nil {
cookie := &http.Cookie{
Name: controller.runtime.ConsentCookieName,
Value: consnetUUID,
Path: "/",
Domain: cookieDomain,
Expires: time.Now().Add(365 * 24 * time.Hour), // set consent cookie for 1 year
Secure: controller.config.Auth.SecureCookie,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(c.Writer, cookie)
} else {
controller.log.App.Error().Err(err).Msg("Failed to determine cookie domain for consent cookie")
}
} else {
controller.log.App.Error().Err(err).Msg("Failed to create consent entry")
}
c.JSON(200, gin.H{
"status": 200,
"redirect_uri": fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()),
+3 -1
View File
@@ -30,6 +30,8 @@ func TestOIDCController(t *testing.T) {
cfg, runtime := test.CreateTestConfigs(t)
helpers := test.CreateTestHelpers()
ctx := context.TODO()
dg := ding.New(ctx)
@@ -831,7 +833,7 @@ func TestOIDCController(t *testing.T) {
svc = nil
}
controller.NewOIDCController(log, svc, runtime, group, &router.RouterGroup)
controller.NewOIDCController(log, svc, runtime, helpers, cfg, group, &router.RouterGroup)
recorder := httptest.NewRecorder()
+3 -1
View File
@@ -24,6 +24,8 @@ func TestProxyController(t *testing.T) {
cfg, runtime := test.CreateTestConfigs(t)
helpers := test.CreateTestHelpers()
const browserUserAgent = `
Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36`
@@ -395,7 +397,7 @@ func TestProxyController(t *testing.T) {
Log: log,
})
authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
authService := service.NewAuthService(log, cfg, runtime, helpers, ctx, dg, nil, store, broker, nil, policyEngine)
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
+6 -6
View File
@@ -150,7 +150,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
Email: email,
Provider: "local",
TotpPending: true,
})
}, c.RemoteIP())
if err != nil {
controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Failed to create pending TOTP session")
@@ -195,7 +195,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
}
}
cookie, err := controller.auth.CreateSession(c, sessionCookie)
cookie, err := controller.auth.CreateSession(c, sessionCookie, c.RemoteIP())
if err != nil {
controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Failed to create session cookie after successful login")
@@ -246,7 +246,7 @@ func (controller *UserController) logoutHandler(c *gin.Context) {
return
}
cookie, err := controller.auth.DeleteSession(c, uuid)
cookie, err := controller.auth.DeleteSession(c, uuid, c.RemoteIP())
if err != nil {
controller.log.App.Error().Err(err).Msg("Error deleting session on logout")
@@ -350,7 +350,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
uuid, err := c.Cookie(controller.runtime.SessionCookieName)
if err == nil {
_, err = controller.auth.DeleteSession(c, uuid)
_, err = controller.auth.DeleteSession(c, uuid, c.RemoteIP())
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to delete pending TOTP session after successful verification")
}
@@ -374,7 +374,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
sessionCookie.Email = user.Attributes.Email
}
cookie, err := controller.auth.CreateSession(c, sessionCookie)
cookie, err := controller.auth.CreateSession(c, sessionCookie, c.RemoteIP())
if err != nil {
controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful TOTP verification")
@@ -424,7 +424,7 @@ func (controller *UserController) tailscaleHandler(c *gin.Context) {
Provider: "tailscale",
}
cookie, err := controller.auth.CreateSession(c, sessionCookie)
cookie, err := controller.auth.CreateSession(c, sessionCookie, c.RemoteIP())
if err != nil {
controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful Tailscale login")
+3 -1
View File
@@ -29,6 +29,8 @@ func TestUserController(t *testing.T) {
cfg, runtime := test.CreateTestConfigs(t)
helpers := test.CreateTestHelpers()
totpCtx := func(c *gin.Context) {
c.Set("context", &model.UserContext{
Authenticated: false,
@@ -418,7 +420,7 @@ func TestUserController(t *testing.T) {
require.NoError(t, err)
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
authService := service.NewAuthService(log, cfg, runtime, helpers, ctx, dg, nil, store, broker, nil, policyEngine)
beforeEach := func() {
// Clear failed login attempts before each test