jwt for webapp
This commit is contained in:
parent
7e77fd7d27
commit
9d332bbda4
5 changed files with 112 additions and 152 deletions
|
|
@ -31,11 +31,6 @@ func (db *DB) GetUser(username string) (u core.User, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (db *DB) UserIsAdmin(username string) bool {
|
||||
u, _ := db.GetUser(username)
|
||||
return u.Admin
|
||||
}
|
||||
|
||||
func (db *DB) UserExists(username string) error {
|
||||
return db.users.Get(username, nil)
|
||||
}
|
||||
|
|
@ -100,14 +95,14 @@ func (db *DB) CreateUser(username, password string, admin bool) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (db *DB) Login(username, password string) (err error) {
|
||||
var u core.User
|
||||
func (db *DB) Login(username, password string) (u core.User, err error) {
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked == 3 {
|
||||
return errors.New("db: user locked")
|
||||
err = errors.New("db: user locked")
|
||||
return
|
||||
}
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(u.Password), validatePassword(password)); err != nil {
|
||||
u.Locked++
|
||||
|
|
@ -115,7 +110,8 @@ func (db *DB) Login(username, password string) (err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
return errors.New("db: wrong login")
|
||||
err = errors.New("db: wrong login")
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
|
|
|
|||
|
|
@ -3,28 +3,42 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/jwt"
|
||||
"github.com/gorilla/mux"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) *Context {
|
||||
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Context) {
|
||||
h := w.Header()
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("X-XSS-Protection", "1; mode=block")
|
||||
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self' 'unsafe-inline';frame-ancestors 'none'")
|
||||
return &Context{
|
||||
ctx = &Context{
|
||||
Request: r,
|
||||
Response: w,
|
||||
Srv: s,
|
||||
Time: time.Now(),
|
||||
}
|
||||
if c, err := r.Cookie(cookieName); err == nil {
|
||||
t, err := jwt.DecodeToken(c.Value)
|
||||
if err != nil {
|
||||
log.Println("DecodeToken:", err)
|
||||
} else {
|
||||
if err = s.JWT.Verify(t); err != nil {
|
||||
log.Println("VerifyToken:", err)
|
||||
} else {
|
||||
ctx.Token = *t
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.SetCookie(nil)
|
||||
return
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
|
|
@ -36,6 +50,8 @@ type Context struct {
|
|||
Err error
|
||||
Time time.Time
|
||||
|
||||
Token jwt.Token
|
||||
|
||||
Data webData
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +80,8 @@ func (c *Context) Exec() {
|
|||
c.Err = errors.New("template is nil")
|
||||
return
|
||||
}
|
||||
c.newToken()
|
||||
c.Data.Token = newXsrf(c.Token.RawSig()[:keySize])
|
||||
|
||||
c.Data.Version = c.Srv.Version
|
||||
if c.Data.BodyTitle == "" {
|
||||
c.Data.BodyTitle = c.Data.Title
|
||||
|
|
@ -139,111 +156,54 @@ func (c *Context) Var(name string) (ret string) {
|
|||
return
|
||||
}
|
||||
|
||||
func (c *Context) initToken() {
|
||||
token := c.getToken()
|
||||
if token == nil || len(token) != keyLen {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
s.Values["token"] = genKey(keyLen)
|
||||
s.Options = c.Srv.Store.Options
|
||||
err := s.Save(c.Request, c.Response)
|
||||
if err != nil {
|
||||
log.Println("InitToken: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) newToken() {
|
||||
c.initToken()
|
||||
rnd := genKey(keyLen)
|
||||
c.Data.Token = base64.StdEncoding.EncodeToString(append(rnd, xor(rnd, c.getToken())...))
|
||||
}
|
||||
|
||||
// CheckToken validates the xsrf token
|
||||
func (c *Context) CheckToken(f func()) bool {
|
||||
token, err := base64.StdEncoding.DecodeString(c.Form("token"))
|
||||
if err != nil {
|
||||
log.Println("CheckToken: ", err)
|
||||
return false
|
||||
}
|
||||
if len(token) != authLen {
|
||||
return false
|
||||
}
|
||||
ok := subtle.ConstantTimeCompare(c.getToken(), xor(token[keyLen:], token[:keyLen])) == 1
|
||||
func (c *Context) CheckXsrf(f func()) (ok bool) {
|
||||
ok = checkXsrf(c.Form("token"), c.Token.RawSig()[:keySize])
|
||||
if !ok {
|
||||
if f != nil {
|
||||
f()
|
||||
return ok
|
||||
return
|
||||
}
|
||||
c.Data.Msg = "wrong csrf token"
|
||||
c.Exec()
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *Context) LoggedOn() (ok bool) {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
_, ok = s.Values["user"]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) SetUdata(user string, admin bool) {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
s.Values["user"] = user
|
||||
s.Values["admin"] = admin
|
||||
s.Options = c.Srv.Store.Options
|
||||
if err := s.Save(c.Request, c.Response); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
func (c *Context) LoggedOn() (ok bool) {
|
||||
_, ok = c.Token.Claims["user"]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) UnsetUdata() {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
s.Values = make(map[interface{}]interface{})
|
||||
s.Options = c.Srv.Store.Options
|
||||
if err := s.Save(c.Request, c.Response); err != nil {
|
||||
func (c *Context) SetCookie(claims map[string]interface{}) {
|
||||
t := jwt.NewToken(claims, nil)
|
||||
if err := c.Srv.JWT.Sign(t); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
c.Token = *t
|
||||
http.SetCookie(c.Response, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: t.Sig(),
|
||||
MaxAge: 3600 * 12,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Context) GetUser() string {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
if ret, ok := s.Values["user"].(string); ok {
|
||||
if ret, ok := c.Token.Claims["user"].(string); ok {
|
||||
return ret
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Context) GetAdmin() bool {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
if ret, ok := s.Values["admin"].(bool); ok {
|
||||
if ret, ok := c.Token.Claims["admin"].(bool); ok {
|
||||
return ret
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Context) getToken() []byte {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
t, ok := s.Values["token"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
b, ok := t.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func xor(a, b []byte) (c []byte) {
|
||||
n := len(a)
|
||||
if len(b) != n {
|
||||
return
|
||||
}
|
||||
c = make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
c[i] = a[i] ^ b[i]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ctxHandler func(*Context)
|
||||
|
|
|
|||
|
|
@ -68,9 +68,6 @@ func staticHandler(ctx *Context) {
|
|||
}
|
||||
|
||||
func indexHandler(ctx *Context) {
|
||||
if !ctx.LoggedOn() {
|
||||
ctx.UnsetUdata()
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +88,7 @@ func loginHandler(ctx *Context) {
|
|||
user := ctx.Form("user")
|
||||
password := ctx.Form("password")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken(nil) {
|
||||
if !ctx.CheckXsrf(nil) {
|
||||
return
|
||||
}
|
||||
if user == "" || password == "" || submit != "Login" {
|
||||
|
|
@ -99,13 +96,13 @@ func loginHandler(ctx *Context) {
|
|||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
err := ctx.Srv.DB.Login(user, password)
|
||||
u, err := ctx.Srv.DB.Login(user, password)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.SetUdata(user, ctx.Srv.DB.UserIsAdmin(user))
|
||||
ctx.SetCookie(map[string]interface{}{"username": u.Username, "admin": u.Admin})
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
}
|
||||
|
|
@ -122,7 +119,7 @@ func searchHandler(ctx *Context) {
|
|||
case "POST":
|
||||
search := ctx.Form("search")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken(nil) {
|
||||
if !ctx.CheckXsrf(nil) {
|
||||
return
|
||||
}
|
||||
if submit != "Search" {
|
||||
|
|
@ -178,7 +175,7 @@ func wikiNewHandler(ctx *Context) {
|
|||
MD: text,
|
||||
}
|
||||
ctx.Data.Data = p
|
||||
if !ctx.CheckToken(nil) {
|
||||
if !ctx.CheckXsrf(nil) {
|
||||
return
|
||||
}
|
||||
if submit != "Submit" {
|
||||
|
|
@ -256,7 +253,7 @@ func wikiEditHandler(ctx *Context) {
|
|||
}
|
||||
ctx.Data.Title = ftitle
|
||||
ctx.Data.BodyTitle = ftitle
|
||||
if !ctx.CheckToken(nil) {
|
||||
if !ctx.CheckXsrf(nil) {
|
||||
return
|
||||
}
|
||||
if submit != "Submit" {
|
||||
|
|
@ -281,7 +278,7 @@ func wikiDelHandler(ctx *Context) {
|
|||
f := func() {
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
}
|
||||
if !ctx.CheckToken(f) {
|
||||
if !ctx.CheckXsrf(f) {
|
||||
return
|
||||
}
|
||||
if submit != "Delete" {
|
||||
|
|
@ -333,7 +330,7 @@ func userNewHandler(ctx *Context) {
|
|||
adm = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken(nil) {
|
||||
if !ctx.CheckXsrf(nil) {
|
||||
return
|
||||
}
|
||||
if user == "" || password == "" || password != repeat || submit != "Submit" {
|
||||
|
|
@ -376,7 +373,7 @@ func userEditHandler(ctx *Context) {
|
|||
adm = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken(nil) {
|
||||
if !ctx.CheckXsrf(nil) {
|
||||
return
|
||||
}
|
||||
if user == "" || password != repeat || submit != "Submit" {
|
||||
|
|
@ -438,6 +435,6 @@ func userDelHandler(ctx *Context) {
|
|||
}
|
||||
|
||||
func logoutHandler(ctx *Context) {
|
||||
ctx.UnsetUdata()
|
||||
ctx.SetCookie(nil)
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,33 +2,30 @@ package server
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"html/template"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/db"
|
||||
"git.giftfish.de/ston1th/jwt"
|
||||
)
|
||||
|
||||
const (
|
||||
keyLen = 32
|
||||
authLen = keyLen * 2
|
||||
cookieName = "gosession"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
Version string
|
||||
SecCookie bool
|
||||
Store *sessions.CookieStore
|
||||
DB *db.DB
|
||||
listener net.Listener
|
||||
srv *http.Server
|
||||
templ map[string]*template.Template
|
||||
res map[string][]byte
|
||||
Version string
|
||||
listener net.Listener
|
||||
srv *http.Server
|
||||
|
||||
DB *db.DB
|
||||
JWT *jwt.JWT
|
||||
|
||||
templ map[string]*template.Template
|
||||
res map[string][]byte
|
||||
}
|
||||
|
||||
func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServer) {
|
||||
|
|
@ -36,6 +33,7 @@ func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServ
|
|||
Version: version,
|
||||
SecCookie: secCookie,
|
||||
listener: l,
|
||||
JWT: jwt.New(time.Hour*12, nil, nil),
|
||||
}
|
||||
srv.cookieStore()
|
||||
srv.loadTemplates()
|
||||
|
|
@ -46,7 +44,7 @@ func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServ
|
|||
}
|
||||
|
||||
func (s *HTTPServer) Start() (err error) {
|
||||
s.BS, err = NewBoltStore()
|
||||
s.DB, err = db.New()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -81,37 +79,3 @@ func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc {
|
|||
h(newContext(w, r, s))
|
||||
}
|
||||
}
|
||||
func (s *HTTPServer) cookieStore() {
|
||||
options := &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 3600 * 12,
|
||||
Secure: s.SecCookie,
|
||||
HttpOnly: true,
|
||||
}
|
||||
authKey := genKey(authLen)
|
||||
encKey := genKey(keyLen)
|
||||
s.Store = sessions.NewCookieStore(authKey, encKey)
|
||||
s.Store.Options = options
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Hour * 12)
|
||||
newAuthKey := genKey(authLen)
|
||||
newEncKey := genKey(keyLen)
|
||||
newStore := sessions.NewCookieStore(newAuthKey, newEncKey, authKey, encKey)
|
||||
newStore.Options = options
|
||||
|
||||
s.Store = newStore
|
||||
authKey = newAuthKey
|
||||
encKey = newEncKey
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func genKey(length int) (bytes []byte) {
|
||||
bytes = make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
|
||||
log.Println("genKey:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
43
pkg/server/xsrf.go
Normal file
43
pkg/server/xsrf.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/jwt"
|
||||
)
|
||||
|
||||
const keySize = jwt.KeySize / 2
|
||||
|
||||
func genKey(length int) (bytes []byte) {
|
||||
bytes = make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
|
||||
log.Println("genKey:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newXsrf(secret []byte) string {
|
||||
rnd := genKey(keySize)
|
||||
return base64.StdEncoding.EncodeToString(append(rnd, xor(rnd, secret)...))
|
||||
}
|
||||
|
||||
func checkXsrf(xsrf string, secret []byte) bool {
|
||||
t, err := base64.StdEncoding.DecodeString(xsrf)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare(secret, xor(t[keySize:], t[:keySize])) == 1
|
||||
}
|
||||
|
||||
func xor(a, b []byte) []byte {
|
||||
n := len(a)
|
||||
if len(b) != n {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
a[i] = a[i] ^ b[i]
|
||||
}
|
||||
return a
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue