Store OAuth2 session data in database (#3660)

* Store OAuth2 session data in database

* Rename table to `oauth2_session` and do not skip xormstorage initialization error
release/v1.5
Lauris BH 6 years ago committed by GitHub
parent 8d5f58d834
commit 5a62eb30df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -97,14 +97,17 @@ func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) {
}
// InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library
func InitOAuth2() {
oauth2.Init()
func InitOAuth2() error {
if err := oauth2.Init(x); err != nil {
return err
}
loginSources, _ := GetActiveOAuth2ProviderLoginSources()
for _, source := range loginSources {
oAuth2Config := source.OAuth2()
oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
}
return nil
}
// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2

@ -7,13 +7,12 @@ package oauth2
import (
"math"
"net/http"
"os"
"path/filepath"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/gorilla/sessions"
"github.com/go-xorm/xorm"
"github.com/lafriks/xormstore"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/bitbucket"
@ -41,13 +40,14 @@ type CustomURLMapping struct {
}
// Init initialize the setup of the OAuth2 library
func Init() {
sessionDir := filepath.Join(setting.AppDataPath, "sessions", "oauth2")
if err := os.MkdirAll(sessionDir, 0700); err != nil {
log.Fatal(4, "Fail to create dir %s: %v", sessionDir, err)
}
func Init(x *xorm.Engine) error {
store, err := xormstore.NewOptions(x, xormstore.Options{
TableName: "oauth2_session",
}, []byte(sessionUsersStoreKey))
store := sessions.NewFilesystemStore(sessionDir, []byte(sessionUsersStoreKey))
if err != nil {
return err
}
// according to the Goth lib:
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
// securecookie: the value is too long
@ -65,6 +65,7 @@ func Init() {
return req.Header.Get(providerHeaderKey), nil
}
return nil
}
// Auth OAuth2 auth service

@ -60,7 +60,9 @@ func GlobalInit() {
log.Fatal(4, "Failed to initialize ORM engine: %v", err)
}
models.HasEngine = true
models.InitOAuth2()
if err := models.InitOAuth2(); err != nil {
log.Fatal(4, "Failed to initialize OAuth2 support: %v", err)
}
models.LoadRepoConfig()
models.NewRepoContext()

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

12
vendor/vendor.json vendored

@ -647,6 +647,18 @@
"revision": "cb6bfca970f6908083f26f39a79009d608efd5cd",
"revisionTime": "2016-10-16T15:41:25Z"
},
{
"checksumSHA1": "/X7eCdN7MX8zgCjA9s0ktzgTPlA=",
"path": "github.com/lafriks/xormstore",
"revision": "3a80a383a04b29ec2e1bf61279dd948aa809335b",
"revisionTime": "2018-04-09T10:45:24Z"
},
{
"checksumSHA1": "Vxvfs8mukr9GOLSuGIPU4ODyOZc=",
"path": "github.com/lafriks/xormstore/util",
"revision": "c0e2f3dc1ecab3536617967e4b47ee5b9e2ca229",
"revisionTime": "2018-03-11T19:16:53Z"
},
{
"checksumSHA1": "QV4HZTfaXvhD+5PcGM2p+7aCYYI=",
"path": "github.com/lib/pq",

Loading…
Cancel
Save