jwt for webapp

This commit is contained in:
ston1th 2018-09-12 00:37:13 +02:00
commit 9d332bbda4
5 changed files with 112 additions and 152 deletions

View file

@ -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

View file

@ -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)

View file

@ -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)
}

View file

@ -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
View 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
}