aboutsummaryrefslogtreecommitdiffstats
path: root/walrss
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 /walrss
parentf38b2c15ae796ade9f9779974dc5c237597b0829 (diff)
Implement OIDC role-based authorisation
Diffstat (limited to 'walrss')
-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
4 files changed, 115 insertions, 50 deletions
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"`
}