Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ type Node struct {
}

type Auth struct {
Driver string `env:"DRIVER"`
JWTSecret string `env:"JWT_SECRET"`
JWKS claim.JWKS `env:"JWKS_URL"`
TokenLife time.Duration `env:"TOKEN_LIFE" envDefault:"24h"`
Basic BasicAuth `envPrefix:"BASIC_"`
Clerk ClerkAuth `envPrefix:"CLERK_"`
Driver string `env:"DRIVER"`
JWTSecret string `env:"JWT_SECRET"`
JWKS claim.JWKS `env:"JWKS_URL"`
TokenLife time.Duration `env:"TOKEN_LIFE" envDefault:"24h"`
AllowedRedirectHosts []string `env:"ALLOWED_REDIRECT_HOSTS" envSeparator:","`
Basic BasicAuth `envPrefix:"BASIC_"`
Clerk ClerkAuth `envPrefix:"CLERK_"`
}

type BasicAuth struct {
Expand Down
69 changes: 64 additions & 5 deletions internal/http/controllers/v1/management/auth.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package v1

import (
"bytes"
"errors"
"io"
"net/http"
"net/url"
"strings"

"github.com/jmoiron/sqlx"
"github.com/lunogram/platform/internal/config"
Expand All @@ -14,6 +19,8 @@ import (
"go.uber.org/zap"
)

var ErrInvalidRedirect = errors.New("redirect host not allowed")

func NewAuthController(logger *zap.Logger, db *sqlx.DB, cfg config.Node, engine *rbac.Engine) (*AuthController, error) {
stores := management.NewState(db)

Expand All @@ -23,14 +30,16 @@ func NewAuthController(logger *zap.Logger, db *sqlx.DB, cfg config.Node, engine
}

return &AuthController{
logger: logger,
provider: provider,
logger: logger,
provider: provider,
allowedRedirectHosts: cfg.Auth.AllowedRedirectHosts,
}, nil
}

type AuthController struct {
logger *zap.Logger
provider providers.Provider
logger *zap.Logger
provider providers.Provider
allowedRedirectHosts []string
}

func (c *AuthController) GetAuthMethods(w http.ResponseWriter, r *http.Request) {
Expand All @@ -43,8 +52,31 @@ func (c *AuthController) AuthCallback(w http.ResponseWriter, r *http.Request, dr
return
}

// Decode the body once to validate the redirect parameter, then re-encode
// it so the provider can read it without consuming the body a second time.
var req oapi.AuthCallbackRequest
if err := json.Decode(r.Body, &req); err != nil {
oapi.WriteProblem(w, problem.ErrBadRequest(problem.Describe("failed to read request body")))
return
}

if req.Redirect != nil {
if err := c.validateRedirect(*req.Redirect); err != nil {
oapi.WriteProblem(w, problem.ErrBadRequest(problem.Describe("invalid redirect URL")))
return
}
}

encoded, err := json.Marshal(req)
if err != nil {
oapi.WriteProblem(w, problem.ErrInternal(problem.Describe("authentication failed")))
return
}

r.Body = io.NopCloser(bytes.NewReader(encoded))

ctx := r.Context()
_, err := c.provider.Authenticate(ctx, w, r)
_, err = c.provider.Authenticate(ctx, w, r)
if err != nil {
c.logger.Error("auth validation failed", zap.String("driver", string(driver)), zap.Error(err))
c.writeAuthError(w, err)
Expand All @@ -54,6 +86,33 @@ func (c *AuthController) AuthCallback(w http.ResponseWriter, r *http.Request, dr
w.WriteHeader(http.StatusOK)
}

// validateRedirect checks that the redirect URL is safe to redirect to.
// Relative URLs (starting with "/" but not "//") are always allowed.
// Absolute URLs are only allowed if their hostname appears in the
// configured AllowedRedirectHosts list.
func (c *AuthController) validateRedirect(redirect string) error {
// Allow relative URLs that start with "/" but not "//" (protocol-relative).
// Protocol-relative URLs such as "//evil.com/path" are treated as absolute
// by browsers and would redirect to an external host.
if strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") {
return nil
}

parsed, err := url.Parse(redirect)
if err != nil {
return ErrInvalidRedirect
}

host := parsed.Hostname()
for _, allowed := range c.allowedRedirectHosts {
if host == allowed {
return nil
}
}

return ErrInvalidRedirect
}

func (c *AuthController) AuthWebhook(w http.ResponseWriter, r *http.Request, driver oapi.AuthWebhookParamsDriver) {
if string(driver) != c.provider.Driver() {
oapi.WriteProblem(w, problem.ErrNotFound(problem.Describe("auth driver not found")))
Expand Down
117 changes: 117 additions & 0 deletions internal/http/controllers/v1/management/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package v1
import (
"encoding/json"
"net/http/httptest"
"strings"
"testing"

"github.com/lunogram/platform/internal/config"
Expand Down Expand Up @@ -140,3 +141,119 @@ func TestAuthWebhookWithInvalidDriver(t *testing.T) {
})
}
}

func TestValidateRedirect(t *testing.T) {
t.Parallel()

type test struct {
redirect string
allowedRedirectHosts []string
wantErr bool
}

tests := map[string]test{
"relative path is always allowed": {
redirect: "/dashboard",
allowedRedirectHosts: nil,
wantErr: false,
},
"relative root is always allowed": {
redirect: "/",
allowedRedirectHosts: nil,
wantErr: false,
},
"protocol-relative url is rejected": {
redirect: "//evil.com/steal",
allowedRedirectHosts: nil,
wantErr: true,
},
"absolute url with allowed host": {
redirect: "https://app.example.com/dashboard",
allowedRedirectHosts: []string{"app.example.com"},
wantErr: false,
},
"absolute url with disallowed host": {
redirect: "https://evil.example.com/steal",
allowedRedirectHosts: []string{"app.example.com"},
wantErr: true,
},
"absolute url with no allowed hosts configured": {
redirect: "https://app.example.com/dashboard",
allowedRedirectHosts: nil,
wantErr: true,
},
"absolute url host must match exactly": {
redirect: "https://notapp.example.com/path",
allowedRedirectHosts: []string{"app.example.com"},
wantErr: true,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
c := &AuthController{
allowedRedirectHosts: test.allowedRedirectHosts,
}
err := c.validateRedirect(test.redirect)
if test.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

func TestAuthCallbackRejectsDisallowedRedirect(t *testing.T) {
t.Parallel()

logger := zaptest.NewLogger(t)
mgmt, _, _ := teststore.RunPostgreSQL(t)

cfg := config.Node{
Auth: config.Auth{
Driver: "clerk",
Clerk: config.ClerkAuth{
SecretKey: "sk_test_xxx",
},
AllowedRedirectHosts: []string{"app.example.com"},
},
}

controller, err := NewAuthController(logger, mgmt, cfg, nil)
require.NoError(t, err)

type test struct {
redirect string
code int
}

tests := map[string]test{
"disallowed absolute redirect returns 400": {
redirect: "https://evil.com/steal",
code: 400,
},
"allowed absolute redirect passes redirect validation": {
redirect: "https://app.example.com/dashboard",
code: 401,
},
"relative redirect always passes": {
redirect: "/dashboard",
code: 401,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
body := strings.NewReader(`{"redirect":"` + test.redirect + `"}`)
res := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/v1/auth/callback/clerk", body)
req.Header.Set("Content-Type", "application/json")
controller.AuthCallback(res, req, oapi.AuthCallbackParamsDriverClerk)

require.Equal(t, test.code, res.Code, res.Body.String())
})
}
}
8 changes: 8 additions & 0 deletions internal/wasm/test/action/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
8 changes: 8 additions & 0 deletions internal/wasm/test/provider/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
8 changes: 8 additions & 0 deletions modules/actions/webhook/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
8 changes: 8 additions & 0 deletions modules/providers/logger/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
8 changes: 8 additions & 0 deletions modules/providers/twilio/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=