From 24f166551e64cb99a160fdd595b490d3713db9a4 Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 11 Jun 2026 18:09:14 +0300 Subject: [PATCH] feat: add backend for oidc consent --- internal/bootstrap/app_bootstrap.go | 7 +- internal/bootstrap/app_helpers.go | 55 ++++++++++++++ internal/bootstrap/router_bootstrap.go | 4 +- internal/bootstrap/service_bootstrap.go | 2 +- internal/controller/oauth_controller.go | 35 ++++++--- internal/controller/oidc_controller.go | 72 ++++++++++++++++--- internal/controller/oidc_controller_test.go | 4 +- internal/controller/proxy_controller_test.go | 4 +- internal/controller/user_controller.go | 12 ++-- internal/controller/user_controller_test.go | 4 +- internal/middleware/context_middleware.go | 4 +- .../middleware/context_middleware_test.go | 4 +- internal/model/constants.go | 3 +- internal/model/runtime.go | 9 ++- internal/service/auth_service.go | 53 +++++++------- internal/service/oidc_service.go | 45 ++++++++++++ internal/test/test.go | 9 +++ 17 files changed, 262 insertions(+), 64 deletions(-) create mode 100644 internal/bootstrap/app_helpers.go diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 7fc0cb54..2180df2c 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -47,6 +47,7 @@ type Services struct { type BootstrapApp struct { config model.Config runtime model.RuntimeConfig + helpers model.RuntimeHelpers services Services log *logger.Logger ctx context.Context @@ -185,9 +186,8 @@ 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.OAuthSessionCookieName = fmt.Sprintf("%s-%s", model.OAuthSessionCookieName, cookieId) + app.runtime.ConsentCookieName = fmt.Sprintf("%s-%s", model.ConsentCookieName, cookieId) // database store, err := app.SetupStore() @@ -264,6 +264,9 @@ func (app *BootstrapApp) Setup() error { app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, "https://"+app.services.tailscaleService.GetHostname()) } + // runtime helpers + app.helpers.GetCookieDomain = app.getCookieDomain + // setup router err = app.setupRouter() diff --git a/internal/bootstrap/app_helpers.go b/internal/bootstrap/app_helpers.go new file mode 100644 index 00000000..4be94753 --- /dev/null +++ b/internal/bootstrap/app_helpers.go @@ -0,0 +1,55 @@ +package bootstrap + +import ( + "context" + "errors" + "fmt" + + "github.com/tinyauthapp/tinyauth/internal/utils" +) + +// Not really the best place for the helpers to be but it works because bootstrap app provides +// them with everything they need + +func (app *BootstrapApp) getCookieDomain(ctx context.Context, ip string) (string, error) { + cookieDomain := app.runtime.CookieDomain + + if app.isTailscaleRequest(ctx, ip) { + if app.services.tailscaleService == nil { + return "", errors.New("tailscale service is not configured") + } + + tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", app.services.tailscaleService.GetHostname())) + + if err != nil { + return "", fmt.Errorf("failed to get cookie domain for tailscale user: %w", err) + } + + cookieDomain = tsCookieDomain + } + + if app.config.Auth.SubdomainsEnabled { + cookieDomain = "." + cookieDomain + } + + return cookieDomain, nil +} + +func (app *BootstrapApp) isTailscaleRequest(ctx context.Context, ip string) bool { + if app.services.tailscaleService == nil { + return false + } + + whois, err := app.services.tailscaleService.Whois(ctx, ip) + + if err != nil { + app.log.App.Error().Err(err).Msgf("Error performing Tailscale whois for IP %s: %v", ip, err) + return false + } + + if whois == nil { + return false + } + + return true +} diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 5244ab20..eb91fdd7 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -58,8 +58,8 @@ func (app *BootstrapApp) setupRouter() error { apiRouter := engine.Group("/api") controller.NewContextController(app.log, app.config, app.runtime, apiRouter) - controller.NewOAuthController(app.log, app.config, app.runtime, apiRouter, app.services.authService) - controller.NewOIDCController(app.log, app.services.oidcService, app.runtime, apiRouter, &engine.RouterGroup) + controller.NewOAuthController(app.log, app.config, app.runtime, app.helpers, apiRouter, app.services.authService) + controller.NewOIDCController(app.log, app.services.oidcService, app.runtime, app.helpers, app.config, apiRouter, &engine.RouterGroup) controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService, app.services.policyEngine) controller.NewUserController(app.log, app.runtime, apiRouter, app.services.authService) controller.NewResourcesController(app.config, &engine.RouterGroup) diff --git a/internal/bootstrap/service_bootstrap.go b/internal/bootstrap/service_bootstrap.go index bf94c5c4..452b6d8f 100644 --- a/internal/bootstrap/service_bootstrap.go +++ b/internal/bootstrap/service_bootstrap.go @@ -42,7 +42,7 @@ func (app *BootstrapApp) setupServices() error { oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx) app.services.oauthBrokerService = oauthBrokerService - authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, app.ding, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService, app.services.policyEngine) + authService := service.NewAuthService(app.log, app.config, app.runtime, app.helpers, app.ctx, app.ding, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService, app.services.policyEngine) app.services.authService = authService oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ding) diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 788fedfa..84f83989 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -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 -} diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index 5105f7d7..198e460d 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -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()), diff --git a/internal/controller/oidc_controller_test.go b/internal/controller/oidc_controller_test.go index d4a07baa..42d95462 100644 --- a/internal/controller/oidc_controller_test.go +++ b/internal/controller/oidc_controller_test.go @@ -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() diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index c6a358b4..a6f71908 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -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) { diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index fd3159f7..9758c89c 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -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") diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go index f3c0bed2..abb2aef5 100644 --- a/internal/controller/user_controller_test.go +++ b/internal/controller/user_controller_test.go @@ -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 diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index fc694ddf..d2042084 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -206,12 +206,12 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string, ip stri } if !m.auth.IsEmailWhitelisted(userContext.OAuth.ID, userContext.OAuth.Email) { - m.auth.DeleteSession(ctx, uuid) + m.auth.DeleteSession(ctx, uuid, ip) return nil, nil, fmt.Errorf("email from session cookie not whitelisted: %s", userContext.OAuth.Email) } } - cookie, err := m.auth.RefreshSession(ctx, uuid) + cookie, err := m.auth.RefreshSession(ctx, uuid, ip) if err != nil { return nil, nil, fmt.Errorf("error refreshing session: %w", err) diff --git a/internal/middleware/context_middleware_test.go b/internal/middleware/context_middleware_test.go index 50ededdb..547104aa 100644 --- a/internal/middleware/context_middleware_test.go +++ b/internal/middleware/context_middleware_test.go @@ -27,6 +27,8 @@ func TestContextMiddleware(t *testing.T) { cfg, runtime := test.CreateTestConfigs(t) + helpers := test.CreateTestHelpers() + basicAuthHeader := func(username, password string) string { return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) } @@ -258,7 +260,7 @@ func TestContextMiddleware(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) contextMiddleware := middleware.NewContextMiddleware(log, runtime, authService, broker, nil) diff --git a/internal/model/constants.go b/internal/model/constants.go index d5885dcf..35ce2813 100644 --- a/internal/model/constants.go +++ b/internal/model/constants.go @@ -18,8 +18,7 @@ var OverrideProviders = map[string]string{ } const SessionCookieName = "tinyauth-session" -const CSRFCookieName = "tinyauth-csrf" -const RedirectCookieName = "tinyauth-redirect" const OAuthSessionCookieName = "tinyauth-oauth" +const ConsentCookieName = "tinyauth-consent" const GracefulShutdownTimeout = 5 // seconds diff --git a/internal/model/runtime.go b/internal/model/runtime.go index 9df20b85..7b067210 100644 --- a/internal/model/runtime.go +++ b/internal/model/runtime.go @@ -1,13 +1,14 @@ package model +import "context" + type RuntimeConfig struct { AppURL string UUID string CookieDomain string SessionCookieName string - CSRFCookieName string - RedirectCookieName string OAuthSessionCookieName string + ConsentCookieName string LocalUsers []LocalUser OAuthProviders map[string]OAuthServiceConfig OAuthWhitelist []string @@ -16,6 +17,10 @@ type RuntimeConfig struct { TrustedDomains []string } +type RuntimeHelpers struct { + GetCookieDomain func(ctx context.Context, ip string) (string, error) +} + type Provider struct { Name string `json:"name"` ID string `json:"id"` diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index ef3e9e08..15be926a 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -59,6 +59,7 @@ type AuthService struct { log *logger.Logger config model.Config runtime model.RuntimeConfig + helpers model.RuntimeHelpers ctx context.Context ldap *LdapService @@ -86,6 +87,7 @@ func NewAuthService( log *logger.Logger, config model.Config, runtime model.RuntimeConfig, + helpers model.RuntimeHelpers, ctx context.Context, dg *ding.Ding, ldap *LdapService, @@ -97,6 +99,7 @@ func NewAuthService( service := &AuthService{ log: log, runtime: runtime, + helpers: helpers, ctx: ctx, config: config, ldap: ldap, @@ -322,7 +325,7 @@ func (auth *AuthService) IsEmailWhitelisted(provider string, email string) bool }) } -func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) { +func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session, ip string) (*http.Cookie, error) { if data.Provider == "tailscale" && auth.tailscale == nil { return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user") } @@ -363,33 +366,17 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess return nil, fmt.Errorf("failed to create session entry: %w", err) } - if data.Provider == "tailscale" { - auth.log.App.Trace().Str("url", fmt.Sprintf("https://%s", auth.tailscale.GetHostname())).Msg("Extracting root domain from Tailscale hostname") + cookieDomain, err := auth.helpers.GetCookieDomain(ctx, ip) - tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", auth.tailscale.GetHostname())) - - if err != nil { - return nil, fmt.Errorf("failed to get cookie domain for tailscale user: %w", err) - } - - return &http.Cookie{ - Name: auth.runtime.SessionCookieName, - Value: session.UUID, - Path: "/", - Domain: fmt.Sprintf(".%s", tsCookieDomain), - Expires: expiresAt, - MaxAge: int(time.Until(expiresAt).Seconds()), - Secure: auth.config.Auth.SecureCookie, - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - }, nil + if err != nil { + return nil, fmt.Errorf("failed to determine cookie domain: %w", err) } return &http.Cookie{ Name: auth.runtime.SessionCookieName, Value: session.UUID, Path: "/", - Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain), + Domain: cookieDomain, Expires: expiresAt, MaxAge: int(time.Until(expiresAt).Seconds()), Secure: auth.config.Auth.SecureCookie, @@ -398,13 +385,17 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess }, nil } -func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http.Cookie, error) { +func (auth *AuthService) RefreshSession(ctx context.Context, uuid string, ip string) (*http.Cookie, error) { session, err := auth.queries.GetSession(ctx, uuid) if err != nil { return nil, fmt.Errorf("failed to retrieve session: %w", err) } + if session.Provider == "tailscale" && auth.tailscale == nil { + return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user") + } + currentTime := time.Now().Unix() var refreshThreshold int64 @@ -438,11 +429,17 @@ func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http return nil, fmt.Errorf("failed to update session expiry: %w", err) } + cookieDomain, err := auth.helpers.GetCookieDomain(ctx, ip) + + if err != nil { + return nil, fmt.Errorf("failed to determine cookie domain: %w", err) + } + return &http.Cookie{ Name: auth.runtime.SessionCookieName, Value: session.UUID, Path: "/", - Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain), + Domain: cookieDomain, Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second), MaxAge: int(newExpiry - currentTime), Secure: auth.config.Auth.SecureCookie, @@ -452,18 +449,24 @@ func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http } -func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.Cookie, error) { +func (auth *AuthService) DeleteSession(ctx context.Context, uuid string, ip string) (*http.Cookie, error) { err := auth.queries.DeleteSession(ctx, uuid) if err != nil { auth.log.App.Error().Err(err).Str("uuid", uuid).Msg("Failed to delete session from database") } + cookieDomain, err := auth.helpers.GetCookieDomain(ctx, ip) + + if err != nil { + return nil, fmt.Errorf("failed to determine cookie domain: %w", err) + } + return &http.Cookie{ Name: auth.runtime.SessionCookieName, Value: "", Path: "/", - Domain: fmt.Sprintf(".%s", auth.runtime.CookieDomain), + Domain: cookieDomain, Expires: time.Now(), MaxAge: -1, Secure: auth.config.Auth.SecureCookie, diff --git a/internal/service/oidc_service.go b/internal/service/oidc_service.go index cafb59d1..f0e7c99f 100644 --- a/internal/service/oidc_service.go +++ b/internal/service/oidc_service.go @@ -21,6 +21,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" @@ -904,3 +905,47 @@ func (service *OIDCService) DecodeAuthorizeJWT(tokenString string) (*AuthorizeRe return claims, nil } + +func (service *OIDCService) CreateConsentEntry(ctx context.Context, clientId string, scope string) (string, error) { + u := uuid.New() + + entry := repository.CreateOIDCConsentParams{ + UUID: u.String(), + ClientID: clientId, + Scopes: scope, + } + + _, err := service.queries.CreateOIDCConsent(ctx, entry) + + if err != nil { + return "", err + } + + return entry.UUID, nil +} + +func (service *OIDCService) GetConsentEntry(ctx context.Context, uuid string) (*repository.OidcConsent, error) { + entry, err := service.queries.GetOIDCConsentByUUID(ctx, uuid) + + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + return nil, nil + } + return nil, err + } + + return &entry, nil +} + +func (service *OIDCService) DeleteConsentEntry(ctx context.Context, uuid string) error { + return service.queries.DeleteOIDCConsentByUUID(ctx, uuid) +} + +func (service *OIDCService) UpdateConsentEntry(ctx context.Context, uuid string, scopes string) error { + _, err := service.queries.UpdateOIDCConsent(ctx, repository.UpdateOIDCConsentParams{ + UUID: uuid, + Scopes: scopes, + }) + + return err +} diff --git a/internal/test/test.go b/internal/test/test.go index 415591fa..589b111a 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -1,6 +1,7 @@ package test import ( + "context" "path/filepath" "testing" @@ -133,3 +134,11 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) { return config, runtime } + +func CreateTestHelpers() model.RuntimeHelpers { + return model.RuntimeHelpers{ + GetCookieDomain: func(ctx context.Context, ip string) (string, error) { + return "example.com", nil + }, + } +}