aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.net>2025-08-09 18:10:00 +0100
committerLeonardo Bishop <me@leonardobishop.net>2025-08-09 18:10:00 +0100
commit399a3ef4329ce3d22fcd39402faeefbae18bb58b (patch)
tree09f27f512644dac3d6bb3d49066097fcb01ab8bc
parentf38b2c15ae796ade9f9779974dc5c237597b0829 (diff)
Implement OIDC role-based authorisation
-rw-r--r--go.mod3
-rw-r--r--go.sum7
-rw-r--r--walrss/internal/http/auth.go80
-rw-r--r--walrss/internal/http/views/signin.qtpl.html31
-rw-r--r--walrss/internal/http/views/signin.qtpl.html.go43
-rw-r--r--walrss/internal/state/state.go11
6 files changed, 125 insertions, 50 deletions
diff --git a/go.mod b/go.mod
index f0ce0f7..7768f8a 100644
--- a/go.mod
+++ b/go.mod
@@ -55,6 +55,9 @@ require (
github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
+ github.com/tidwall/gjson v1.18.0 // indirect
+ github.com/tidwall/match v1.1.1 // indirect
+ github.com/tidwall/pretty v1.2.1 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index ef69338..5fdc293 100644
--- a/go.sum
+++ b/go.sum
@@ -110,6 +110,13 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.1.7 h1:biOoh5dov69hQPBlaRsXSHoEOIEnCxFzQvUmbscSNJI=
diff --git a/walrss/internal/http/auth.go b/walrss/internal/http/auth.go
index af098fa..ffd069f 100644
--- a/walrss/internal/http/auth.go
+++ b/walrss/internal/http/auth.go
@@ -2,20 +2,25 @@ package http
import (
"context"
+ "encoding/base64"
"errors"
+ "fmt"
+ "math/rand"
+ "strings"
+ "sync"
+ "time"
+
"github.com/codemicro/walrss/walrss/internal/core"
"github.com/codemicro/walrss/walrss/internal/http/views"
"github.com/codemicro/walrss/walrss/internal/urls"
"github.com/gofiber/fiber/v2"
"github.com/stevelacy/daz"
- "math/rand"
- "sync"
- "time"
+ "github.com/tidwall/gjson"
)
func (s *Server) authRegister(ctx *fiber.Ctx) error {
- if s.state.Config.Platform.DisableRegistration {
+ if s.state.Config.Platform.DisableRegistration || s.state.Config.Platform.DisableBasicAuth {
ctx.Status(fiber.StatusForbidden)
return views.SendPage(ctx, &views.PolyPage{
TitleString: "Site registration disabled",
@@ -77,8 +82,9 @@ success:
func (s *Server) authSignIn(ctx *fiber.Ctx) error {
page := &views.SignInPage{
- Problem: ctx.Query("problem"),
- OIDCEnabled: s.state.Config.OIDC.Enable,
+ Problem: ctx.Query("problem"),
+ OIDCEnabled: s.state.Config.OIDC.Enable,
+ BasicAuthEnabled: !s.state.Config.Platform.DisableBasicAuth,
}
if getCurrentUserID(ctx) != "" {
@@ -86,6 +92,10 @@ func (s *Server) authSignIn(ctx *fiber.Ctx) error {
}
if ctx.Method() == fiber.MethodPost {
+ if s.state.Config.Platform.DisableBasicAuth {
+ goto basicAuthIsDisabled
+ }
+
email := ctx.FormValue("email")
ok, err := core.AreUserCredentialsCorrect(
@@ -129,7 +139,12 @@ success:
)
incorrectUsernameOrPassword:
ctx.Status(fiber.StatusUnauthorized)
- return views.SendPage(ctx, &views.SignInPage{Problem: "Incorrect username or password"})
+ page.Problem = "Incorrect username or password"
+ return views.SendPage(ctx, page)
+basicAuthIsDisabled:
+ ctx.Status(fiber.StatusForbidden)
+ page.Problem = "Basic authentication is disabled"
+ return views.SendPage(ctx, page)
}
var (
@@ -199,25 +214,50 @@ func (s *Server) authOIDCCallback(ctx *fiber.Ctx) error {
return errors.New("missing ID token")
}
- idToken, err := s.oidcVerifier.Verify(context.Background(), rawIDToken)
+ _, err = s.oidcVerifier.Verify(context.Background(), rawIDToken)
if err != nil {
return err
}
- var claims struct {
- Email string `json:"email"`
- }
- if err := idToken.Claims(&claims); err != nil {
+ claims, err := getRawClaims(rawIDToken)
+ if err != nil {
return err
}
- user, err := core.GetUserByEmail(s.state, claims.Email)
+ emailClaim := gjson.Get(claims, "email")
+ if !emailClaim.Exists() {
+ return core.NewUserError("Email is missing from claims")
+ }
+ email := emailClaim.Str
+
+ if s.state.Config.OIDC.LoginFilter != "" {
+ rolesClaim := gjson.Get(claims, s.state.Config.OIDC.LoginFilter)
+ if !rolesClaim.Exists() {
+ return core.NewUserError("Cannot verify authorisation as '%s' is missing from claims", s.state.Config.OIDC.LoginFilter)
+ }
+ roles := rolesClaim.Array()
+ var authorisation bool
+ out:
+ for _, allowedRole := range s.state.Config.OIDC.LoginFilterAllowedValues {
+ for _, role := range roles {
+ if role.Str == allowedRole {
+ authorisation = true
+ break out
+ }
+ }
+ }
+ if !authorisation {
+ return core.NewUserError("You are not authorised to use this service")
+ }
+ }
+
+ user, err := core.GetUserByEmail(s.state, email)
if err != nil {
if errors.Is(err, core.ErrNotFound) {
if s.state.Config.Platform.DisableRegistration {
return core.NewUserError("Cannot register user on-demand as registrations are disabled.")
}
- user, err = core.RegisterUserOIDC(s.state, claims.Email)
+ user, err = core.RegisterUserOIDC(s.state, email)
if err != nil {
return err
}
@@ -238,3 +278,15 @@ func (s *Server) authOIDCCallback(ctx *fiber.Ctx) error {
return ctx.Redirect(urls.Index)
}
+
+func getRawClaims(p string) (string, error) {
+ parts := strings.Split(p, ".")
+ if len(parts) < 2 {
+ return "", fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
+ }
+ payload, err := base64.RawURLEncoding.DecodeString(parts[1])
+ if err != nil {
+ return "", fmt.Errorf("oidc: malformed jwt payload: %v", err)
+ }
+ return string(payload[:]), nil
+}
diff --git a/walrss/internal/http/views/signin.qtpl.html b/walrss/internal/http/views/signin.qtpl.html
index b0c7d4a..693a53b 100644
--- a/walrss/internal/http/views/signin.qtpl.html
+++ b/walrss/internal/http/views/signin.qtpl.html
@@ -4,6 +4,7 @@
BasePage
Problem string
OIDCEnabled bool
+ BasicAuthEnabled bool
} %}
{% func (p *SignInPage) Title() %}Sign in{% endfunc %}
@@ -17,20 +18,22 @@
{%= ProblemBox(p.Problem) %}
{% endif %}
- <form action="" method="post">
- <div class="mb-3">
- <label for="emailInput" class="form-label">Email address</label>
- <input type="email" class="form-control" id="emailInput" name="email">
- </div>
- <div class="mb-3">
- <label for="passwordInput" class="form-label">Password</label>
- <input type="password" class="form-control" id="passwordInput" name="password">
- </div>
- <button type="submit" class="btn btn-primary">Submit</button>
- </form>
- <br>
- <a href="{%s= urls.AuthRegister %}">No account? Click here to register</a>
- <br>
+ {% if p.BasicAuthEnabled %}
+ <form action="" method="post">
+ <div class="mb-3">
+ <label for="emailInput" class="form-label">Email address</label>
+ <input type="email" class="form-control" id="emailInput" name="email">
+ </div>
+ <div class="mb-3">
+ <label for="passwordInput" class="form-label">Password</label>
+ <input type="password" class="form-control" id="passwordInput" name="password">
+ </div>
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </form>
+ <br>
+ <a href="{%s= urls.AuthRegister %}">No account? Click here to register</a>
+ <br>
+ {% endif %}
{% if p.OIDCEnabled %}
<a href="{%s= urls.AuthOIDCOutbound %}">Click here to login with OIDC</a>
{% endif %}
diff --git a/walrss/internal/http/views/signin.qtpl.html.go b/walrss/internal/http/views/signin.qtpl.html.go
index 35e9fb7..2fcf366 100644
--- a/walrss/internal/http/views/signin.qtpl.html.go
+++ b/walrss/internal/http/views/signin.qtpl.html.go
@@ -18,8 +18,9 @@ var (
type SignInPage struct {
BasePage
- Problem string
- OIDCEnabled bool
+ Problem string
+ OIDCEnabled bool
+ BasicAuthEnabled bool
}
func (p *SignInPage) StreamTitle(qw422016 *qt422016.Writer) {
@@ -59,22 +60,28 @@ func (p *SignInPage) StreamBody(qw422016 *qt422016.Writer) {
}
qw422016.N().S(`
- <form action="" method="post">
- <div class="mb-3">
- <label for="emailInput" class="form-label">Email address</label>
- <input type="email" class="form-control" id="emailInput" name="email">
- </div>
- <div class="mb-3">
- <label for="passwordInput" class="form-label">Password</label>
- <input type="password" class="form-control" id="passwordInput" name="password">
- </div>
- <button type="submit" class="btn btn-primary">Submit</button>
- </form>
- <br>
- <a href="`)
- qw422016.N().S(urls.AuthRegister)
- qw422016.N().S(`">No account? Click here to register</a>
- <br>
+ `)
+ if p.BasicAuthEnabled {
+ qw422016.N().S(`
+ <form action="" method="post">
+ <div class="mb-3">
+ <label for="emailInput" class="form-label">Email address</label>
+ <input type="email" class="form-control" id="emailInput" name="email">
+ </div>
+ <div class="mb-3">
+ <label for="passwordInput" class="form-label">Password</label>
+ <input type="password" class="form-control" id="passwordInput" name="password">
+ </div>
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </form>
+ <br>
+ <a href="`)
+ qw422016.N().S(urls.AuthRegister)
+ qw422016.N().S(`">No account? Click here to register</a>
+ <br>
+ `)
+ }
+ qw422016.N().S(`
`)
if p.OIDCEnabled {
qw422016.N().S(`
diff --git a/walrss/internal/state/state.go b/walrss/internal/state/state.go
index 1fe33ee..8678fda 100644
--- a/walrss/internal/state/state.go
+++ b/walrss/internal/state/state.go
@@ -36,15 +36,18 @@ type Config struct {
ExternalURL string `fig:"externalURL" validate:"required"`
}
Platform struct {
+ DisableBasicAuth bool `fig:"disableBasicAuth"`
DisableRegistration bool `fig:"disableRegistration"`
DisableSecureCookies bool `fig:"disableSecureCookies"`
ContactInformation string `fig:"contactInformation"`
}
OIDC struct {
- Enable bool `fig:"enable"`
- ClientID string `fig:"clientID"`
- ClientSecret string `fig:"clientSecret"`
- Issuer string `fig:"issuer"`
+ Enable bool `fig:"enable"`
+ ClientID string `fig:"clientID"`
+ ClientSecret string `fig:"clientSecret"`
+ Issuer string `fig:"issuer"`
+ LoginFilter string `fig:"loginFilter"`
+ LoginFilterAllowedValues []string `fig:"loginFilterAllowedValues"`
}
Debug bool `fig:"debug"`
}