diff options
| author | Leonardo Bishop <me@leonardobishop.net> | 2025-08-09 18:10:00 +0100 |
|---|---|---|
| committer | Leonardo Bishop <me@leonardobishop.net> | 2025-08-09 18:10:00 +0100 |
| commit | 399a3ef4329ce3d22fcd39402faeefbae18bb58b (patch) | |
| tree | 09f27f512644dac3d6bb3d49066097fcb01ab8bc /walrss | |
| parent | f38b2c15ae796ade9f9779974dc5c237597b0829 (diff) | |
Implement OIDC role-based authorisation
Diffstat (limited to 'walrss')
| -rw-r--r-- | walrss/internal/http/auth.go | 80 | ||||
| -rw-r--r-- | walrss/internal/http/views/signin.qtpl.html | 31 | ||||
| -rw-r--r-- | walrss/internal/http/views/signin.qtpl.html.go | 43 | ||||
| -rw-r--r-- | walrss/internal/state/state.go | 11 |
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"` } |
