2018-03-31 06:42:54 +03:00
|
|
|
// A GIN middleware providing low-fi security for sites with simple needs.
|
|
|
|
//
|
|
|
|
// Redirects users to a login page until they provide a secret code.
|
|
|
|
// No CSRF protection, so any js on the web can log you
|
|
|
|
// out (or in, if they know the password).
|
|
|
|
//
|
|
|
|
// Protects you from brute-force attacks by making all login attempts
|
|
|
|
// take 1 second (configurable) and serializing them through a mutex.
|
|
|
|
//
|
|
|
|
// Scripts can send `Authorization: <secret code>` instead of
|
|
|
|
// having to keep a cookie jar.
|
|
|
|
//
|
2018-01-21 04:09:16 +03:00
|
|
|
package gin_teeny_security
|
|
|
|
|
|
|
|
import "github.com/gin-gonic/gin"
|
|
|
|
import "github.com/gin-contrib/sessions"
|
|
|
|
import "net/http"
|
2018-01-22 13:07:50 +03:00
|
|
|
import "net/url"
|
|
|
|
import "io"
|
2018-02-15 12:10:27 +03:00
|
|
|
import "time"
|
|
|
|
import "sync"
|
2018-01-22 13:07:50 +03:00
|
|
|
import "html/template"
|
2018-01-21 04:09:16 +03:00
|
|
|
|
2018-03-31 06:42:54 +03:00
|
|
|
// Convenient entry-point for those using gin-sessions and
|
|
|
|
// not wanting to override anything.
|
2018-01-21 04:09:16 +03:00
|
|
|
func RequiresSecretAccessCode(secretAccessCode, path string) gin.HandlerFunc {
|
2018-01-22 13:07:50 +03:00
|
|
|
cfg := &Config{
|
|
|
|
Path: path,
|
|
|
|
Secret: secretAccessCode,
|
|
|
|
}
|
|
|
|
|
|
|
|
return cfg.Middleware
|
|
|
|
}
|
|
|
|
|
2018-03-31 06:42:54 +03:00
|
|
|
// Main entry point
|
2018-01-22 13:07:50 +03:00
|
|
|
type Config struct {
|
2018-03-31 06:42:54 +03:00
|
|
|
Path string // defaults to 'login'
|
|
|
|
Secret string // the password
|
|
|
|
RequireAuth func(*gin.Context) bool // defaults to always requiring auth if unset; override to allow some public access.
|
|
|
|
Template *template.Template // Markup for the login page
|
|
|
|
SaveKeyToSession func(*gin.Context, string) // Override to use something other than gin-sessions
|
|
|
|
GetKeyFromSession func(*gin.Context) string // Override to use something other than gin-sessions
|
|
|
|
|
|
|
|
LoginAttemptSlowdown time.Duration // Increase to slow-down attempts to brute force your password.
|
2018-02-15 12:10:27 +03:00
|
|
|
mutex sync.Mutex
|
2018-01-22 13:07:50 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c Config) saveKey(ctx *gin.Context, k string) {
|
|
|
|
if c.SaveKeyToSession == nil {
|
|
|
|
c.SaveKeyToSession = DefaultSetSession
|
|
|
|
}
|
|
|
|
c.SaveKeyToSession(ctx, k)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Config) getKey(ctx *gin.Context) string {
|
|
|
|
if c.GetKeyFromSession == nil {
|
|
|
|
c.GetKeyFromSession = DefaultGetSession
|
|
|
|
}
|
|
|
|
return c.GetKeyFromSession(ctx)
|
|
|
|
}
|
|
|
|
|
2018-03-31 06:42:54 +03:00
|
|
|
// Saves your login status using gin-sessions
|
2018-01-22 13:07:50 +03:00
|
|
|
func DefaultSetSession(c *gin.Context, secret string) {
|
|
|
|
session := sessions.Default(c)
|
|
|
|
session.Set("secretAccessCode", secret)
|
|
|
|
session.Save()
|
|
|
|
}
|
|
|
|
|
2018-03-31 06:42:54 +03:00
|
|
|
// Gets your login status from gin-sessions
|
2018-01-22 13:07:50 +03:00
|
|
|
func DefaultGetSession(c *gin.Context) string {
|
|
|
|
session := sessions.Default(c)
|
|
|
|
str, ok := session.Get("secretAccessCode").(string)
|
|
|
|
if !ok {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Config) path() string {
|
|
|
|
if c.Path == "" {
|
|
|
|
return "/login/"
|
|
|
|
}
|
|
|
|
return c.Path
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Config) requireAuth(ctx *gin.Context) bool {
|
2018-03-31 06:42:54 +03:00
|
|
|
if ctx.Request.Header.Get("Authorization") != "" {
|
|
|
|
// Slow down brute-force attempts.
|
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
|
|
|
time.Sleep(c.loginSlowdown())
|
|
|
|
}
|
2018-01-22 13:07:50 +03:00
|
|
|
if ctx.Request.Header.Get("Authorization") == c.Secret {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return c.RequireAuth == nil || c.RequireAuth(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Config) template() *template.Template {
|
|
|
|
if c.Template == nil {
|
|
|
|
return DEFAULT_LOGIN_PAGE
|
|
|
|
}
|
|
|
|
return c.Template
|
|
|
|
}
|
|
|
|
|
2018-02-15 12:10:27 +03:00
|
|
|
func (c Config) loginSlowdown() time.Duration {
|
|
|
|
if c.LoginAttemptSlowdown == 0 {
|
|
|
|
return time.Second
|
|
|
|
}
|
|
|
|
return c.LoginAttemptSlowdown
|
|
|
|
}
|
|
|
|
|
2018-01-22 13:07:50 +03:00
|
|
|
func (c Config) ExecTemplate(w io.Writer, message, returnUrl string) error {
|
|
|
|
return c.template().Execute(w, LoginPageParams{
|
|
|
|
Message: message,
|
|
|
|
Path: c.path() + "?" + url.Values{"return": []string{returnUrl}}.Encode(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
type LoginPageParams struct {
|
|
|
|
Message string
|
|
|
|
Path string
|
|
|
|
}
|
|
|
|
|
|
|
|
var DEFAULT_LOGIN_PAGE = template.Must(template.New("login").Parse(`
|
|
|
|
<h1>Login</h1>
|
|
|
|
{{ if .Message }}<h2>{{ .Message }}</h2>{{ end }}
|
|
|
|
<form action="{{.Path}}" method="POST">
|
2018-02-15 12:10:27 +03:00
|
|
|
<input type="password" name="secretAccessCode" />
|
2018-01-22 13:07:50 +03:00
|
|
|
<input type="submit" value="Login" />
|
|
|
|
</form>
|
2018-03-31 06:42:54 +03:00
|
|
|
|
|
|
|
<div style="display: none">
|
|
|
|
CURL users: try setting -H 'Authorization: <your secret>'
|
|
|
|
</div>
|
2018-01-22 13:07:50 +03:00
|
|
|
`))
|
|
|
|
|
|
|
|
func (cfg *Config) Middleware(c *gin.Context) {
|
|
|
|
if c.Request.URL.Path == cfg.path() {
|
|
|
|
returnTo := c.Request.URL.Query().Get("return")
|
|
|
|
if returnTo == "" {
|
|
|
|
returnTo = "/"
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Request.Method == "POST" {
|
2018-02-15 12:10:27 +03:00
|
|
|
// slow down brute-force attacks
|
|
|
|
cfg.mutex.Lock()
|
|
|
|
defer cfg.mutex.Unlock()
|
|
|
|
time.Sleep(cfg.loginSlowdown())
|
|
|
|
|
2018-01-22 13:07:50 +03:00
|
|
|
c.Request.ParseForm()
|
|
|
|
|
|
|
|
if c.Request.PostForm.Get("secretAccessCode") == cfg.Secret {
|
|
|
|
c.Header("Location", returnTo)
|
|
|
|
cfg.saveKey(c, cfg.Secret)
|
|
|
|
|
|
|
|
c.AbortWithStatus(http.StatusFound)
|
2018-01-21 04:09:16 +03:00
|
|
|
return
|
|
|
|
} else {
|
2018-01-22 13:07:50 +03:00
|
|
|
cfg.saveKey(c, "")
|
|
|
|
c.Writer.WriteHeader(http.StatusForbidden)
|
|
|
|
cfg.ExecTemplate(c.Writer, "Wrong Password", returnTo)
|
|
|
|
c.Abort()
|
2018-01-21 04:09:16 +03:00
|
|
|
return
|
|
|
|
}
|
2018-01-22 13:07:50 +03:00
|
|
|
} else if c.Request.Method == "GET" {
|
|
|
|
cfg.ExecTemplate(c.Writer, "", returnTo)
|
|
|
|
c.Abort()
|
|
|
|
return
|
2018-01-21 04:09:16 +03:00
|
|
|
} else {
|
|
|
|
c.Next()
|
2018-01-22 13:07:50 +03:00
|
|
|
return
|
2018-01-21 04:09:16 +03:00
|
|
|
}
|
|
|
|
}
|
2018-01-22 13:07:50 +03:00
|
|
|
|
|
|
|
v := cfg.getKey(c)
|
|
|
|
if cfg.requireAuth(c) && (v != cfg.Secret) {
|
|
|
|
c.Header("Location", cfg.Path+"?"+url.Values{"return": []string{c.Request.URL.RequestURI()}}.Encode())
|
|
|
|
c.AbortWithStatus(http.StatusTemporaryRedirect)
|
|
|
|
} else {
|
|
|
|
c.Next()
|
|
|
|
}
|
2018-01-21 04:09:16 +03:00
|
|
|
}
|