Compare commits

...

2 Commits

Author SHA1 Message Date
Stavros a736c4ad90 fix: add request param to authorize post 2026-04-27 20:14:36 +03:00
Stavros 74901dd88f feat: support for authorize post in oidc controller 2026-04-27 20:13:14 +03:00
6 changed files with 75 additions and 13 deletions
+10
View File
@@ -57,6 +57,16 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/robots.txt/, ""), 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, allowedHosts: true,
}, },
+1 -1
View File
@@ -87,7 +87,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
oauthController.SetupRoutes() oauthController.SetupRoutes()
oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{}, app.services.oidcService, apiRouter) oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{}, app.services.oidcService, apiRouter, engine)
oidcController.SetupRoutes() oidcController.SetupRoutes()
+17 -1
View File
@@ -3,6 +3,7 @@ package controller
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"slices" "slices"
"strings" "strings"
@@ -21,6 +22,7 @@ type OIDCController struct {
config OIDCControllerConfig config OIDCControllerConfig
router *gin.RouterGroup router *gin.RouterGroup
oidc *service.OIDCService oidc *service.OIDCService
engine *gin.Engine
} }
type AuthorizeCallback struct { type AuthorizeCallback struct {
@@ -57,11 +59,12 @@ type ClientCredentials struct {
ClientSecret string 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{ return &OIDCController{
config: config, config: config,
oidc: oidcService, oidc: oidcService,
router: router, router: router,
engine: engine,
} }
} }
@@ -72,6 +75,7 @@ func (controller *OIDCController) SetupRoutes() {
oidcGroup.POST("/token", controller.Token) oidcGroup.POST("/token", controller.Token)
oidcGroup.GET("/userinfo", controller.Userinfo) oidcGroup.GET("/userinfo", controller.Userinfo)
oidcGroup.POST("/userinfo", controller.Userinfo) oidcGroup.POST("/userinfo", controller.Userinfo)
controller.engine.POST("/authorize", controller.AuthorizePseudoPost)
} }
func (controller *OIDCController) GetClientInfo(c *gin.Context) { 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) { func (controller *OIDCController) Token(c *gin.Context) {
if !controller.oidc.IsConfigured() { if !controller.oidc.IsConfigured() {
tlog.App.Warn().Msg("OIDC not configured") tlog.App.Warn().Msg("OIDC not configured")
+31 -3
View File
@@ -12,14 +12,14 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/go-querystring/query" "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/bootstrap"
"github.com/tinyauthapp/tinyauth/internal/config" "github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog" "github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestOIDCController(t *testing.T) { func TestOIDCController(t *testing.T) {
@@ -846,6 +846,34 @@ func TestOIDCController(t *testing.T) {
assert.Equal(t, "invalid_grant", res["error"]) 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{}) app := bootstrap.NewBootstrapApp(config.Config{})
@@ -869,7 +897,7 @@ func TestOIDCController(t *testing.T) {
group := router.Group("/api") group := router.Group("/api")
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
oidcController := controller.NewOIDCController(controllerCfg, oidcService, group) oidcController := controller.NewOIDCController(controllerCfg, oidcService, group, router)
oidcController.SetupRoutes() oidcController.SetupRoutes()
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
+7
View File
@@ -39,6 +39,7 @@ func (m *UIMiddleware) Init() error {
func (m *UIMiddleware) Middleware() gin.HandlerFunc { func (m *UIMiddleware) Middleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
path := strings.TrimPrefix(c.Request.URL.Path, "/") path := strings.TrimPrefix(c.Request.URL.Path, "/")
method := c.Request.Method
tlog.App.Debug().Str("path", path).Msg("path") 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")) c.Writer.Write([]byte("User-agent: *\nDisallow: /\n"))
return return
default: 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) _, err := fs.Stat(m.uiFs, path)
// Enough for one authentication flow // Enough for one authentication flow
+9 -8
View File
@@ -100,14 +100,15 @@ type TokenResponse struct {
} }
type AuthorizeRequest struct { type AuthorizeRequest struct {
Scope string `json:"scope" binding:"required"` Scope string `json:"scope" binding:"required" url:"scope"`
ResponseType string `json:"response_type" binding:"required"` ResponseType string `json:"response_type" binding:"required" url:"response_type"`
ClientID string `json:"client_id" binding:"required"` ClientID string `json:"client_id" binding:"required" url:"client_id"`
RedirectURI string `json:"redirect_uri" binding:"required"` RedirectURI string `json:"redirect_uri" binding:"required" url:"redirect_uri"`
State string `json:"state"` State string `json:"state" url:"state"`
Nonce string `json:"nonce"` Nonce string `json:"nonce" url:"nonce"`
CodeChallenge string `json:"code_challenge"` CodeChallenge string `json:"code_challenge" url:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method"` CodeChallengeMethod string `json:"code_challenge_method" url:"code_challenge_method"`
Request string `json:"request" url:"request"`
} }
type OIDCServiceConfig struct { type OIDCServiceConfig struct {