Compare commits
2 commits
99def55e80
...
c98fe735ad
| Author | SHA1 | Date | |
|---|---|---|---|
| c98fe735ad | |||
| 1e1888625d |
15 changed files with 80 additions and 2 deletions
27
altcha.go
Normal file
27
altcha.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
goaltcha "github.com/k42-software/go-altcha"
|
||||
altchahttp "github.com/k42-software/go-altcha/http"
|
||||
)
|
||||
|
||||
func serveAltchaJS(w http.ResponseWriter, r *http.Request) {
|
||||
altchahttp.ServeJavascript(w, r)
|
||||
}
|
||||
|
||||
func serveAltchaChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
challenge := goaltcha.NewChallenge()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "private, no-cache, no-store, must-revalidate")
|
||||
_, _ = w.Write([]byte(challenge.Encode()))
|
||||
}
|
||||
|
||||
func validateAltcha(r *http.Request) bool {
|
||||
encoded := r.PostFormValue("altcha")
|
||||
if encoded == "" {
|
||||
return false
|
||||
}
|
||||
return goaltcha.ValidateResponse(encoded, true)
|
||||
}
|
||||
|
|
@ -53,6 +53,12 @@ func changePassword(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !validateAltcha(r) {
|
||||
csrfToken, _ := setCSRFToken(w)
|
||||
displayTmplError(w, http.StatusForbidden, "change.html", map[string]interface{}{"error": "Invalid or missing altcha response. Please try again.", "csrf_token": csrfToken})
|
||||
return
|
||||
}
|
||||
|
||||
renderError := func(status int, msg string) {
|
||||
csrfToken, _ := setCSRFToken(w)
|
||||
displayTmplError(w, status, "change.html", map[string]interface{}{"error": msg, "csrf_token": csrfToken})
|
||||
|
|
|
|||
1
csrf.go
1
csrf.go
|
|
@ -25,6 +25,7 @@ func setCSRFToken(w http.ResponseWriter) (string, error) {
|
|||
Path: "/",
|
||||
HttpOnly: false, // must be readable via form hidden field comparison
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: !devMode,
|
||||
})
|
||||
return token, nil
|
||||
}
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -14,6 +14,8 @@ require (
|
|||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/k42-software/go-altcha v0.1.1
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
)
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -41,6 +41,10 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
|||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/k42-software/go-altcha v0.1.1 h1:vfA+0+0gr7jK4vp21Q7xvEpIjDsx8PqzxS0obgIToQs=
|
||||
github.com/k42-software/go-altcha v0.1.1/go.mod h1:2aX+0PkUSI0YPDVfjapZeuGELWt8ugEXkg8gr6QejMU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
|
|
|||
5
login.go
5
login.go
|
|
@ -53,6 +53,11 @@ func tryLogin(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !validateAltcha(r) {
|
||||
displayTmplError(w, http.StatusForbidden, "login.html", map[string]interface{}{"error": "Invalid or missing altcha response. Please try again."})
|
||||
return
|
||||
}
|
||||
|
||||
if entries, err := login(r.PostFormValue("login"), r.PostFormValue("password")); err != nil {
|
||||
log.Println(err)
|
||||
displayTmplError(w, http.StatusInternalServerError, "login.html", map[string]interface{}{"error": err.Error()})
|
||||
|
|
|
|||
5
lost.go
5
lost.go
|
|
@ -107,6 +107,11 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !validateAltcha(r) {
|
||||
displayTmplError(w, http.StatusForbidden, "lost.html", map[string]interface{}{"error": "Invalid or missing altcha response. Please try again."})
|
||||
return
|
||||
}
|
||||
|
||||
// Connect to the LDAP server
|
||||
conn, err := myLDAP.Connect()
|
||||
if err != nil || conn == nil {
|
||||
|
|
|
|||
8
main.go
8
main.go
|
|
@ -18,6 +18,7 @@ import (
|
|||
)
|
||||
|
||||
var myPublicURL = "https://ldap.nemunai.re"
|
||||
var devMode bool
|
||||
|
||||
// dockerRegistrySecret is required for X-Special-Auth anonymous access.
|
||||
// If empty, the feature is disabled.
|
||||
|
|
@ -80,9 +81,14 @@ func main() {
|
|||
var baseURL = flag.String("baseurl", "/", "URL prepended to each URL")
|
||||
var configfile = flag.String("config", "", "path to the configuration file")
|
||||
var publicURL = flag.String("public-url", myPublicURL, "Public base URL used in password reset emails")
|
||||
var dev = flag.Bool("dev", false, "Development mode: disables HSTS and cookie Secure flag for local HTTP testing")
|
||||
flag.Parse()
|
||||
|
||||
myPublicURL = *publicURL
|
||||
devMode = *dev
|
||||
if devMode {
|
||||
log.Println("WARNING: running in development mode — security features relaxed, do not use in production")
|
||||
}
|
||||
|
||||
// Sanitize options
|
||||
log.Println("Checking paths...")
|
||||
|
|
@ -213,6 +219,8 @@ func main() {
|
|||
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Register handlers
|
||||
http.HandleFunc(fmt.Sprintf("GET %s/altcha.min.js", *baseURL), serveAltchaJS)
|
||||
http.HandleFunc(fmt.Sprintf("GET %s/altcha-challenge", *baseURL), serveAltchaChallenge)
|
||||
http.HandleFunc(fmt.Sprintf("%s/{$}", *baseURL), changePassword)
|
||||
http.HandleFunc(fmt.Sprintf("POST %s/api/v1/aliases", *baseURL), addyAliasAPI)
|
||||
http.HandleFunc(fmt.Sprintf("DELETE %s/api/v1/aliases/{alias}", *baseURL), addyAliasAPIDelete)
|
||||
|
|
|
|||
5
reset.go
5
reset.go
|
|
@ -44,6 +44,11 @@ func resetPassword(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !validateAltcha(r) {
|
||||
renderError(http.StatusForbidden, "Invalid or missing altcha response. Please try again.")
|
||||
return
|
||||
}
|
||||
|
||||
// Check the two new passwords are identical
|
||||
if r.PostFormValue("newpassword") != r.PostFormValue("new2password") {
|
||||
renderError(http.StatusNotAcceptable, "New passwords are not identical. Please retry.")
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ func securityHeaders(next http.Handler) http.Handler {
|
|||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'unsafe-inline' https://stackpath.bootstrapcdn.com; style-src https://stackpath.bootstrapcdn.com; img-src 'self'; font-src https://stackpath.bootstrapcdn.com")
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' https://stackpath.bootstrapcdn.com; style-src https://stackpath.bootstrapcdn.com; img-src 'self'; font-src https://stackpath.bootstrapcdn.com")
|
||||
if !devMode {
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<altcha-widget challengeurl="altcha-challenge"></altcha-widget>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Change my password</button>
|
||||
<a href="/lost" class="btn btn-outline-secondary">Forgot your password?</a>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
|
||||
<title>nemunai.re password change</title>
|
||||
<script src="altcha.min.js" async defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
<div class="form-group">
|
||||
<input name="password" required="" class="form-control" id="input_1" type="password" placeholder="Current password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<altcha-widget challengeurl="altcha-challenge"></altcha-widget>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Sign in</button>
|
||||
<a href="/lost" class="btn btn-outline-secondary">Forgot your password?</a>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
<div class="form-group">
|
||||
<input name="login" required="" class="form-control" id="input_0" type="text" placeholder="Login" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<altcha-widget challengeurl="altcha-challenge"></altcha-widget>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Reset my password</button>
|
||||
<a href="/change" class="btn btn-outline-success">Just want to change your password?</a>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
<div class="form-group">
|
||||
<input name="new2password" required="" class="form-control" id="input_3" type="password" placeholder="Retype new password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<altcha-widget challengeurl="altcha-challenge"></altcha-widget>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Reset my password</button>
|
||||
</form>
|
||||
{{template "footer"}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue