Browse Source
* Store OAuth2 session data in database * Rename table to `oauth2_session` and do not skip xormstorage initialization erroraj-second-factor
Lauris BH
6 years ago
committed by
GitHub
11 changed files with 603 additions and 12 deletions
@ -0,0 +1,75 @@ |
|||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. |
|||
|
|||
|
|||
[[projects]] |
|||
branch = "master" |
|||
name = "github.com/denisenkom/go-mssqldb" |
|||
packages = ["."] |
|||
revision = "ee492709d4324cdcb051d2ac266b77ddc380f5c5" |
|||
|
|||
[[projects]] |
|||
name = "github.com/go-sql-driver/mysql" |
|||
packages = ["."] |
|||
revision = "a0583e0143b1624142adab07e0e97fe106d99561" |
|||
version = "v1.3" |
|||
|
|||
[[projects]] |
|||
branch = "master" |
|||
name = "github.com/go-xorm/builder" |
|||
packages = ["."] |
|||
revision = "488224409dd8aa2ce7a5baf8d10d55764a913738" |
|||
|
|||
[[projects]] |
|||
name = "github.com/go-xorm/core" |
|||
packages = ["."] |
|||
revision = "da1adaf7a28ca792961721a34e6e04945200c890" |
|||
version = "v0.5.7" |
|||
|
|||
[[projects]] |
|||
name = "github.com/go-xorm/xorm" |
|||
packages = ["."] |
|||
revision = "1933dd69e294c0a26c0266637067f24dbb25770c" |
|||
version = "v0.6.4" |
|||
|
|||
[[projects]] |
|||
name = "github.com/gorilla/context" |
|||
packages = ["."] |
|||
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" |
|||
version = "v1.1" |
|||
|
|||
[[projects]] |
|||
name = "github.com/gorilla/securecookie" |
|||
packages = ["."] |
|||
revision = "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983" |
|||
version = "v1.1.1" |
|||
|
|||
[[projects]] |
|||
name = "github.com/gorilla/sessions" |
|||
packages = ["."] |
|||
revision = "ca9ada44574153444b00d3fd9c8559e4cc95f896" |
|||
version = "v1.1" |
|||
|
|||
[[projects]] |
|||
branch = "master" |
|||
name = "github.com/lib/pq" |
|||
packages = [".","oid"] |
|||
revision = "88edab0803230a3898347e77b474f8c1820a1f20" |
|||
|
|||
[[projects]] |
|||
name = "github.com/mattn/go-sqlite3" |
|||
packages = ["."] |
|||
revision = "6c771bb9887719704b210e87e934f08be014bdb1" |
|||
version = "v1.6.0" |
|||
|
|||
[[projects]] |
|||
branch = "master" |
|||
name = "golang.org/x/crypto" |
|||
packages = ["md4"] |
|||
revision = "c7dcf104e3a7a1417abc0230cb0d5240d764159d" |
|||
|
|||
[solve-meta] |
|||
analyzer-name = "dep" |
|||
analyzer-version = 1 |
|||
inputs-digest = "bba98a94e8c6668ae9556b4978bbffdfc5d4d535d522c8865465335bfaa2fc70" |
|||
solver-name = "gps-cdcl" |
|||
solver-version = 1 |
@ -0,0 +1,50 @@ |
|||
|
|||
# Gopkg.toml example |
|||
# |
|||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md |
|||
# for detailed Gopkg.toml documentation. |
|||
# |
|||
# required = ["github.com/user/thing/cmd/thing"] |
|||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] |
|||
# |
|||
# [[constraint]] |
|||
# name = "github.com/user/project" |
|||
# version = "1.0.0" |
|||
# |
|||
# [[constraint]] |
|||
# name = "github.com/user/project2" |
|||
# branch = "dev" |
|||
# source = "github.com/myfork/project2" |
|||
# |
|||
# [[override]] |
|||
# name = "github.com/x/y" |
|||
# version = "2.4.0" |
|||
|
|||
|
|||
[[constraint]] |
|||
name = "github.com/go-sql-driver/mysql" |
|||
version = "1.3.0" |
|||
|
|||
[[constraint]] |
|||
name = "github.com/go-xorm/xorm" |
|||
version = "0.6.4" |
|||
|
|||
[[constraint]] |
|||
name = "github.com/gorilla/context" |
|||
version = "1.1.0" |
|||
|
|||
[[constraint]] |
|||
name = "github.com/gorilla/securecookie" |
|||
version = "1.1.1" |
|||
|
|||
[[constraint]] |
|||
name = "github.com/gorilla/sessions" |
|||
version = "1.1.0" |
|||
|
|||
[[constraint]] |
|||
branch = "master" |
|||
name = "github.com/lib/pq" |
|||
|
|||
[[constraint]] |
|||
name = "github.com/mattn/go-sqlite3" |
|||
version = "1.6.0" |
@ -0,0 +1,19 @@ |
|||
Copyright (c) 2018 Lauris Bukšis-Haberkorns, Mattias Wadman |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy of |
|||
this software and associated documentation files (the "Software"), to deal in |
|||
the Software without restriction, including without limitation the rights to |
|||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
|||
of the Software, and to permit persons to whom the Software is furnished to do |
|||
so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
@ -0,0 +1,48 @@ |
|||
[![GoDoc](https://godoc.org/github.com/lafriks/xormstore?status.svg)](https://godoc.org/github.com/lafriks/xormstore) |
|||
[![Build Status](https://travis-ci.org/lafriks/xormstore.svg?branch=master)](https://travis-ci.org/lafriks/xormstore) |
|||
[![codecov](https://codecov.io/gh/lafriks/xormstore/branch/master/graph/badge.svg)](https://codecov.io/gh/lafriks/xormstore) |
|||
|
|||
#### XORM backend for gorilla sessions |
|||
|
|||
go get github.com/lafriks/xormstore |
|||
|
|||
#### Example |
|||
|
|||
```go |
|||
// initialize and setup cleanup |
|||
store := xormstore.New(engine, []byte("secret")) |
|||
// db cleanup every hour |
|||
// close quit channel to stop cleanup |
|||
quit := make(chan struct{}) |
|||
go store.PeriodicCleanup(1*time.Hour, quit) |
|||
``` |
|||
|
|||
```go |
|||
// in HTTP handler |
|||
func handlerFunc(w http.ResponseWriter, r *http.Request) { |
|||
session, err := store.Get(r, "session") |
|||
session.Values["user_id"] = 123 |
|||
store.Save(r, w, session) |
|||
http.Error(w, "", http.StatusOK) |
|||
} |
|||
``` |
|||
|
|||
For more details see [xormstore godoc documentation](https://godoc.org/github.com/lafriks/xormstore). |
|||
|
|||
#### Testing |
|||
|
|||
Just sqlite3 tests: |
|||
|
|||
go test |
|||
|
|||
All databases using docker: |
|||
|
|||
./test |
|||
|
|||
If docker is not local (docker-machine etc): |
|||
|
|||
DOCKER_IP=$(docker-machine ip dev) ./test |
|||
|
|||
#### License |
|||
|
|||
xormstore is licensed under the MIT license. See [LICENSE](LICENSE) for the full license text. |
@ -0,0 +1,70 @@ |
|||
#!/bin/bash |
|||
|
|||
DOCKER_IP=${DOCKER_IP:-127.0.0.1} |
|||
|
|||
sqlite3() { |
|||
DATABASE_URI="sqlite3://file:dummy?mode=memory&cache=shared" go test -v -race -cover -coverprofile=coverage.txt -covermode=atomic |
|||
return $? |
|||
} |
|||
|
|||
postgres10() { |
|||
ID=$(docker run -p 5432 -d postgres:10-alpine) |
|||
PORT=$(docker port "$ID" 5432 | cut -d : -f 2) |
|||
DATABASE_URI="postgres://user=postgres password=postgres dbname=postgres host=$DOCKER_IP port=$PORT sslmode=disable" go test -v -race -cover |
|||
S=$? |
|||
docker rm -vf "$ID" > /dev/null |
|||
return $S |
|||
} |
|||
|
|||
postgres96() { |
|||
ID=$(docker run -p 5432 -d postgres:9.6-alpine) |
|||
PORT=$(docker port "$ID" 5432 | cut -d : -f 2) |
|||
DATABASE_URI="postgres://user=postgres password=postgres dbname=postgres host=$DOCKER_IP port=$PORT sslmode=disable" go test -v -race -cover |
|||
S=$? |
|||
docker rm -vf "$ID" > /dev/null |
|||
return $S |
|||
} |
|||
|
|||
postgres94() { |
|||
ID=$(docker run -p 5432 -d postgres:9.4-alpine) |
|||
PORT=$(docker port "$ID" 5432 | cut -d : -f 2) |
|||
DATABASE_URI="postgres://user=postgres password=postgres dbname=postgres host=$DOCKER_IP port=$PORT sslmode=disable" go test -v -race -cover |
|||
S=$? |
|||
docker rm -vf "$ID" > /dev/null |
|||
return $S |
|||
} |
|||
|
|||
mysql57() { |
|||
ID=$(docker run \ |
|||
-e MYSQL_ROOT_PASSWORD=root \ |
|||
-e MYSQL_USER=mysql \ |
|||
-e MYSQL_PASSWORD=mysql \ |
|||
-e MYSQL_DATABASE=mysql \ |
|||
-p 3306 -d mysql:5.7) |
|||
PORT=$(docker port "$ID" 3306 | cut -d : -f 2) |
|||
DATABASE_URI="mysql://mysql:mysql@tcp($DOCKER_IP:$PORT)/mysql?charset=utf8&parseTime=True" go test -v -race -cover |
|||
S=$? |
|||
docker rm -vf "$ID" > /dev/null |
|||
return $S |
|||
} |
|||
|
|||
mariadb10() { |
|||
ID=$(docker run \ |
|||
-e MYSQL_ROOT_PASSWORD=root \ |
|||
-e MYSQL_USER=mysql \ |
|||
-e MYSQL_PASSWORD=mysql \ |
|||
-e MYSQL_DATABASE=mysql \ |
|||
-p 3306 -d mariadb:10) |
|||
PORT=$(docker port "$ID" 3306 | cut -d : -f 2) |
|||
DATABASE_URI="mysql://mysql:mysql@tcp($DOCKER_IP:$PORT)/mysql?charset=utf8&parseTime=True" go test -v -race -cover |
|||
S=$? |
|||
docker rm -vf "$ID" > /dev/null |
|||
return $S |
|||
} |
|||
|
|||
sqlite3 || exit 1 |
|||
postgres94 || exit 1 |
|||
postgres96 || exit 1 |
|||
postgres10 || exit 1 |
|||
mysql57 || exit 1 |
|||
mariadb10 || exit 1 |
@ -0,0 +1,60 @@ |
|||
package util |
|||
|
|||
import ( |
|||
"time" |
|||
) |
|||
|
|||
// TimeStamp defines a timestamp
|
|||
type TimeStamp int64 |
|||
|
|||
// TimeStampNow returns now int64
|
|||
func TimeStampNow() TimeStamp { |
|||
return TimeStamp(time.Now().Unix()) |
|||
} |
|||
|
|||
// Add adds seconds and return sum
|
|||
func (ts TimeStamp) Add(seconds int64) TimeStamp { |
|||
return ts + TimeStamp(seconds) |
|||
} |
|||
|
|||
// AddDuration adds time.Duration and return sum
|
|||
func (ts TimeStamp) AddDuration(interval time.Duration) TimeStamp { |
|||
return ts + TimeStamp(interval/time.Second) |
|||
} |
|||
|
|||
// Year returns the time's year
|
|||
func (ts TimeStamp) Year() int { |
|||
return ts.AsTime().Year() |
|||
} |
|||
|
|||
// AsTime convert timestamp as time.Time in Local locale
|
|||
func (ts TimeStamp) AsTime() (tm time.Time) { |
|||
tm = time.Unix(int64(ts), 0).Local() |
|||
return |
|||
} |
|||
|
|||
// AsTimePtr convert timestamp as *time.Time in Local locale
|
|||
func (ts TimeStamp) AsTimePtr() *time.Time { |
|||
tm := time.Unix(int64(ts), 0).Local() |
|||
return &tm |
|||
} |
|||
|
|||
// Format formats timestamp as
|
|||
func (ts TimeStamp) Format(f string) string { |
|||
return ts.AsTime().Format(f) |
|||
} |
|||
|
|||
// FormatLong formats as RFC1123Z
|
|||
func (ts TimeStamp) FormatLong() string { |
|||
return ts.Format(time.RFC1123Z) |
|||
} |
|||
|
|||
// FormatShort formats as short
|
|||
func (ts TimeStamp) FormatShort() string { |
|||
return ts.Format("Jan 02, 2006") |
|||
} |
|||
|
|||
// IsZero is zero time
|
|||
func (ts TimeStamp) IsZero() bool { |
|||
return ts.AsTime().IsZero() |
|||
} |
@ -0,0 +1,251 @@ |
|||
/* |
|||
Package xormstore is a XORM backend for gorilla sessions |
|||
|
|||
Simplest form: |
|||
|
|||
store, err := xormstore.New(engine, []byte("secret-hash-key")) |
|||
|
|||
All options: |
|||
|
|||
store, err := xormstore.NewOptions( |
|||
engine, // *xorm.Engine
|
|||
xormstore.Options{ |
|||
TableName: "sessions", // "sessions" is default
|
|||
SkipCreateTable: false, // false is default
|
|||
}, |
|||
[]byte("secret-hash-key"), // 32 or 64 bytes recommended, required
|
|||
[]byte("secret-encyption-key")) // nil, 16, 24 or 32 bytes, optional
|
|||
|
|||
if err != nil { |
|||
// xormstore can not be initialized
|
|||
} |
|||
|
|||
// some more settings, see sessions.Options
|
|||
store.SessionOpts.Secure = true |
|||
store.SessionOpts.HttpOnly = true |
|||
store.SessionOpts.MaxAge = 60 * 60 * 24 * 60 |
|||
|
|||
If you want periodic cleanup of expired sessions: |
|||
|
|||
quit := make(chan struct{}) |
|||
go store.PeriodicCleanup(1*time.Hour, quit) |
|||
|
|||
For more information about the keys see https://github.com/gorilla/securecookie
|
|||
|
|||
For API to use in HTTP handlers see https://github.com/gorilla/sessions
|
|||
*/ |
|||
package xormstore |
|||
|
|||
import ( |
|||
"encoding/base32" |
|||
"net/http" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/lafriks/xormstore/util" |
|||
|
|||
"github.com/go-xorm/xorm" |
|||
"github.com/gorilla/context" |
|||
"github.com/gorilla/securecookie" |
|||
"github.com/gorilla/sessions" |
|||
) |
|||
|
|||
const sessionIDLen = 32 |
|||
const defaultTableName = "sessions" |
|||
const defaultMaxAge = 60 * 60 * 24 * 30 // 30 days
|
|||
const defaultPath = "/" |
|||
|
|||
// Options for xormstore
|
|||
type Options struct { |
|||
TableName string |
|||
SkipCreateTable bool |
|||
} |
|||
|
|||
// Store represent a xormstore
|
|||
type Store struct { |
|||
e *xorm.Engine |
|||
opts Options |
|||
Codecs []securecookie.Codec |
|||
SessionOpts *sessions.Options |
|||
} |
|||
|
|||
type xormSession struct { |
|||
ID string `xorm:"VARCHAR(400) PK NAME 'id'"` |
|||
Data string `xorm:"TEXT"` |
|||
CreatedUnix util.TimeStamp `xorm:"created"` |
|||
UpdatedUnix util.TimeStamp `xorm:"updated"` |
|||
ExpiresUnix util.TimeStamp `xorm:"INDEX"` |
|||
|
|||
tableName string `xorm:"-"` // just to store table name for easier access
|
|||
} |
|||
|
|||
// Define a type for context keys so that they can't clash with anything else stored in context
|
|||
type contextKey string |
|||
|
|||
func (xs *xormSession) TableName() string { |
|||
return xs.tableName |
|||
} |
|||
|
|||
// New creates a new xormstore session
|
|||
func New(e *xorm.Engine, keyPairs ...[]byte) (*Store, error) { |
|||
return NewOptions(e, Options{}, keyPairs...) |
|||
} |
|||
|
|||
// NewOptions creates a new xormstore session with options
|
|||
func NewOptions(e *xorm.Engine, opts Options, keyPairs ...[]byte) (*Store, error) { |
|||
st := &Store{ |
|||
e: e, |
|||
opts: opts, |
|||
Codecs: securecookie.CodecsFromPairs(keyPairs...), |
|||
SessionOpts: &sessions.Options{ |
|||
Path: defaultPath, |
|||
MaxAge: defaultMaxAge, |
|||
}, |
|||
} |
|||
if st.opts.TableName == "" { |
|||
st.opts.TableName = defaultTableName |
|||
} |
|||
|
|||
if !st.opts.SkipCreateTable { |
|||
if err := st.e.Sync2(&xormSession{tableName: st.opts.TableName}); err != nil { |
|||
return nil, err |
|||
} |
|||
} |
|||
|
|||
return st, nil |
|||
} |
|||
|
|||
// Get returns a session for the given name after adding it to the registry.
|
|||
func (st *Store) Get(r *http.Request, name string) (*sessions.Session, error) { |
|||
return sessions.GetRegistry(r).Get(st, name) |
|||
} |
|||
|
|||
// New creates a session with name without adding it to the registry.
|
|||
func (st *Store) New(r *http.Request, name string) (*sessions.Session, error) { |
|||
session := sessions.NewSession(st, name) |
|||
opts := *st.SessionOpts |
|||
session.Options = &opts |
|||
|
|||
st.MaxAge(st.SessionOpts.MaxAge) |
|||
|
|||
// try fetch from db if there is a cookie
|
|||
if cookie, err := r.Cookie(name); err == nil { |
|||
if err := securecookie.DecodeMulti(name, cookie.Value, &session.ID, st.Codecs...); err != nil { |
|||
return session, nil |
|||
} |
|||
s := &xormSession{tableName: st.opts.TableName} |
|||
if has, err := st.e.Where("id = ? AND expires_unix >= ?", session.ID, util.TimeStampNow()).Get(s); !has || err != nil { |
|||
return session, nil |
|||
} |
|||
if err := securecookie.DecodeMulti(session.Name(), s.Data, &session.Values, st.Codecs...); err != nil { |
|||
return session, nil |
|||
} |
|||
|
|||
context.Set(r, contextKey(name), s) |
|||
} |
|||
|
|||
return session, nil |
|||
} |
|||
|
|||
// Save session and set cookie header
|
|||
func (st *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { |
|||
s, _ := context.Get(r, contextKey(session.Name())).(*xormSession) |
|||
|
|||
// delete if max age is < 0
|
|||
if session.Options.MaxAge < 0 { |
|||
if s != nil { |
|||
if _, err := st.e.Delete(&xormSession{ |
|||
ID: session.ID, |
|||
tableName: st.opts.TableName, |
|||
}); err != nil { |
|||
return err |
|||
} |
|||
} |
|||
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options)) |
|||
return nil |
|||
} |
|||
|
|||
data, err := securecookie.EncodeMulti(session.Name(), session.Values, st.Codecs...) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
now := util.TimeStampNow() |
|||
expire := now.AddDuration(time.Second * time.Duration(session.Options.MaxAge)) |
|||
|
|||
if s == nil { |
|||
// generate random session ID key suitable for storage in the db
|
|||
session.ID = strings.TrimRight( |
|||
base32.StdEncoding.EncodeToString( |
|||
securecookie.GenerateRandomKey(sessionIDLen)), "=") |
|||
s = &xormSession{ |
|||
ID: session.ID, |
|||
Data: data, |
|||
CreatedUnix: now, |
|||
UpdatedUnix: now, |
|||
ExpiresUnix: expire, |
|||
tableName: st.opts.TableName, |
|||
} |
|||
if _, err := st.e.Insert(s); err != nil { |
|||
return err |
|||
} |
|||
context.Set(r, contextKey(session.Name()), s) |
|||
} else { |
|||
s.Data = data |
|||
s.UpdatedUnix = now |
|||
s.ExpiresUnix = expire |
|||
if _, err := st.e.ID(s.ID).Cols("data", "updated_unix", "expires_unix").Update(s); err != nil { |
|||
return err |
|||
} |
|||
} |
|||
|
|||
// set session id cookie
|
|||
id, err := securecookie.EncodeMulti(session.Name(), session.ID, st.Codecs...) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
http.SetCookie(w, sessions.NewCookie(session.Name(), id, session.Options)) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// MaxAge sets the maximum age for the store and the underlying cookie
|
|||
// implementation. Individual sessions can be deleted by setting
|
|||
// Options.MaxAge = -1 for that session.
|
|||
func (st *Store) MaxAge(age int) { |
|||
st.SessionOpts.MaxAge = age |
|||
for _, codec := range st.Codecs { |
|||
if sc, ok := codec.(*securecookie.SecureCookie); ok { |
|||
sc.MaxAge(age) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MaxLength restricts the maximum length of new sessions to l.
|
|||
// If l is 0 there is no limit to the size of a session, use with caution.
|
|||
// The default is 4096 (default for securecookie)
|
|||
func (st *Store) MaxLength(l int) { |
|||
for _, c := range st.Codecs { |
|||
if codec, ok := c.(*securecookie.SecureCookie); ok { |
|||
codec.MaxLength(l) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Cleanup deletes expired sessions
|
|||
func (st *Store) Cleanup() { |
|||
st.e.Where("expires_unix < ?", util.TimeStampNow()).Delete(&xormSession{tableName: st.opts.TableName}) |
|||
} |
|||
|
|||
// PeriodicCleanup runs Cleanup every interval. Close quit channel to stop.
|
|||
func (st *Store) PeriodicCleanup(interval time.Duration, quit <-chan struct{}) { |
|||
t := time.NewTicker(interval) |
|||
defer t.Stop() |
|||
for { |
|||
select { |
|||
case <-t.C: |
|||
st.Cleanup() |
|||
case <-quit: |
|||
return |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue