Commit 58b34085 authored by ale's avatar ale

Merge branch 'better-login' into 'master'

Refactor the login handler

See merge request !6
parents 6d3a620e dfe0056b
Pipeline #5396 passed with stages
in 3 minutes and 35 seconds
......@@ -13,15 +13,16 @@ import (
"strings"
"time"
"github.com/gorilla/sessions"
"git.autistici.org/id/go-sso"
"git.autistici.org/id/go-sso/httputil"
"github.com/gorilla/securecookie"
)
type authSession struct {
*httputil.ExpiringSession
const (
ssoCookieName = "sso"
nonceCookieName = "sso_n"
)
type authSession struct {
Auth bool
Username string
Groups []string
......@@ -29,14 +30,13 @@ type authSession struct {
type authSessionKeyType int
var authSessionKey authSessionKeyType = 42
const authSessionKey authSessionKeyType = 0
func getCurrentAuthSession(req *http.Request) *authSession {
s, ok := req.Context().Value(authSessionKey).(*authSession)
if !ok {
return nil
if s, ok := req.Context().Value(authSessionKey).(*authSession); ok {
return s
}
return s
return nil
}
// Authenticated returns true if the user is successfully
......@@ -64,7 +64,7 @@ func Groups(req *http.Request) []string {
return nil
}
var authSessionLifetime = 1 * time.Hour
var defaultAuthSessionTTL = 1 * time.Hour
func init() {
gob.Register(&authSession{})
......@@ -72,27 +72,30 @@ func init() {
// SSOWrapper protects http handlers with single-sign-on authentication.
type SSOWrapper struct {
v sso.Validator
sessionAuthKey []byte
sessionEncKey []byte
serverURL string
serverOrigin string
v sso.Validator
sc *securecookie.SecureCookie
serverURL string
serverOrigin string
}
// NewSSOWrapper returns a new SSOWrapper that will authenticate users
// on the specified login service.
func NewSSOWrapper(serverURL string, pkey []byte, domain string, sessionAuthKey, sessionEncKey []byte) (*SSOWrapper, error) {
func NewSSOWrapper(serverURL string, pkey []byte, domain string, sessionAuthKey, sessionEncKey []byte, ttl time.Duration) (*SSOWrapper, error) {
v, err := sso.NewValidator(pkey, domain)
if err != nil {
return nil, err
}
if ttl == 0 {
ttl = defaultAuthSessionTTL
}
sc := securecookie.New(sessionAuthKey, sessionEncKey).MaxAge(int(ttl.Seconds()))
return &SSOWrapper{
v: v,
serverURL: serverURL,
serverOrigin: originFromURL(serverURL),
sessionAuthKey: sessionAuthKey,
sessionEncKey: sessionEncKey,
v: v,
sc: sc,
serverURL: serverURL,
serverOrigin: originFromURL(serverURL),
}, nil
}
......@@ -100,50 +103,47 @@ func NewSSOWrapper(serverURL string, pkey []byte, domain string, sessionAuthKey,
// Currently only a simple form of group-based ACLs is supported.
func (s *SSOWrapper) Wrap(h http.Handler, service string, groups []string) http.Handler {
svcPath := pathFromService(service)
store := sessions.NewCookieStore(s.sessionAuthKey, s.sessionEncKey)
store.Options = &sessions.Options{
HttpOnly: true,
Secure: true,
MaxAge: 0,
Path: svcPath,
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
session, _ := store.Get(req, "sso")
switch strings.TrimPrefix(req.URL.Path, svcPath) {
case "sso_login":
s.handleLogin(w, req, session, service, groups)
s.handleLogin(w, req, service, groups)
case "sso_logout":
s.handleLogout(w, req, session)
s.handleLogout(w, req)
default:
if auth, ok := session.Values["a"].(*authSession); ok && auth.Valid() && auth.Auth {
req.Header.Set("X-Authenticated-User", auth.Username)
var auth authSession
if cookie, err := req.Cookie(ssoCookieName); err == nil {
s.sc.Decode(ssoCookieName, cookie.Value, &auth) // nolint
}
ctx := context.WithValue(req.Context(), authSessionKey, auth)
h.ServeHTTP(w, req.WithContext(ctx))
if auth.Auth {
req.Header.Set("X-Authenticated-User", auth.Username)
req = req.WithContext(context.WithValue(req.Context(), authSessionKey, &auth))
h.ServeHTTP(w, req)
return
}
s.redirectToLogin(w, req, session, service, groups)
s.redirectToLogin(w, req, service, groups)
}
})
}
func (s *SSOWrapper) handleLogin(w http.ResponseWriter, req *http.Request, session *sessions.Session, service string, groups []string) {
func (s *SSOWrapper) handleLogin(w http.ResponseWriter, req *http.Request, service string, groups []string) {
t := req.FormValue("t")
d := req.FormValue("d")
// Pop the nonce from the session.
nonce, ok := session.Values["nonce"].(string)
if !ok || nonce == "" {
// Pop the nonce from the cookies.
cookie, err := req.Cookie(nonceCookieName)
if err != nil {
log.Printf("got login request without nonce")
http.Error(w, "Missing nonce", http.StatusBadRequest)
return
}
delete(session.Values, "nonce")
nonce := cookie.Value
cookie.MaxAge = -1
cookie.Value = ""
http.SetCookie(w, cookie)
tkt, err := s.v.Validate(t, nonce, service, groups)
if err != nil {
......@@ -153,24 +153,34 @@ func (s *SSOWrapper) handleLogin(w http.ResponseWriter, req *http.Request, sessi
}
// Authenticate the user.
session.Values["a"] = &authSession{
ExpiringSession: httputil.NewExpiringSession(authSessionLifetime),
Auth: true,
Username: tkt.User,
Groups: tkt.Groups,
auth := authSession{
Auth: true,
Username: tkt.User,
Groups: tkt.Groups,
}
if err := sessions.Save(req, w); err != nil {
encoded, err := s.sc.Encode(ssoCookieName, &auth)
if err != nil {
log.Printf("error saving SSO session: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: ssoCookieName,
Value: encoded,
Path: pathFromService(service),
Secure: true,
HttpOnly: true,
})
http.Redirect(w, req, d, http.StatusFound)
}
func (s *SSOWrapper) handleLogout(w http.ResponseWriter, req *http.Request, session *sessions.Session) {
session.Options.MaxAge = -1
if err := sessions.Save(req, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
func (s *SSOWrapper) handleLogout(w http.ResponseWriter, req *http.Request) {
// Delete the session cookie, if present.
if cookie, err := req.Cookie(ssoCookieName); err == nil {
cookie.MaxAge = -1
cookie.Value = ""
http.SetCookie(w, cookie)
}
w.Header().Set("Content-Type", "text/plain")
......@@ -182,14 +192,16 @@ func (s *SSOWrapper) handleLogout(w http.ResponseWriter, req *http.Request, sess
}
// Redirect to the SSO server.
func (s *SSOWrapper) redirectToLogin(w http.ResponseWriter, req *http.Request, session *sessions.Session, service string, groups []string) {
// Generate a random nonce and store it in the local session.
func (s *SSOWrapper) redirectToLogin(w http.ResponseWriter, req *http.Request, service string, groups []string) {
// Generate a random nonce and store it in a cookie.
nonce := makeUniqueNonce()
session.Values["nonce"] = nonce
if err := sessions.Save(req, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: nonceCookieName,
Value: nonce,
Path: pathFromService(service) + "sso_login",
Secure: true,
HttpOnly: true,
})
v := make(url.Values)
v.Set("s", service)
......
......@@ -12,7 +12,6 @@ import (
"testing"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"golang.org/x/crypto/ed25519"
......@@ -108,9 +107,8 @@ func TestSSOWrapper(t *testing.T) {
t.Fatal(err)
}
// Build a test app - note that we want to use a gorilla Mux
// here, otherwise cookie-based sessions won't work.
m := mux.NewRouter()
// Build a test app.
m := http.NewServeMux()
m.HandleFunc("/test/groups", func(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, strings.Join(Groups(req), ",")) // nolint
})
......@@ -118,7 +116,7 @@ func TestSSOWrapper(t *testing.T) {
io.WriteString(w, "OK") // nolint
})
w, err := NewSSOWrapper("https://"+testLoginServer+"/", pub, testDomain, securecookie.GenerateRandomKey(64), securecookie.GenerateRandomKey(32))
w, err := NewSSOWrapper("https://"+testLoginServer+"/", pub, testDomain, securecookie.GenerateRandomKey(64), securecookie.GenerateRandomKey(32), 0)
if err != nil {
t.Fatal("NewSSOWrapper():", err)
}
......
package httputil
import (
"encoding/gob"
"time"
)
// ExpiringSession is a session with server-side expiration check.
// Session data is saved in signed, encrypted cookies in the
// browser. We'd like these cookies to expire when a certain amount of
// time passes, or when the user closes the browser. We trust the
// browser for the latter, but we enforce time-based expiration on the
// server.
type ExpiringSession struct {
Expiry time.Time
}
// NewExpiringSession returns a session that is valid for the given
// duration.
func NewExpiringSession(ttl time.Duration) *ExpiringSession {
return &ExpiringSession{
Expiry: time.Now().Add(ttl),
}
}
// Valid returns true if the session has not expired yet.
// It can be called with a nil receiver.
func (e *ExpiringSession) Valid() bool {
return e != nil && time.Now().Before(e.Expiry)
}
func init() {
gob.Register(&ExpiringSession{})
}
package httputil
import (
"bytes"
"encoding/gob"
"reflect"
"testing"
"time"
)
func TestExpiringSession(t *testing.T) {
type mySession struct {
*ExpiringSession
Data string
}
s := &mySession{
ExpiringSession: NewExpiringSession(60 * time.Second),
Data: "data",
}
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(s); err != nil {
t.Fatal("encode:", err)
}
var s2 mySession
if err := gob.NewDecoder(&buf).Decode(&s2); err != nil {
t.Fatal("decode:", err)
}
if !reflect.DeepEqual(s.Data, s2.Data) {
t.Fatalf("sessions differ: %+v vs %+v", s, &s2)
}
}
......@@ -19,6 +19,9 @@ import (
"git.autistici.org/id/go-sso/httpsso"
)
// TTL for SSO sessions on the proxy.
var proxyAuthTTL = 1 * time.Hour
// RNG for the random backend selector.
var rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
......@@ -131,6 +134,7 @@ func NewProxy(config *Config) (http.Handler, error) {
config.SSODomain,
[]byte(config.SessionAuthKey),
[]byte(config.SessionEncKey),
proxyAuthTTL,
)
if err != nil {
return nil, err
......
......@@ -26,6 +26,9 @@ import (
"git.autistici.org/id/go-sso/httpsso"
)
// Lifetime of an authenticated session.
var samlAuthTTL = 1 * time.Hour
type serviceProvider struct {
// Descriptor can either be an inline XML document, or it can
// be read from a file with the syntax "@filename".
......@@ -229,7 +232,7 @@ func NewSAMLIDP(config *Config) (http.Handler, error) {
return nil, err
}
w, err := httpsso.NewSSOWrapper(config.SSOLoginServerURL, pkey, config.SSODomain, []byte(config.SessionAuthKey), []byte(config.SessionEncKey))
w, err := httpsso.NewSSOWrapper(config.SSOLoginServerURL, pkey, config.SSODomain, []byte(config.SessionAuthKey), []byte(config.SessionEncKey), samlAuthTTL)
if err != nil {
return nil, err
}
......
......@@ -74,7 +74,7 @@ func staticCssBootstrapMinCss() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/css/bootstrap.min.css", size: 140936, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
info := bindataFileInfo{name: "static/css/bootstrap.min.css", size: 140936, mode: os.FileMode(420), modTime: time.Unix(1550305824, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -82,7 +82,7 @@ func staticCssBootstrapMinCss() (*asset, error) {
var _staticCssSigninCss = []byte(`body {
padding-top: 15%;
padding-bottom: 20%;
background-color: #eee;
background-color: #efefef;
}
.form-signin {
max-width: 330px;
......@@ -117,6 +117,9 @@ var _staticCssSigninCss = []byte(`body {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.form-signin input[type="text"] {
margin-bottom: 10px;
}
.error {
font-weight: bold;
color: red;
......@@ -147,7 +150,7 @@ func staticCssSigninCss() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/css/signin.css", size: 1009, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
info := bindataFileInfo{name: "static/css/signin.css", size: 1071, mode: os.FileMode(420), modTime: time.Unix(1576447419, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -170,7 +173,7 @@ func staticJsBootstrap413MinJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/js/bootstrap-4.1.3.min.js", size: 51039, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
info := bindataFileInfo{name: "static/js/bootstrap-4.1.3.min.js", size: 51039, mode: os.FileMode(420), modTime: time.Unix(1550305766, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -189,7 +192,7 @@ func staticJsJquery331MinJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/js/jquery-3.3.1.min.js", size: 86927, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
info := bindataFileInfo{name: "static/js/jquery-3.3.1.min.js", size: 86927, mode: os.FileMode(420), modTime: time.Unix(1516469204, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -243,7 +246,7 @@ func staticJsLogoutJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/js/logout.js", size: 1005, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
info := bindataFileInfo{name: "static/js/logout.js", size: 1005, mode: os.FileMode(420), modTime: time.Unix(1535013418, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -265,7 +268,7 @@ func staticJsPopper1143MinJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/js/popper-1.14.3.min.js", size: 20337, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
info := bindataFileInfo{name: "static/js/popper-1.14.3.min.js", size: 20337, mode: os.FileMode(420), modTime: time.Unix(1526549114, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -1030,7 +1033,7 @@ func staticJsU2fApiJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/js/u2f-api.js", size: 20880, mode: os.FileMode(420), modTime: time.Unix(1512325237, 0)}
info := bindataFileInfo{name: "static/js/u2f-api.js", size: 20880, mode: os.FileMode(420), modTime: time.Unix(1535013418, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -1099,14 +1102,14 @@ func staticJsU2fJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/js/u2f.js", size: 1274, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
info := bindataFileInfo{name: "static/js/u2f.js", size: 1274, mode: os.FileMode(420), modTime: time.Unix(1541228751, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _templatesLogin_otpHtml = []byte(`{{template "header" .}}
<form class="form-signin" action="{{.URLPrefix}}/login" method="post">
<form class="form-signin" action="{{.URLPrefix}}/login/otp" method="post">
{{.CSRFField}}
<div class="row no-gutters">
......@@ -1154,7 +1157,7 @@ func templatesLogin_otpHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "templates/login_otp.html", size: 956, mode: os.FileMode(420), modTime: time.Unix(1561757406, 0)}
info := bindataFileInfo{name: "templates/login_otp.html", size: 960, mode: os.FileMode(420), modTime: time.Unix(1576445228, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -1225,14 +1228,14 @@ func templatesLogin_passwordHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "templates/login_password.html", size: 1432, mode: os.FileMode(420), modTime: time.Unix(1561757427, 0)}
info := bindataFileInfo{name: "templates/login_password.html", size: 1432, mode: os.FileMode(420), modTime: time.Unix(1561884470, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _templatesLogin_u2fHtml = []byte(`{{template "header" .}}
<form class="form-signin" id="u2fForm" action="{{.URLPrefix}}/login" method="post">
<form class="form-signin" id="u2fForm" action="{{.URLPrefix}}/login/u2f" method="post">
{{.CSRFField}}
<input type="hidden" id="u2fResponseField" name="u2f_response" value="">
......@@ -1282,7 +1285,7 @@ func templatesLogin_u2fHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "templates/login_u2f.html", size: 908, mode: os.FileMode(420), modTime: time.Unix(1561757448, 0)}
info := bindataFileInfo{name: "templates/login_u2f.html", size: 912, mode: os.FileMode(420), modTime: time.Unix(1576445233, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -1342,7 +1345,7 @@ func templatesLogoutHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "templates/logout.html", size: 1063, mode: os.FileMode(420), modTime: time.Unix(1561757528, 0)}
info := bindataFileInfo{name: "templates/logout.html", size: 1063, mode: os.FileMode(420), modTime: time.Unix(1548600535, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......@@ -1353,8 +1356,8 @@ var _templatesPageHtml = []byte(`{{define "header"}}<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}}
<link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO">
<link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o">
<link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css"{{SRI "/static/css/bootstrap.min.css"}}>
<link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css"{{SRI "/static/css/signin.css"}}>
{{if .SiteFavicon}}<link rel="icon" type="image/x-icon" href="{{.URLPrefix}}/favicon.ico">{{end}}
<title>{{if .SiteName}}{{.SiteName}} - {{end}}Sign In</title>
</head>
......@@ -1366,15 +1369,15 @@ var _templatesPageHtml = []byte(`{{define "header"}}<!DOCTYPE html>
{{define "footer"}}
</div>
<script src="{{.URLPrefix}}/static/js/jquery-3.3.1.min.js" integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"></script>
<script src="{{.URLPrefix}}/static/js/popper-1.14.3.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"></script>
<script src="{{.URLPrefix}}/static/js/bootstrap-4.1.3.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"></script>
<script src="{{.URLPrefix}}/static/js/jquery-3.3.1.min.js"{{SRI "/static/js/jquery-3.3.1.min.js"}}></script>
<script src="{{.URLPrefix}}/static/js/popper-1.14.3.min.js"{{SRI "/static/js/popper-1.14.3.min.js"}}></script>
<script src="{{.URLPrefix}}/static/js/bootstrap-4.1.3.min.js"{{SRI "/static/js/bootstrap-4.1.3.min.js"}}></script>
{{if .U2FSignRequest}}
<script src="{{.URLPrefix}}/static/js/u2f-api.js" integrity="sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR"></script>
<script src="{{.URLPrefix}}/static/js/u2f.js" integrity="sha384-7zZy25ajTABErGlCQgcyRDpQDS9QVZv9o+95IfvCjWftQe20f411F1a39Ge5xmCe"></script>
<script src="{{.URLPrefix}}/static/js/u2f-api.js"{{SRI "/static/js/u2f-api.js"}}></script>
<script src="{{.URLPrefix}}/static/js/u2f.js"{{SRI "/static/js/u2f.js"}}></script>
{{end}}
{{if .IncludeLogoutScripts}}
<script src="{{.URLPrefix}}/static/js/logout.js" integrity="sha384-lChVngGLNFXetIJTSxc+scDpi1vsBL+7Xa4r2uZpQFP/6Y2z9eCDXe/Y4IUdklRD"></script>
<script src="{{.URLPrefix}}/static/js/logout.js"{{SRI "/static/js/logout.js"}}></script>
{{end}}
</body>
</html>
......@@ -1391,7 +1394,7 @@ func templatesPageHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "templates/page.html", size: 1865, mode: os.FileMode(420), modTime: time.Unix(1561757493, 0)}
info := bindataFileInfo{name: "templates/page.html", size: 1476, mode: os.FileMode(420), modTime: time.Unix(1576422396, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
......
......@@ -10,22 +10,34 @@ import (
"strings"
"git.autistici.org/id/auth"
"github.com/gorilla/sessions"
"github.com/gorilla/securecookie"
"github.com/mssola/user_agent"
)
func randomDeviceID() string {
const (
deviceIDCookieName = "_dev"
deviceIDCookieMaxAge = 10 * 365 * 86400
)
type DeviceID []byte
func (d DeviceID) String() string {
return hex.EncodeToString(d)
}
func randomDeviceID() DeviceID {
b := make([]byte, 8)
if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
panic(err)
}
return hex.EncodeToString(b)
return b
}
// Manager can provide DeviceInfo entries for incoming HTTP requests.
type Manager struct {
store sessions.Store
geodb *geoIPDb
sc *securecookie.SecureCookie
urlPrefix string
geodb *geoIPDb
}
// Config stores options for the device info manager.
......@@ -35,7 +47,7 @@ type Config struct {
}
// New returns a new Manager with the given configuration.
func New(config *Config) (*Manager, error) {
func New(config *Config, urlPrefix string) (*Manager, error) {
if config == nil {
config = &Config{}
}
......@@ -45,26 +57,33 @@ func New(config *Config) (*Manager, error) {
log.Printf("Warning: GeoIP disabled: %v", err)
}
// This should only happen in tests.
if config.AuthKey == "" {
log.Printf("Warning: device_manager.auth_key unset, generating temporary random secrets")
config.AuthKey = string(securecookie.GenerateRandomKey(64))
}
sc := securecookie.New([]byte(config.AuthKey), nil)
sc.MaxAge(deviceIDCookieMaxAge)
sc.SetSerializer(securecookie.NopEncoder{})
return &Manager{
geodb: geodb,
store: newStore([]byte(config.AuthKey)),
sc: sc,
geodb: geodb,
urlPrefix: urlPrefix,
}, nil
}
const deviceIDSessionName = "_dev"
// GetDeviceInfoFromRequest will retrieve or create a DeviceInfo
// object for the given request. It will always return a valid object.
// The ResponseWriter is needed to store the unique ID on the client
// when a new device info object is created.
func (m *Manager) GetDeviceInfoFromRequest(w http.ResponseWriter, req *http.Request) *auth.DeviceInfo {
session, _ := m.store.Get(req, deviceIDSessionName)
devID, ok := session.Values["id"].(string)
if !ok || devID == "" {
devID, ok := m.getDeviceCookie(req)
if !ok || len(devID) == 0 {
// Generate a new Device ID and save it on the client.
devID = randomDeviceID()
session.Values["id"] = devID
if err := session.Save(req, w); err != nil {
if err := m.setDeviceCookie(w, devID); err != nil {
// This is likely a misconfiguration issue, so
// we want to know about it.
log.Printf("error saving device manager session: %v", err)
......@@ -75,7 +94,7 @@ func (m *Manager) GetDeviceInfoFromRequest(w http.ResponseWriter, req *http.Requ
ua := user_agent.New(uaStr)
browser, _ := ua.Browser()
d := auth.DeviceInfo{
ID: devID,
ID: devID.String(),
UserAgent: uaStr,
Mobile: ua.Mobile(),
OS: ua.OS(),
......@@ -103,6 +122,33 @@ func (m *Manager) GetDeviceInfoFromRequest(w http.ResponseWriter, req *http.Requ
return &d
}
func (m *Manager) getDeviceCookie(r *http.Request) (DeviceID, bool) {
if cookie, err := r.Cookie(deviceIDCookieName); err == nil {
var value []byte
if err = m.sc.Decode(deviceIDCookieName, cookie.Value, &value); err == nil {
return DeviceID(value), true
}
}
return nil, false
}
func (m *Manager) setDeviceCookie(w http.ResponseWriter, value DeviceID) error {
encoded, err := m.sc.Encode(deviceIDCookieName, []byte(value))
if err != nil {