diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index bdcdf3f..62557b6 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -57,6 +57,16 @@ export default defineConfig({ changeOrigin: true, rewrite: (path) => path.replace(/^\/robots.txt/, ""), }, + "/authorize": { + target: "http://tinyauth-backend:3000/authorize", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/authorize/, ""), + bypass: (req) => { + if (req.method === "GET") { + return "/index.html"; + } + }, + }, }, allowedHosts: true, }, diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 91d36ac..dd0f5a4 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -87,7 +87,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) { oauthController.SetupRoutes() - oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{}, app.services.oidcService, apiRouter) + oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{}, app.services.oidcService, apiRouter, engine) oidcController.SetupRoutes() diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index fa61461..02762c4 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -3,6 +3,7 @@ package controller import ( "errors" "fmt" + "io" "net/http" "slices" "strings" @@ -21,6 +22,7 @@ type OIDCController struct { config OIDCControllerConfig router *gin.RouterGroup oidc *service.OIDCService + engine *gin.Engine } type AuthorizeCallback struct { @@ -57,11 +59,12 @@ type ClientCredentials struct { ClientSecret string } -func NewOIDCController(config OIDCControllerConfig, oidcService *service.OIDCService, router *gin.RouterGroup) *OIDCController { +func NewOIDCController(config OIDCControllerConfig, oidcService *service.OIDCService, router *gin.RouterGroup, engine *gin.Engine) *OIDCController { return &OIDCController{ config: config, oidc: oidcService, router: router, + engine: engine, } } @@ -72,6 +75,7 @@ func (controller *OIDCController) SetupRoutes() { oidcGroup.POST("/token", controller.Token) oidcGroup.GET("/userinfo", controller.Userinfo) oidcGroup.POST("/userinfo", controller.Userinfo) + controller.engine.POST("/authorize", controller.AuthorizePseudoPost) } func (controller *OIDCController) GetClientInfo(c *gin.Context) { @@ -195,6 +199,18 @@ func (controller *OIDCController) Authorize(c *gin.Context) { }) } +// Pseudo handler that will just redirect to get in frontend then back to backend +func (controller *OIDCController) AuthorizePseudoPost(c *gin.Context) { + body, err := io.ReadAll(c.Request.Body) + if err != nil { + tlog.App.Error().Err(err).Msg("Failed to read request body") + c.Redirect(http.StatusFound, fmt.Sprintf("%s/authorize", controller.oidc.GetIssuer())) + return + } + redirectUrl := fmt.Sprintf("%s/authorize?%s", controller.oidc.GetIssuer(), body) + c.Redirect(http.StatusFound, redirectUrl) +} + func (controller *OIDCController) Token(c *gin.Context) { if !controller.oidc.IsConfigured() { tlog.App.Warn().Msg("OIDC not configured") diff --git a/internal/controller/oidc_controller_test.go b/internal/controller/oidc_controller_test.go index a09697b..25561f4 100644 --- a/internal/controller/oidc_controller_test.go +++ b/internal/controller/oidc_controller_test.go @@ -12,14 +12,14 @@ import ( "github.com/gin-gonic/gin" "github.com/google/go-querystring/query" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tinyauthapp/tinyauth/internal/bootstrap" "github.com/tinyauthapp/tinyauth/internal/config" "github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/utils/tlog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestOIDCController(t *testing.T) { @@ -846,6 +846,34 @@ func TestOIDCController(t *testing.T) { assert.Equal(t, "invalid_grant", res["error"]) }, }, + { + description: "Test authorize request with POST method", + middlewares: []gin.HandlerFunc{ + simpleCtx, + }, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + body := service.AuthorizeRequest{ + Scope: "openid", + ResponseType: "code", + ClientID: "some-client-id", + RedirectURI: "https://test.example.com/callback", + State: "some-state", + Nonce: "some-nonce", + CodeChallenge: "some-challenge", + CodeChallengeMethod: "plain", + } + queries, err := query.Values(body) + assert.NoError(t, err) + + req := httptest.NewRequest("POST", "/authorize", strings.NewReader(string(queries.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + router.ServeHTTP(recorder, req) + assert.Equal(t, 302, recorder.Code) + location := recorder.Header().Get("Location") + assert.NotEmpty(t, location) + assert.Equal(t, "https://tinyauth.example.com/authorize?client_id=some-client-id&code_challenge=some-challenge&code_challenge_method=plain&nonce=some-nonce&redirect_uri=https%3A%2F%2Ftest.example.com%2Fcallback&response_type=code&scope=openid&state=some-state", location) + }, + }, } app := bootstrap.NewBootstrapApp(config.Config{}) @@ -869,7 +897,7 @@ func TestOIDCController(t *testing.T) { group := router.Group("/api") gin.SetMode(gin.TestMode) - oidcController := controller.NewOIDCController(controllerCfg, oidcService, group) + oidcController := controller.NewOIDCController(controllerCfg, oidcService, group, router) oidcController.SetupRoutes() recorder := httptest.NewRecorder() diff --git a/internal/middleware/ui_middleware.go b/internal/middleware/ui_middleware.go index 96553b0..bbf340a 100644 --- a/internal/middleware/ui_middleware.go +++ b/internal/middleware/ui_middleware.go @@ -39,6 +39,7 @@ func (m *UIMiddleware) Init() error { func (m *UIMiddleware) Middleware() gin.HandlerFunc { return func(c *gin.Context) { path := strings.TrimPrefix(c.Request.URL.Path, "/") + method := c.Request.Method tlog.App.Debug().Str("path", path).Msg("path") @@ -52,6 +53,12 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc { c.Writer.Write([]byte("User-agent: *\nDisallow: /\n")) return default: + // For OIDC post authentication, we need to redirect the POST to /authorize to the backend + if method == http.MethodPost && strings.HasPrefix(path, "authorize") { + c.Next() + return + } + _, err := fs.Stat(m.uiFs, path) // Enough for one authentication flow diff --git a/internal/service/oidc_service.go b/internal/service/oidc_service.go index 888ad0e..dc4cd08 100644 --- a/internal/service/oidc_service.go +++ b/internal/service/oidc_service.go @@ -100,14 +100,14 @@ type TokenResponse struct { } type AuthorizeRequest struct { - Scope string `json:"scope" binding:"required"` - ResponseType string `json:"response_type" binding:"required"` - ClientID string `json:"client_id" binding:"required"` - RedirectURI string `json:"redirect_uri" binding:"required"` - State string `json:"state"` - Nonce string `json:"nonce"` - CodeChallenge string `json:"code_challenge"` - CodeChallengeMethod string `json:"code_challenge_method"` + Scope string `json:"scope" binding:"required" url:"scope"` + ResponseType string `json:"response_type" binding:"required" url:"response_type"` + ClientID string `json:"client_id" binding:"required" url:"client_id"` + RedirectURI string `json:"redirect_uri" binding:"required" url:"redirect_uri"` + State string `json:"state" url:"state"` + Nonce string `json:"nonce" url:"nonce"` + CodeChallenge string `json:"code_challenge" url:"code_challenge"` + CodeChallengeMethod string `json:"code_challenge_method" url:"code_challenge_method"` } type OIDCServiceConfig struct {