diff --git a/backend/frontend/frontend_excluded.go b/backend/frontend/frontend_excluded.go index 2e0e35ef..a3e5a691 100644 --- a/backend/frontend/frontend_excluded.go +++ b/backend/frontend/frontend_excluded.go @@ -2,8 +2,11 @@ package frontend -import "github.com/gin-gonic/gin" +import ( + "github.com/gin-gonic/gin" + "github.com/pocket-id/pocket-id/backend/internal/service" +) -func RegisterFrontend(router *gin.Engine, rateLimitMiddleware gin.HandlerFunc) error { +func RegisterFrontend(router *gin.Engine, rateLimitMiddleware gin.HandlerFunc, oidcService *service.OidcService) error { return ErrFrontendNotIncluded } diff --git a/backend/frontend/frontend_included.go b/backend/frontend/frontend_included.go index 568449ef..42813f44 100644 --- a/backend/frontend/frontend_included.go +++ b/backend/frontend/frontend_included.go @@ -4,6 +4,7 @@ package frontend import ( "bytes" + "context" "embed" "fmt" "io" @@ -17,6 +18,8 @@ import ( "github.com/gin-gonic/gin" "github.com/pocket-id/pocket-id/backend/internal/middleware" + "github.com/pocket-id/pocket-id/backend/internal/service" + "github.com/pocket-id/pocket-id/backend/internal/utils" ) //go:embed all:dist/* @@ -54,7 +57,28 @@ func init() { } } -func RegisterFrontend(router *gin.Engine, rateLimitMiddleware gin.HandlerFunc) error { +// validateRedirectURI validates that the redirect_uri is in the client's allowed callback URLs +func validateRedirectURI(ctx any, oidcService *service.OidcService, clientID, redirectURI string) (bool, error) { + client, err := oidcService.GetClient(ctx.(context.Context), clientID) + if err != nil { + return false, err + } + + // If the client has no callback URLs configured, reject the redirect URI + if len(client.CallbackURLs) == 0 { + return false, fmt.Errorf("client has no callback URLs configured") + } + + // Validate the redirect URI against the client's callback URLs + _, err = utils.GetCallbackURLFromList(client.CallbackURLs, redirectURI) + if err != nil { + return false, err + } + + return true, nil +} + +func RegisterFrontend(router *gin.Engine, rateLimitMiddleware gin.HandlerFunc, oidcService *service.OidcService) error { distFS, err := fs.Sub(frontendFS, "dist") if err != nil { return fmt.Errorf("failed to create sub FS: %w", err) @@ -92,12 +116,17 @@ func RegisterFrontend(router *gin.Engine, rateLimitMiddleware gin.HandlerFunc) e if path == "index.html" { // Check if this is an OAuth2 authorization request with response_mode=form_post - // In that case, we need to allow form submissions to the redirect_uri + // In that case, we need to validate and allow form submissions to the redirect_uri responseMode := c.Query("response_mode") redirectURI := c.Query("redirect_uri") - if responseMode == "form_post" && redirectURI != "" { - // Set the allowed form-action in CSP to include the redirect URI - middleware.SetAllowedFormAction(c, redirectURI) + clientID := c.Query("client_id") + if responseMode == "form_post" && redirectURI != "" && clientID != "" { + // Validate the redirect_uri against the client's allowlist + isValid, err := validateRedirectURI(c.Request.Context(), oidcService, clientID, redirectURI) + if err == nil && isValid { + // Set the allowed form-action in CSP to include the redirect URI + middleware.SetAllowedFormAction(c, redirectURI) + } } nonce := middleware.GetCSPNonce(c) diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index 6803734d..fc957bd6 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -61,7 +61,7 @@ func initRouter(db *gorm.DB, svc *services) (utils.Service, error) { r.Use(middleware.NewErrorHandlerMiddleware().Add()) frontendRateLimitMiddleware := middleware.NewRateLimitMiddleware().Add(rate.Every(100*time.Millisecond), 300) - err := frontend.RegisterFrontend(r, frontendRateLimitMiddleware) + err := frontend.RegisterFrontend(r, frontendRateLimitMiddleware, svc.oidcService) if errors.Is(err, frontend.ErrFrontendNotIncluded) { slog.Warn("Frontend is not included in the build. Skipping frontend registration.") } else if err != nil {