Pull request: 2470 session token

Merge in DNS/adguard-home from 2470-session-token to master

Updates #2470.

Squashed commit of the following:

commit 02e874404808ee23000c49b4b2980b049dc4d0e6
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Mar 1 20:11:35 2021 +0300

    home: imp time formating

commit 6f4a6c9b190b2672cecd3e3e31413b03d19f8771
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Mar 1 18:48:15 2021 +0300

    home: rm user's data from session token
This commit is contained in:
Eugene Burkov 2021-03-01 20:37:28 +03:00
parent 91403d0b95
commit 94e783d572
3 changed files with 68 additions and 26 deletions

View File

@ -29,6 +29,12 @@ and this project adheres to
[#2692]: https://github.com/AdguardTeam/AdGuardHome/issues/2692 [#2692]: https://github.com/AdguardTeam/AdGuardHome/issues/2692
[#2757]: https://github.com/AdguardTeam/AdGuardHome/issues/2757 [#2757]: https://github.com/AdguardTeam/AdGuardHome/issues/2757
### Security
- Session token doesn't contain user's information anymore ([#2470]).
[#2470]: https://github.com/AdguardTeam/AdGuardHome/issues/2470
## [v0.105.1] - 2021-02-15 ## [v0.105.1] - 2021-02-15

View File

@ -2,13 +2,10 @@ package home
import ( import (
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"math/big"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@ -20,8 +17,12 @@ import (
) )
const ( const (
cookieTTL = 365 * 24 // in hours // cookieTTL is given in hours.
cookieTTL = 365 * 24
sessionCookieName = "agh_session" sessionCookieName = "agh_session"
// sessionTokenSize is the length of session token in bytes.
sessionTokenSize = 16
) )
type session struct { type session struct {
@ -285,16 +286,29 @@ type loginJSON struct {
Password string `json:"password"` Password string `json:"password"`
} }
func getSession(u *User) ([]byte, error) { // newSessionToken returns cryptographically secure randomly generated slice of
maxSalt := big.NewInt(math.MaxUint32) // bytes of sessionTokenSize length.
salt, err := rand.Int(rand.Reader, maxSalt) //
// TODO(e.burkov): Think about using byte array instead of byte slice.
func newSessionToken() (data []byte, err error) {
randData := make([]byte, sessionTokenSize)
_, err = rand.Read(randData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
d := []byte(fmt.Sprintf("%s%s%s", salt, u.Name, u.PasswordHash)) return randData, nil
hash := sha256.Sum256(d) }
return hash[:], nil
// cookieTimeFormat is the format to be used in (time.Time).Format for cookie's
// expiry field.
const cookieTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
// cookieExpiryFormat returns the formatted exp to be used in cookie string.
// It's quite simple for now, but probably will be expanded in the future.
func cookieExpiryFormat(exp time.Time) (formatted string) {
return exp.Format(cookieTimeFormat)
} }
func (a *Auth) httpCookie(req loginJSON) (string, error) { func (a *Auth) httpCookie(req loginJSON) (string, error) {
@ -303,24 +317,23 @@ func (a *Auth) httpCookie(req loginJSON) (string, error) {
return "", nil return "", nil
} }
sess, err := getSession(&u) sess, err := newSessionToken()
if err != nil { if err != nil {
return "", err return "", err
} }
now := time.Now().UTC() now := time.Now().UTC()
expire := now.Add(cookieTTL * time.Hour)
expstr := expire.Format(time.RFC1123)
expstr = expstr[:len(expstr)-len("UTC")] // "UTC" -> "GMT"
expstr += "GMT"
s := session{} a.addSession(sess, &session{
s.userName = u.Name userName: u.Name,
s.expire = uint32(now.Unix()) + a.sessionTTL expire: uint32(now.Unix()) + a.sessionTTL,
a.addSession(sess, &s) })
return fmt.Sprintf("%s=%s; Path=/; HttpOnly; Expires=%s", return fmt.Sprintf(
sessionCookieName, hex.EncodeToString(sess), expstr), nil "%s=%s; Path=/; HttpOnly; Expires=%s",
sessionCookieName, hex.EncodeToString(sess),
cookieExpiryFormat(now.Add(cookieTTL*time.Hour)),
), nil
} }
func handleLogin(w http.ResponseWriter, r *http.Request) { func handleLogin(w http.ResponseWriter, r *http.Request) {

View File

@ -1,6 +1,8 @@
package home package home
import ( import (
"bytes"
"crypto/rand"
"encoding/hex" "encoding/hex"
"net/http" "net/http"
"net/url" "net/url"
@ -11,6 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -24,14 +27,34 @@ func prepareTestDir() string {
return dir return dir
} }
func TestNewSessionToken(t *testing.T) {
// Successful case.
token, err := newSessionToken()
require.Nil(t, err)
assert.Len(t, token, sessionTokenSize)
// Break the rand.Reader.
prevReader := rand.Reader
t.Cleanup(func() {
rand.Reader = prevReader
})
rand.Reader = &bytes.Buffer{}
// Unsuccessful case.
token, err = newSessionToken()
require.NotNil(t, err)
assert.Empty(t, token)
}
func TestAuth(t *testing.T) { func TestAuth(t *testing.T) {
dir := prepareTestDir() dir := prepareTestDir()
defer func() { _ = os.RemoveAll(dir) }() t.Cleanup(func() { _ = os.RemoveAll(dir) })
fn := filepath.Join(dir, "sessions.db") fn := filepath.Join(dir, "sessions.db")
users := []User{ users := []User{{
{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, Name: "name",
} PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
}}
a := InitAuth(fn, nil, 60) a := InitAuth(fn, nil, 60)
s := session{} s := session{}
@ -41,7 +64,7 @@ func TestAuth(t *testing.T) {
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound")) assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
a.RemoveSession("notfound") a.RemoveSession("notfound")
sess, err := getSession(&users[0]) sess, err := newSessionToken()
assert.Nil(t, err) assert.Nil(t, err)
sessStr := hex.EncodeToString(sess) sessStr := hex.EncodeToString(sess)