Skip to content

Instantly share code, notes, and snippets.

@pciet
Created January 26, 2018 01:52
Show Gist options
  • Save pciet/8529531fffbe8d9523d883c901d311da to your computer and use it in GitHub Desktop.
Save pciet/8529531fffbe8d9523d883c901d311da to your computer and use it in GitHub Desktop.
login session gist
// insecure implementation, the session key cookie should be Secure and HttpOnly in a final version
import (
"database/sql"
_ "github.com/lib/pq"
)
type DB struct {
*sql.DB
}
// initialized in main before http.ListenAndServe
var database DB
type TX struct {
*sql.Tx
}
func (db DB) Begin() TX {
tx, err := db.DB.Begin()
if err != nil {
panic(err.Error())
}
return TX{
Tx: tx,
}
}
func (tx TX) Commit() {
err := tx.Tx.Commit()
if err != nil {
panic(err.Error())
}
}
/****************/
import (
"net/http"
)
// in each HTTP handler
key, name := database.validSession(r)
if key == "" {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
if name == "" {
clearClientSession(w)
http.Redirect(w, r, "/login", http.StatusFound)
return
}
/****************/
import (
"golang.org/x/crypto/bcrypt"
)
// The client code is responsible for checking the specifics of password requirements because the server behavior is the same for "wrong password" and "invalid password for new player".
func (db DB) loginOrCreate(name, password string) string {
key := db.login(name, password)
if key != "" {
return key
}
return db.createAndLogin(name, password)
}
func (db DB) login(name, password string) string {
has, encrypt := db.playerCrypt(name)
if has == false {
return ""
}
err := bcrypt.CompareHashAndPassword([]byte(encrypt), []byte(password))
if err != nil {
return ""
}
sessionKey := newSessionKey()
db.newSession(name, sessionKey)
return sessionKey
}
func (db DB) createAndLogin(name, password string) string {
exists, _ := db.playerCrypt(name)
if exists {
return ""
}
if (name == easy_computer_player) || (name == hard_computer_player) {
return ""
}
crypt, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
panic(err.Error())
}
db.newPlayer(name, string(crypt))
return db.login(name, password)
}
/****************/
import (
"crypto/rand"
"database/sql"
"encoding/base64"
"fmt"
"net/http"
)
const (
key_cookie = "k"
key_length = 64
session_table = "sessions"
session_name = "name"
session_key = "key"
)
// key, name
func (db DB) validSession(r *http.Request) (string, string) {
keyCookie, err := r.Cookie(key_cookie)
if err != nil {
return "", ""
}
var name string
err = db.QueryRow("SELECT "+session_name+" FROM "+session_table+" WHERE "+session_key+"=$1;", keyCookie.Value).Scan(&name)
if err != nil {
if debug {
fmt.Println(err.Error())
}
return "", ""
}
return keyCookie.Value, name
}
func clearClientSession(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: key_cookie, // from web_login.go
Value: "",
Path: "/",
HttpOnly: true,
Secure: false, // TODO: set true after TLS certification
})
}
func (db DB) newSession(name, key string) {
tx := db.Begin()
defer tx.Commit()
var playerKey []byte
err := tx.QueryRow("SELECT "+session_key+" FROM "+session_table+" WHERE "+session_name+"=$1 FOR UPDATE;", name).Scan(&playerKey)
if err == nil {
if string(playerKey) != key {
_, err = tx.Exec("UPDATE "+session_table+" SET "+session_key+" =$1 WHERE "+session_name+" =$2;", []byte(key), name)
if err != nil {
panic(err.Error())
}
}
return
} else if err != sql.ErrNoRows {
panic(err.Error())
}
_, err = tx.Exec("INSERT INTO "+session_table+"("+session_name+", "+session_key+") VALUES ($1, $2);", name, []byte(key))
if err != nil {
panic(err.Error())
}
}
func newSessionKey() string {
key := make([]byte, key_length)
count, err := rand.Read(key)
if err != nil {
panic(err.Error())
}
if count != key_length {
panic(fmt.Sprintf("count %v does not match key length %v", count, key_length))
}
return base64.StdEncoding.EncodeToString(key)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment