mirror of
https://github.com/photoprism/photoprism.git
synced 2025-09-26 21:01:58 +08:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -458,28 +458,61 @@ func (m *Session) SetData(data *SessionData) *Session {
|
||||
return m
|
||||
}
|
||||
|
||||
// SetContext updates the session's request context.
|
||||
// SetContext sets the session request context.
|
||||
func (m *Session) SetContext(c *gin.Context) *Session {
|
||||
if c == nil || m == nil {
|
||||
return m
|
||||
}
|
||||
|
||||
// Set client ip address.
|
||||
if ip := c.ClientIP(); ip != "" {
|
||||
// Set client ip address from request context.
|
||||
if ip := header.ClientIP(c); ip != "" {
|
||||
m.SetClientIP(ip)
|
||||
} else if m.ClientIP == "" {
|
||||
// Unit tests often do not set a client IP.
|
||||
m.SetClientIP(UnknownIP)
|
||||
}
|
||||
|
||||
// Set client user agent.
|
||||
if ua := c.GetHeader("User-Agent"); ua != "" {
|
||||
// Set client user agent from request context.
|
||||
if ua := header.UserAgent(c); ua != "" {
|
||||
m.SetUserAgent(ua)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// UpdateContext sets the session request context and updates the session entry in the database if it has changed.
|
||||
func (m *Session) UpdateContext(c *gin.Context) *Session {
|
||||
if c == nil || m == nil {
|
||||
return m
|
||||
}
|
||||
|
||||
changed := false
|
||||
|
||||
// Set client ip address from request context.
|
||||
if ip := header.ClientIP(c); ip != "" && (ip != m.ClientIP || m.LoginIP == "") {
|
||||
m.SetClientIP(ip)
|
||||
changed = true
|
||||
} else if m.ClientIP == "" {
|
||||
// Unit tests often do not set a client IP.
|
||||
m.SetClientIP(UnknownIP)
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Set client user agent from request context.
|
||||
if ua := header.UserAgent(c); ua != "" && ua != m.UserAgent {
|
||||
m.SetUserAgent(ua)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if !changed {
|
||||
return m
|
||||
} else if err := m.Save(); err != nil {
|
||||
log.Debugf("auth: %s while updating session context", err)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// IsVisitor checks if the session belongs to a sharing link visitor.
|
||||
func (m *Session) IsVisitor() bool {
|
||||
return m.User().IsVisitor()
|
||||
|
@@ -37,11 +37,11 @@ func FindSession(id string) (*Session, error) {
|
||||
event.AuditErr([]string{cached.IP(), "session %s", "failed to delete after expiration", "%s"}, cached.RefID, err)
|
||||
}
|
||||
} else if res := Db().First(&found, "id = ?", id); res.RecordNotFound() {
|
||||
return found, fmt.Errorf("not found")
|
||||
return found, fmt.Errorf("invalid session")
|
||||
} else if res.Error != nil {
|
||||
return found, res.Error
|
||||
} else if !rnd.IsSessionID(found.ID) {
|
||||
return found, fmt.Errorf("has invalid id %s", clean.LogQuote(found.ID))
|
||||
return found, fmt.Errorf("invalid session id %s", clean.LogQuote(found.ID))
|
||||
} else if !found.Expired() {
|
||||
found.UpdateLastActive()
|
||||
CacheSession(found, sessionCacheExpiration)
|
||||
@@ -50,7 +50,7 @@ func FindSession(id string) (*Session, error) {
|
||||
event.AuditErr([]string{found.IP(), "session %s", "failed to delete after expiration", "%s"}, found.RefID, err)
|
||||
}
|
||||
|
||||
return found, fmt.Errorf("expired")
|
||||
return found, fmt.Errorf("session expired")
|
||||
}
|
||||
|
||||
// FlushSessionCache resets the session cache.
|
||||
|
@@ -44,6 +44,22 @@ var SessionFixtures = SessionMap{
|
||||
AuthScope: clean.Scope("*"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "alice_token",
|
||||
LastActive: -1,
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
},
|
||||
"alice_token_webdav": {
|
||||
authToken: "bHcZP-YxRbi-irKII-W1kpz",
|
||||
ID: rnd.SessionID("bHcZP-YxRbi-irKII-W1kpz"),
|
||||
RefID: "sesshjtgx8qt",
|
||||
SessTimeout: -1,
|
||||
SessExpires: UnixTime() + UnixDay,
|
||||
AuthScope: clean.Scope("webdav"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "alice_token_webdav",
|
||||
LastActive: -1,
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
@@ -58,6 +74,7 @@ var SessionFixtures = SessionMap{
|
||||
AuthScope: clean.Scope("metrics photos albums videos"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "alice_token_scope",
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
@@ -108,6 +125,7 @@ var SessionFixtures = SessionMap{
|
||||
AuthScope: clean.Scope("metrics"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "visitor_token_metrics",
|
||||
user: &Visitor,
|
||||
UserUID: Visitor.UserUID,
|
||||
UserName: Visitor.UserName,
|
||||
@@ -131,6 +149,7 @@ var SessionFixtures = SessionMap{
|
||||
AuthScope: clean.Scope("metrics"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "token_metrics",
|
||||
user: nil,
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
@@ -146,6 +165,7 @@ var SessionFixtures = SessionMap{
|
||||
AuthScope: clean.Scope("settings"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "token_settings",
|
||||
user: nil,
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
|
35
internal/server/server_test.go
Normal file
35
internal/server/server_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Init test logger.
|
||||
log = logrus.StandardLogger()
|
||||
log.SetLevel(logrus.TraceLevel)
|
||||
event.AuditLog = log
|
||||
|
||||
// Init test config.
|
||||
c := config.TestConfig()
|
||||
get.SetConfig(c)
|
||||
|
||||
// Increase login rate limit for testing.
|
||||
limiter.Login = limiter.NewLimit(1, 10000)
|
||||
|
||||
// Run unit tests.
|
||||
code := m.Run()
|
||||
|
||||
// Close database connection.
|
||||
_ = c.CloseDb()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
gc "github.com/patrickmn/go-cache"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/api"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
@@ -19,13 +20,13 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// To improve performance, we use a basic auth cache
|
||||
// with an expiration time of about 5 minutes.
|
||||
var basicAuthExpiration = 5 * time.Minute
|
||||
var basicAuthCache = gc.New(basicAuthExpiration, basicAuthExpiration)
|
||||
var basicAuthMutex = sync.Mutex{}
|
||||
// Use auth cache to improve WebDAV performance. It has a standard expiration time of about 5 minutes.
|
||||
var webdavAuthExpiration = 5 * time.Minute
|
||||
var webdavAuthCache = gc.New(webdavAuthExpiration, webdavAuthExpiration)
|
||||
var webdavAuthMutex = sync.Mutex{}
|
||||
var BasicAuthRealm = "Basic realm=\"WebDAV Authorization Required\""
|
||||
|
||||
// WebDAVAuth checks authentication and authentication
|
||||
@@ -43,7 +44,7 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// To improve performance, check the cache for already authorized users.
|
||||
if user, found := basicAuthCache.Get(cacheKey); found && user != nil {
|
||||
if user, found := webdavAuthCache.Get(cacheKey); found && user != nil {
|
||||
// Add cached user information to the request context.
|
||||
c.Set(gin.AuthUserKey, user.(*entity.User))
|
||||
// Credentials have already been authorized within the configured
|
||||
@@ -62,7 +63,7 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Get basic authentication credentials.
|
||||
// Get basic authentication credentials, if any.
|
||||
username, password, cacheKey, authorized := basicAuth(c)
|
||||
|
||||
// Allow requests from already authorized users to be processed.
|
||||
@@ -70,17 +71,88 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Re-request authentication if credentials are missing or incomplete.
|
||||
if cacheKey == "" {
|
||||
c.Header("WWW-Authenticate", BasicAuthRealm)
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the client IP address from the request headers
|
||||
// for use in logs and to enforce request rate limits.
|
||||
clientIp := api.ClientIP(c)
|
||||
|
||||
// Get access token, if any.
|
||||
authToken := header.AuthToken(c)
|
||||
|
||||
// Use the value provided in the password field as auth secret if no username was provided
|
||||
// and the format matches.
|
||||
if username == "" && authToken == "" && rnd.IsAuthSecret(password) {
|
||||
authToken = password
|
||||
}
|
||||
|
||||
// Find client session if an auth token has been provided and perform authorization check.
|
||||
if authToken != "" {
|
||||
sid := rnd.SessionID(authToken)
|
||||
|
||||
// Check if client authorization has been cached to improve performance.
|
||||
if user, found := webdavAuthCache.Get(sid); found && user != nil {
|
||||
// Add cached user information to the request context.
|
||||
c.Set(gin.AuthUserKey, user.(*entity.User))
|
||||
return
|
||||
}
|
||||
|
||||
sess, err := entity.FindSession(sid)
|
||||
|
||||
if sess == nil {
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "access webdav", "invalid auth token"})
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
} else if err != nil {
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "access webdav", "%s"}, err.Error())
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
} else {
|
||||
sess.UpdateContext(c)
|
||||
}
|
||||
|
||||
// Required resource scope.
|
||||
resource := acl.ResourceWebDAV
|
||||
|
||||
// If the request is from a client application, check its authorization based
|
||||
// on the allowed scope, the ACL, and the user account it belongs to (if any).
|
||||
if sess.IsClient() {
|
||||
// Check if client belongs to a user and if the "webdav" scope is set.
|
||||
if !sess.HasScope(resource.String()) || !sess.HasUser() {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "access webdav", "denied"}, clean.Log(sess.AuthID), sess.RefID)
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check authorization and grant access if successful.
|
||||
if !sess.HasUser() {
|
||||
event.AuditErr([]string{clientIp, "session %s", "access webdav as unauthorized user", "denied"}, sess.RefID)
|
||||
} else if user := sess.User(); !user.CanUseWebDAV() {
|
||||
// Sync disabled for this account.
|
||||
message := "sync disabled"
|
||||
event.AuditWarn([]string{clientIp, "access webdav as %s", message}, clean.LogQuote(username))
|
||||
} else if err = os.MkdirAll(filepath.Join(conf.OriginalsPath(), user.GetUploadPath()), fs.ModeDir); err != nil {
|
||||
message := "failed to create user upload path"
|
||||
event.AuditWarn([]string{clientIp, "access webdav as %s", message}, clean.LogQuote(username))
|
||||
} else {
|
||||
// Cache successful authentication to improve performance.
|
||||
webdavAuthCache.SetDefault(sid, user)
|
||||
c.Set(gin.AuthUserKey, user)
|
||||
return
|
||||
}
|
||||
|
||||
// Request authentication.
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-request authentication if credentials are missing or incomplete.
|
||||
if cacheKey == "" {
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Check the authentication request rate to block the client after
|
||||
// too many failed attempts (10/req per minute by default).
|
||||
if limiter.Login.Reject(clientIp) {
|
||||
@@ -88,8 +160,8 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
basicAuthMutex.Lock()
|
||||
defer basicAuthMutex.Unlock()
|
||||
webdavAuthMutex.Lock()
|
||||
defer webdavAuthMutex.Unlock()
|
||||
|
||||
// User credentials.
|
||||
f := form.Login{
|
||||
@@ -124,14 +196,19 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||
event.AuditInfo([]string{clientIp, "webdav login as %s", "succeeded"}, clean.LogQuote(username))
|
||||
event.LoginInfo(clientIp, "webdav", username, api.UserAgent(c))
|
||||
|
||||
// Cache successful authentication.
|
||||
basicAuthCache.SetDefault(cacheKey, user)
|
||||
// Cache successful authentication to improve performance.
|
||||
webdavAuthCache.SetDefault(cacheKey, user)
|
||||
c.Set(gin.AuthUserKey, user)
|
||||
return
|
||||
}
|
||||
|
||||
// Abort request.
|
||||
c.Header("WWW-Authenticate", BasicAuthRealm)
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
// Request authentication.
|
||||
WebDAVAbortUnauthorized(c)
|
||||
}
|
||||
}
|
||||
|
||||
// WebDAVAbortUnauthorized aborts the request with the status unauthorized and requests authentication.
|
||||
func WebDAVAbortUnauthorized(c *gin.Context) {
|
||||
c.Header("WWW-Authenticate", BasicAuthRealm)
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
}
|
||||
|
106
internal/server/webdav_auth_test.go
Normal file
106
internal/server/webdav_auth_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
func TestWebDAVAuth(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
webdavHandler := WebDAVAuth(conf)
|
||||
|
||||
t.Run("Unauthorized", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
webdavHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, c.Writer.Status())
|
||||
assert.Equal(t, BasicAuthRealm, c.Writer.Header().Get("WWW-Authenticate"))
|
||||
})
|
||||
t.Run("AliceToken", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
sess := entity.SessionFixtures.Get("alice_token")
|
||||
header.SetAuthorization(c.Request, sess.AuthToken())
|
||||
|
||||
webdavHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||
assert.Equal(t, "", c.Writer.Header().Get("WWW-Authenticate"))
|
||||
})
|
||||
t.Run("AliceTokenWebdav", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
sess := entity.SessionFixtures.Get("alice_token_webdav")
|
||||
header.SetAuthorization(c.Request, sess.AuthToken())
|
||||
|
||||
webdavHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||
assert.Equal(t, "", c.Writer.Header().Get("WWW-Authenticate"))
|
||||
})
|
||||
t.Run("AliceTokenScope", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
sess := entity.SessionFixtures.Get("alice_token_scope")
|
||||
header.SetAuthorization(c.Request, sess.AuthToken())
|
||||
|
||||
webdavHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, c.Writer.Status())
|
||||
assert.Equal(t, BasicAuthRealm, c.Writer.Header().Get("WWW-Authenticate"))
|
||||
})
|
||||
t.Run("InvalidAuthToken", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
header.SetAuthorization(c.Request, rnd.AuthToken())
|
||||
|
||||
webdavHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, c.Writer.Status())
|
||||
assert.Equal(t, BasicAuthRealm, c.Writer.Header().Get("WWW-Authenticate"))
|
||||
})
|
||||
t.Run("InvalidAuthSecret", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
header.SetAuthorization(c.Request, rnd.AuthSecret())
|
||||
|
||||
webdavHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, c.Writer.Status())
|
||||
assert.Equal(t, BasicAuthRealm, c.Writer.Header().Get("WWW-Authenticate"))
|
||||
})
|
||||
}
|
91
pkg/rnd/auth.go
Normal file
91
pkg/rnd/auth.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package rnd
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const (
|
||||
SessionIdLength = 64
|
||||
AuthTokenLength = 48
|
||||
AuthSecretLength = 23
|
||||
AuthSecretSeparator = '-'
|
||||
)
|
||||
|
||||
// AuthToken returns a random hex encoded string that can be used for authentication.
|
||||
//
|
||||
// Examples: 9fa8e562564dac91b96881040e98f6719212a1a364e0bb25
|
||||
func AuthToken() string {
|
||||
b := make([]byte, 24)
|
||||
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
// IsAuthToken checks if the string is a session id.
|
||||
func IsAuthToken(s string) bool {
|
||||
if l := len(s); l == AuthTokenLength {
|
||||
return IsHex(s)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AuthSecret returns a random, human-friendly token that can be used for authentication.
|
||||
// It is separated by 3 dashes for better readability and has a total length of 23 characters.
|
||||
//
|
||||
// Example: iXrDz-aY16n-4IUWM-otkM3
|
||||
func AuthSecret() string {
|
||||
m := big.NewInt(int64(len(CharsetBase62)))
|
||||
b := make([]byte, AuthSecretLength)
|
||||
|
||||
for i := range b {
|
||||
if r, err := rand.Int(rand.Reader, m); err == nil {
|
||||
if (i+1)%6 == 0 {
|
||||
b[i] = AuthSecretSeparator
|
||||
} else {
|
||||
b[i] = CharsetBase62[r.Int64()]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// IsAuthSecret returns true if the string only contains alphanumeric ascii chars without whitespace.
|
||||
func IsAuthSecret(s string) bool {
|
||||
if len(s) != AuthSecretLength {
|
||||
return false
|
||||
}
|
||||
|
||||
sep := 0
|
||||
|
||||
for _, r := range s {
|
||||
if r == AuthSecretSeparator {
|
||||
sep++
|
||||
} else if (r < '0' || r > '9') && (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return sep == AuthSecretLength/6
|
||||
}
|
||||
|
||||
// SessionID returns the hashed session id string.
|
||||
func SessionID(token string) string {
|
||||
return Sha256([]byte(token))
|
||||
}
|
||||
|
||||
// IsSessionID checks if the string is a session id string.
|
||||
func IsSessionID(id string) bool {
|
||||
if len(id) != SessionIdLength {
|
||||
return false
|
||||
}
|
||||
|
||||
return IsHex(id)
|
||||
}
|
101
pkg/rnd/auth_test.go
Normal file
101
pkg/rnd/auth_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package rnd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAuthToken(t *testing.T) {
|
||||
result := AuthToken()
|
||||
assert.Equal(t, AuthTokenLength, len(result))
|
||||
assert.True(t, IsAuthToken(result))
|
||||
assert.True(t, IsHex(result))
|
||||
|
||||
for n := 0; n < 10; n++ {
|
||||
s := AuthToken()
|
||||
t.Logf("AuthToken %d: %s", n, s)
|
||||
assert.NotEmpty(t, s)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAuthToken(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
AuthToken()
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAuthToken(t *testing.T) {
|
||||
assert.True(t, IsAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"))
|
||||
assert.True(t, IsAuthToken(AuthToken()))
|
||||
assert.True(t, IsAuthToken(AuthToken()))
|
||||
assert.False(t, IsAuthToken(SessionID(AuthToken())))
|
||||
assert.False(t, IsAuthToken(SessionID(AuthToken())))
|
||||
assert.False(t, IsAuthToken("55785BAC-9H4B-4747-B090-EE123FFEE437"))
|
||||
assert.False(t, IsAuthToken("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
assert.False(t, IsAuthToken(""))
|
||||
}
|
||||
|
||||
func BenchmarkIsAuthToken(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
IsAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthSecret(t *testing.T) {
|
||||
for n := 0; n < 10; n++ {
|
||||
s := AuthSecret()
|
||||
t.Logf("AuthSecret %d: %s", n, s)
|
||||
assert.Equal(t, AuthSecretLength, len(s))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAuthSecret(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
AuthSecret()
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAuthSecret(t *testing.T) {
|
||||
assert.True(t, IsAuthSecret("f64nl-GQbmZ-CEcLr-Q1VSr"))
|
||||
assert.True(t, IsAuthSecret("XRhOW-DM2ol-INove-dAg6m"))
|
||||
assert.True(t, IsAuthSecret(AuthSecret()))
|
||||
assert.True(t, IsAuthSecret(AuthSecret()))
|
||||
assert.False(t, IsAuthSecret(AuthToken()))
|
||||
assert.False(t, IsAuthSecret(AuthToken()))
|
||||
assert.False(t, IsAuthSecret(SessionID(AuthToken())))
|
||||
assert.False(t, IsAuthSecret(SessionID(AuthToken())))
|
||||
assert.False(t, IsAuthSecret("55785BAC-9H4B-4747-B090-EE123FFEE437"))
|
||||
assert.False(t, IsAuthSecret("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
assert.False(t, IsAuthSecret("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"))
|
||||
assert.False(t, IsAuthSecret(""))
|
||||
}
|
||||
|
||||
func BenchmarkIsAuthSecret(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
IsAuthSecret("f64nl-GQbmZ-CEcLr-Q1VSr")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionID(t *testing.T) {
|
||||
result := SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2")
|
||||
assert.Equal(t, SessionIdLength, len(result))
|
||||
assert.Equal(t, "f22383a703805a031a9835c8c6b6dafb793a21e8f33d0b4887b4ec9bd7ac8cd5", result)
|
||||
|
||||
for n := 0; n < 10; n++ {
|
||||
s := SessionID(AuthToken())
|
||||
t.Logf("SessionID %d: %s", n, s)
|
||||
assert.NotEmpty(t, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSessionID(t *testing.T) {
|
||||
assert.False(t, IsSessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"))
|
||||
assert.False(t, IsSessionID(AuthToken()))
|
||||
assert.False(t, IsSessionID(AuthToken()))
|
||||
assert.True(t, IsSessionID(SessionID(AuthToken())))
|
||||
assert.True(t, IsSessionID(SessionID(AuthToken())))
|
||||
assert.False(t, IsSessionID("55785BAC-9H4B-4747-B090-EE123FFEE437"))
|
||||
assert.False(t, IsSessionID("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
assert.False(t, IsSessionID(""))
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
package rnd
|
||||
|
||||
// GeneratePasscode returns a random 16-digit passcode that can, for example, be used as an app password.
|
||||
// It is separated by 3 dashes for better readability, resulting in a total length of 19 characters.
|
||||
func GeneratePasscode() string {
|
||||
code := make([]byte, 0, 19)
|
||||
code = append(code, Base62(4)...)
|
||||
|
||||
for n := 0; n < 3; n++ {
|
||||
code = append(code, "-"+Base62(4)...)
|
||||
}
|
||||
|
||||
return string(code)
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
package rnd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGeneratePasscode(t *testing.T) {
|
||||
for n := 0; n < 10; n++ {
|
||||
s := GeneratePasscode()
|
||||
t.Logf("Passcode %d: %s", n, s)
|
||||
assert.Equal(t, 19, len(s))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGeneratePasscode(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
GeneratePasscode()
|
||||
}
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
package rnd
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// AuthToken returns a new session id.
|
||||
func AuthToken() string {
|
||||
b := make([]byte, 24)
|
||||
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
// IsAuthToken checks if the string is a session id.
|
||||
func IsAuthToken(s string) bool {
|
||||
if len(s) != 48 {
|
||||
return false
|
||||
}
|
||||
|
||||
return IsHex(s)
|
||||
}
|
||||
|
||||
// SessionID returns the hashed session id string.
|
||||
func SessionID(s string) string {
|
||||
return Sha256([]byte(s))
|
||||
}
|
||||
|
||||
// IsSessionID checks if the string is a session id string.
|
||||
func IsSessionID(s string) bool {
|
||||
if len(s) != 64 {
|
||||
return false
|
||||
}
|
||||
|
||||
return IsHex(s)
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
package rnd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAuthToken(t *testing.T) {
|
||||
result := AuthToken()
|
||||
assert.Equal(t, 48, len(result))
|
||||
assert.True(t, IsAuthToken(result))
|
||||
assert.True(t, IsHex(result))
|
||||
}
|
||||
|
||||
func TestIsAuthToken(t *testing.T) {
|
||||
assert.True(t, IsAuthToken("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"))
|
||||
assert.True(t, IsAuthToken(AuthToken()))
|
||||
assert.True(t, IsAuthToken(AuthToken()))
|
||||
assert.False(t, IsAuthToken(SessionID(AuthToken())))
|
||||
assert.False(t, IsAuthToken(SessionID(AuthToken())))
|
||||
assert.False(t, IsAuthToken("55785BAC-9H4B-4747-B090-EE123FFEE437"))
|
||||
assert.False(t, IsAuthToken("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
assert.False(t, IsAuthToken(""))
|
||||
}
|
||||
|
||||
func TestSessionID(t *testing.T) {
|
||||
result := SessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2")
|
||||
assert.Equal(t, 64, len(result))
|
||||
assert.Equal(t, "f22383a703805a031a9835c8c6b6dafb793a21e8f33d0b4887b4ec9bd7ac8cd5", result)
|
||||
}
|
||||
|
||||
func TestIsSessionID(t *testing.T) {
|
||||
assert.False(t, IsSessionID("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2"))
|
||||
assert.False(t, IsSessionID(AuthToken()))
|
||||
assert.False(t, IsSessionID(AuthToken()))
|
||||
assert.True(t, IsSessionID(SessionID(AuthToken())))
|
||||
assert.True(t, IsSessionID(SessionID(AuthToken())))
|
||||
assert.False(t, IsSessionID("55785BAC-9H4B-4747-B090-EE123FFEE437"))
|
||||
assert.False(t, IsSessionID("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
assert.False(t, IsSessionID(""))
|
||||
}
|
@@ -97,7 +97,7 @@ func IsHex(s string) bool {
|
||||
}
|
||||
|
||||
for _, r := range s {
|
||||
if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 70) && r != 45 {
|
||||
if (r < '0' || r > '9') && (r < 'a' || r > 'f') && (r < 'A' || r > 'F') && r != '-' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user