Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
d3a67a6694
commit
a4e2bb33b9
21 changed files with 171 additions and 162 deletions
|
@ -39,6 +39,8 @@ export class Session extends RestModel {
|
|||
UserUID: "",
|
||||
UserName: "",
|
||||
UserAgent: "",
|
||||
ClientUID: "",
|
||||
ClientName: "",
|
||||
AuthProvider: "",
|
||||
AuthMethod: "",
|
||||
AuthDomain: "",
|
||||
|
|
|
@ -22,6 +22,7 @@ export const Providers = () => {
|
|||
local: $gettext("Local"),
|
||||
client: $gettext("Client"),
|
||||
client_credentials: $gettext("Client Credentials"),
|
||||
application: $gettext("Application"),
|
||||
access_token: $gettext("Access Token"),
|
||||
password: $gettext("Local"),
|
||||
ldap: $gettext("LDAP/AD"),
|
||||
|
@ -36,11 +37,12 @@ export const Methods = () => {
|
|||
return {
|
||||
"": $gettext("Default"),
|
||||
default: $gettext("Default"),
|
||||
personal: $gettext("Personal"),
|
||||
access_token: $gettext("Access Token"),
|
||||
session: $gettext("Session"),
|
||||
totp: "TOTP/2FA",
|
||||
personal: $gettext("Personal"),
|
||||
client: $gettext("Client"),
|
||||
access_token: $gettext("Access Token"),
|
||||
oauth2: "OAuth2",
|
||||
totp: "TOTP/2FA",
|
||||
oidc: "OIDC",
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ import (
|
|||
var AuthAddFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name, n",
|
||||
Usage: "access `TOKEN` name to help identify the client application",
|
||||
Usage: "`CLIENT` name to help identify the application",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "scope, s",
|
||||
|
@ -25,19 +25,20 @@ var AuthAddFlags = []cli.Flag{
|
|||
},
|
||||
cli.Int64Flag{
|
||||
Name: "expires, e",
|
||||
Usage: "authentication `LIFETIME` in seconds, after which the access token expires (-1 to disable the limit)",
|
||||
Usage: "authentication `LIFETIME` in seconds, after which access expires (-1 to disable the limit)",
|
||||
Value: entity.UnixYear,
|
||||
},
|
||||
}
|
||||
|
||||
// AuthAddCommand configures the command name, flags, and action.
|
||||
var AuthAddCommand = cli.Command{
|
||||
Name: "add",
|
||||
Usage: "Creates a new access token for client authentication",
|
||||
Description: "If you provide a username as argument, a personal access token for registered users will be created.",
|
||||
ArgsUsage: "[username]",
|
||||
Flags: AuthAddFlags,
|
||||
Action: authAddAction,
|
||||
Name: "add",
|
||||
Usage: "Adds a new authentication secret for client applications",
|
||||
Description: "If you specify a username as argument, an app password will be created for this user account." +
|
||||
" It can be used as a password replacement to grant limited access to client applications.",
|
||||
ArgsUsage: "[username]",
|
||||
Flags: AuthAddFlags,
|
||||
Action: authAddAction,
|
||||
}
|
||||
|
||||
// authAddAction shows detailed session information.
|
||||
|
@ -89,22 +90,21 @@ func authAddAction(ctx *cli.Context) error {
|
|||
authScope = clean.Scope(res)
|
||||
}
|
||||
|
||||
// Create session with client access token.
|
||||
sess, err := entity.CreateClientAccessToken(clientName, ctx.Int64("expires"), authScope, user)
|
||||
// Create session and show the authentication secret.
|
||||
sess, err := entity.AddClientAuthentication(clientName, ctx.Int64("expires"), authScope, user)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create access token: %s", err)
|
||||
return fmt.Errorf("failed to create authentication secret: %s", err)
|
||||
} else {
|
||||
// Show client authentication credentials.
|
||||
if sess.UserUID == "" {
|
||||
fmt.Printf("\nPLEASE WRITE DOWN THE FOLLOWING RANDOMLY GENERATED CLIENT ACCESS TOKEN, AS YOU WILL NOT BE ABLE TO SEE IT AGAIN:\n")
|
||||
fmt.Printf("\nPLEASE WRITE DOWN THE FOLLOWING RANDOMLY GENERATED ACCESS TOKEN, AS YOU WILL NOT BE ABLE TO SEE IT AGAIN:\n")
|
||||
fmt.Printf("\n%s\n", report.Credentials("Access Token", sess.AuthToken(), "Authorization Scope", sess.Scope()))
|
||||
} else {
|
||||
fmt.Printf("\nPLEASE WRITE DOWN THE FOLLOWING RANDOMLY GENERATED PERSONAL ACCESS TOKEN, AS YOU WILL NOT BE ABLE TO SEE IT AGAIN:\n")
|
||||
fmt.Printf("\nPLEASE WRITE DOWN THE FOLLOWING RANDOMLY GENERATED APP PASSWORD, AS YOU WILL NOT BE ABLE TO SEE IT AGAIN:\n")
|
||||
fmt.Printf("\n%s\n", report.Credentials("App Password", sess.AuthToken(), "Authorization Scope", sess.Scope()))
|
||||
}
|
||||
|
||||
result := report.Credentials("Access Token", sess.AuthToken(), "Authorization Scope", sess.Scope())
|
||||
|
||||
fmt.Printf("\n%s\n", result)
|
||||
}
|
||||
|
||||
return err
|
||||
|
|
|
@ -25,7 +25,7 @@ func authListAction(ctx *cli.Context) error {
|
|||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
var rows [][]string
|
||||
|
||||
cols := []string{"Session ID", "Session User", "Session Client", "Authentication Method", "Scope", "Login IP", "Current IP", "Last Active", "Created At", "Expires At"}
|
||||
cols := []string{"Session ID", "Session User", "Client Name", "Authentication Method", "Scope", "Login IP", "Current IP", "Last Active", "Created At", "Expires At"}
|
||||
|
||||
if ctx.Bool("tokens") {
|
||||
cols = append(cols, "Preview Token", "Download Token")
|
||||
|
|
|
@ -87,7 +87,7 @@ func clientsAddAction(ctx *cli.Context) error {
|
|||
log.Infof("successfully registered new client %s", clean.LogQuote(client.ClientName))
|
||||
|
||||
// Display client details.
|
||||
cols := []string{"Client ID", "Client Name", "Client User", "Authentication Method", "Role", "Scope", "Enabled", "Authentication Expires", "Created At"}
|
||||
cols := []string{"Client ID", "Client Name", "Authentication Method", "User", "Role", "Scope", "Enabled", "Authentication Expires", "Created At"}
|
||||
rows := make([][]string, 1)
|
||||
|
||||
var authExpires string
|
||||
|
@ -104,8 +104,8 @@ func clientsAddAction(ctx *cli.Context) error {
|
|||
rows[0] = []string{
|
||||
client.UID(),
|
||||
client.Name(),
|
||||
client.UserInfo(),
|
||||
client.AuthInfo(),
|
||||
client.UserInfo(),
|
||||
client.AclRole().String(),
|
||||
client.Scope(),
|
||||
report.Bool(client.AuthEnabled, report.Yes, report.No),
|
||||
|
|
|
@ -23,7 +23,7 @@ var ClientsListCommand = cli.Command{
|
|||
// clientsListAction lists registered client applications
|
||||
func clientsListAction(ctx *cli.Context) error {
|
||||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
cols := []string{"Client ID", "Client Name", "Client User", "Authentication Method", "Role", "Scope", "Enabled", "Authentication Expires", "Created At"}
|
||||
cols := []string{"Client ID", "Client Name", "Authentication Method", "User", "Role", "Scope", "Enabled", "Authentication Expires", "Created At"}
|
||||
|
||||
// Fetch clients from database.
|
||||
clients, err := query.Clients(ctx.Int("n"), 0, "", ctx.Args().First())
|
||||
|
@ -58,8 +58,8 @@ func clientsListAction(ctx *cli.Context) error {
|
|||
rows[i] = []string{
|
||||
client.UID(),
|
||||
client.Name(),
|
||||
client.UserInfo(),
|
||||
client.AuthInfo(),
|
||||
client.UserInfo(),
|
||||
client.AclRole().String(),
|
||||
client.Scope(),
|
||||
report.Bool(client.AuthEnabled, report.Yes, report.No),
|
||||
|
|
|
@ -62,7 +62,7 @@ func TestClientsListCommand(t *testing.T) {
|
|||
// Check command output for plausibility.
|
||||
// t.Logf(output)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, output, "Client ID;Client Name;Client User;Authentication Method;Role;Scope;")
|
||||
assert.Contains(t, output, "Client ID;Client Name;Authentication Method;User;Role;Scope;Enabled;Authentication Expires;Created At")
|
||||
assert.Contains(t, output, "Monitoring")
|
||||
assert.Contains(t, output, "metrics")
|
||||
assert.NotContains(t, output, "bob")
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUsersModCommand(t *testing.T) {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUsersRemoveCommand(t *testing.T) {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUsersCommand(t *testing.T) {
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// NewClientAccessToken returns a new access token session instance
|
||||
// that can be used to access the API with unregistered clients.
|
||||
func NewClientAccessToken(clientName string, lifetime int64, scope string, user *User) *Session {
|
||||
sess := NewSession(lifetime, 0)
|
||||
|
||||
if clientName == "" {
|
||||
clientName = rnd.Name()
|
||||
}
|
||||
|
||||
sess.SetClientName(clientName)
|
||||
sess.SetProvider(authn.ProviderAccessToken)
|
||||
sess.SetScope(scope)
|
||||
|
||||
if user != nil {
|
||||
sess.SetUser(user)
|
||||
sess.SetAuthToken(rnd.AuthSecret())
|
||||
sess.SetMethod(authn.MethodPersonal)
|
||||
} else {
|
||||
sess.SetMethod(authn.MethodDefault)
|
||||
}
|
||||
|
||||
return sess
|
||||
}
|
||||
|
||||
// CreateClientAccessToken initializes and creates a new access token session
|
||||
// that can be used to access the API with unregistered clients.
|
||||
func CreateClientAccessToken(clientName string, lifetime int64, scope string, user *User) (*Session, error) {
|
||||
sess := NewClientAccessToken(clientName, lifetime, scope, user)
|
||||
|
||||
if err := sess.Create(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
}
|
41
internal/entity/auth_session_client.go
Normal file
41
internal/entity/auth_session_client.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// NewClientAuthentication returns a new session that authenticates a client application.
|
||||
func NewClientAuthentication(clientName string, lifetime int64, scope string, user *User) *Session {
|
||||
sess := NewSession(lifetime, 0)
|
||||
|
||||
if clientName == "" {
|
||||
clientName = rnd.Name()
|
||||
}
|
||||
|
||||
sess.SetClientName(clientName)
|
||||
sess.SetScope(scope)
|
||||
|
||||
if user != nil {
|
||||
sess.SetUser(user)
|
||||
sess.SetAuthToken(rnd.AppPassword())
|
||||
sess.SetProvider(authn.ProviderApplication)
|
||||
sess.SetMethod(authn.MethodDefault)
|
||||
} else {
|
||||
sess.SetProvider(authn.ProviderAccessToken)
|
||||
sess.SetMethod(authn.MethodOAuth2)
|
||||
}
|
||||
|
||||
return sess
|
||||
}
|
||||
|
||||
// AddClientAuthentication creates a new session for authenticating a client application.
|
||||
func AddClientAuthentication(clientName string, lifetime int64, scope string, user *User) (*Session, error) {
|
||||
sess := NewClientAuthentication(clientName, lifetime, scope, user)
|
||||
|
||||
if err := sess.Create(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
}
|
|
@ -6,9 +6,9 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewClientAccessToken(t *testing.T) {
|
||||
func TestNewClientAuthentication(t *testing.T) {
|
||||
t.Run("Anonymous", func(t *testing.T) {
|
||||
sess := NewClientAccessToken("Anonymous", UnixDay, "metrics", nil)
|
||||
sess := NewClientAuthentication("Anonymous", UnixDay, "metrics", nil)
|
||||
|
||||
if sess == nil {
|
||||
t.Fatal("session must not be nil")
|
||||
|
@ -23,7 +23,7 @@ func TestNewClientAccessToken(t *testing.T) {
|
|||
t.Fatal("user must not be nil")
|
||||
}
|
||||
|
||||
sess := NewClientAccessToken("alice", UnixDay, "metrics", user)
|
||||
sess := NewClientAuthentication("alice", UnixDay, "metrics", user)
|
||||
|
||||
if sess == nil {
|
||||
t.Fatal("session must not be nil")
|
||||
|
@ -38,7 +38,7 @@ func TestNewClientAccessToken(t *testing.T) {
|
|||
t.Fatal("user must not be nil")
|
||||
}
|
||||
|
||||
sess := NewClientAccessToken("alice", UnixDay, "", user)
|
||||
sess := NewClientAuthentication("alice", UnixDay, "", user)
|
||||
|
||||
if sess == nil {
|
||||
t.Fatal("session must not be nil")
|
||||
|
@ -53,7 +53,7 @@ func TestNewClientAccessToken(t *testing.T) {
|
|||
t.Fatal("user must not be nil")
|
||||
}
|
||||
|
||||
sess := NewClientAccessToken("", 0, "metrics", user)
|
||||
sess := NewClientAuthentication("", 0, "metrics", user)
|
||||
|
||||
if sess == nil {
|
||||
t.Fatal("session must not be nil")
|
||||
|
@ -63,9 +63,9 @@ func TestNewClientAccessToken(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestCreateClientAccessToken(t *testing.T) {
|
||||
func TestAddClientAuthentication(t *testing.T) {
|
||||
t.Run("Anonymous", func(t *testing.T) {
|
||||
sess, err := CreateClientAccessToken("", UnixDay, "metrics", nil)
|
||||
sess, err := AddClientAuthentication("", UnixDay, "metrics", nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -82,7 +82,7 @@ func TestCreateClientAccessToken(t *testing.T) {
|
|||
t.Fatal("user must not be nil")
|
||||
}
|
||||
|
||||
sess, err := CreateClientAccessToken("My Client App Token", UnixDay, "metrics", user)
|
||||
sess, err := AddClientAuthentication("My Client App Token", UnixDay, "metrics", user)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -44,10 +44,10 @@ var Auth = func(f form.Login, m *Session, c *gin.Context) (user *User, provider
|
|||
func AuthSession(f form.Login, c *gin.Context) (sess *Session, user *User, err error) {
|
||||
if f.Password == "" {
|
||||
// Abort authentication if no token was provided.
|
||||
return nil, nil, fmt.Errorf("no auth secret provided")
|
||||
} else if !rnd.IsAuthSecret(f.Password, true) {
|
||||
return nil, nil, fmt.Errorf("no password provided")
|
||||
} else if !rnd.IsAppPassword(f.Password, true) {
|
||||
// Abort authentication if token doesn't match expected format.
|
||||
return nil, nil, fmt.Errorf("auth secret does not match expected format")
|
||||
return nil, nil, fmt.Errorf("password does not match expected format")
|
||||
}
|
||||
|
||||
// Get session ID for the auth token provided.
|
||||
|
@ -58,7 +58,7 @@ func AuthSession(f form.Login, c *gin.Context) (sess *Session, user *User, err e
|
|||
|
||||
// Log error and return nil if no matching session was found.
|
||||
if sess == nil || err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid auth secret")
|
||||
return nil, nil, fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
// Update the client IP and the user agent from
|
||||
|
@ -113,14 +113,14 @@ func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (authn.Prov
|
|||
if !authUser.IsRegistered() || authUser.UserUID != user.UserUID {
|
||||
message := "incorrect user"
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "session %s", "login as %s with auth secret", message}, m.RefID, clean.LogQuote(userName))
|
||||
event.AuditErr([]string{clientIp, "session %s", "login as %s with app password", message}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
} else if !authSess.IsClient() || !authSess.HasScope(acl.ResourceSessions.String()) {
|
||||
message := "unauthorized"
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "session %s", "login as %s with auth secret", message}, m.RefID, clean.LogQuote(userName))
|
||||
event.AuditErr([]string{clientIp, "session %s", "login as %s with app password", message}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
|
@ -129,9 +129,9 @@ func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (authn.Prov
|
|||
m.ClientName = authSess.ClientName
|
||||
m.SetScope(authSess.Scope())
|
||||
m.SetMethod(authn.MethodSession)
|
||||
event.AuditInfo([]string{clientIp, "session %s", "login as %s with auth secret", "succeeded"}, m.RefID, clean.LogQuote(userName))
|
||||
event.AuditInfo([]string{clientIp, "session %s", "login as %s with app password", "succeeded"}, m.RefID, clean.LogQuote(userName))
|
||||
event.LoginInfo(clientIp, "api", userName, m.UserAgent)
|
||||
return authn.ProviderClient, err
|
||||
return authn.ProviderApplication, err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,11 +15,11 @@ import (
|
|||
)
|
||||
|
||||
func TestAuthSession(t *testing.T) {
|
||||
t.Run("RandomAuthSecret", func(t *testing.T) {
|
||||
t.Run("RandomAppPassword", func(t *testing.T) {
|
||||
// Create test request form.
|
||||
f := form.Login{
|
||||
UserName: "alice",
|
||||
Password: rnd.AuthSecret(),
|
||||
Password: rnd.AppPassword(),
|
||||
}
|
||||
|
||||
// Create test request context.
|
||||
|
|
|
@ -83,8 +83,8 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
|||
authToken := header.AuthToken(c)
|
||||
|
||||
// Use the value provided in the password field as auth token if no username was provided
|
||||
// and the format matches auth secrets e.g. "OXiV72-wTtiL9-d04jO7-X7XP4p".
|
||||
if username != "" && authToken == "" && rnd.IsAuthSecret(password, true) {
|
||||
// and the format matches an app password e.g. "OXiV72-wTtiL9-d04jO7-X7XP4p".
|
||||
if username != "" && authToken == "" && rnd.IsAppPassword(password, true) {
|
||||
authToken = password
|
||||
}
|
||||
|
||||
|
|
|
@ -133,14 +133,14 @@ func TestWebDAVAuth(t *testing.T) {
|
|||
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) {
|
||||
t.Run("InvalidAppPassword", 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())
|
||||
header.SetAuthorization(c.Request, rnd.AppPassword())
|
||||
|
||||
webdavAuthCache.Flush()
|
||||
webdavHandler(c)
|
||||
|
@ -228,18 +228,18 @@ func TestWebDAVAuthSession(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||
assert.Equal(t, "", c.Writer.Header().Get("WWW-Authenticate"))
|
||||
})
|
||||
t.Run("InvalidAuthSecret", func(t *testing.T) {
|
||||
t.Run("InvalidAppPassword", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
authToken := rnd.AuthSecret()
|
||||
authId := rnd.SessionID(authToken)
|
||||
appPassword := rnd.AppPassword()
|
||||
authId := rnd.SessionID(appPassword)
|
||||
|
||||
// Get session with invalid auth secret.
|
||||
sess, user, sid, cached := WebDAVAuthSession(c, authToken)
|
||||
// Get session with invalid app password.
|
||||
sess, user, sid, cached := WebDAVAuthSession(c, appPassword)
|
||||
|
||||
// Check result.
|
||||
assert.Nil(t, sess)
|
||||
|
|
|
@ -16,6 +16,7 @@ const (
|
|||
ProviderDefault ProviderType = "default"
|
||||
ProviderClient ProviderType = "client"
|
||||
ProviderClientCredentials ProviderType = "client_credentials"
|
||||
ProviderApplication ProviderType = "application"
|
||||
ProviderAccessToken ProviderType = "access_token"
|
||||
ProviderLocal ProviderType = "local"
|
||||
ProviderLDAP ProviderType = "ldap"
|
||||
|
@ -38,6 +39,7 @@ var LocalProviders = list.List{
|
|||
var ClientProviders = list.List{
|
||||
string(ProviderClient),
|
||||
string(ProviderClientCredentials),
|
||||
string(ProviderApplication),
|
||||
string(ProviderAccessToken),
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ func TestAuthToken(t *testing.T) {
|
|||
bearerToken := BearerToken(c)
|
||||
assert.Equal(t, "", bearerToken)
|
||||
})
|
||||
t.Run("AuthSecret", func(t *testing.T) {
|
||||
t.Run("AppPassword", func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
@ -86,8 +86,8 @@ func TestAuthToken(t *testing.T) {
|
|||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
// Set X-Auth-Token header to a random value generated by rnd.AuthSecret().
|
||||
expected := rnd.AuthSecret()
|
||||
// Set X-Auth-Token header to a random value generated by rnd.AppPassword().
|
||||
expected := rnd.AppPassword()
|
||||
c.Request.Header.Add(XAuthToken, expected)
|
||||
|
||||
// Check header for expected token.
|
||||
|
|
|
@ -9,13 +9,13 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
SessionIdLength = 64
|
||||
AuthTokenLength = 48
|
||||
AuthSecretLength = 27
|
||||
AuthSecretSeparator = '-'
|
||||
SessionIdLength = 64
|
||||
AuthTokenLength = 48
|
||||
AppPasswordLength = 27
|
||||
AppPasswordSeparator = '-'
|
||||
)
|
||||
|
||||
// AuthToken returns a random hexadecimal character string that can be used for authentication purposes.
|
||||
// AuthToken generates a random hexadecimal character token for authenticating client applications.
|
||||
//
|
||||
// Examples: 9fa8e562564dac91b96881040e98f6719212a1a364e0bb25
|
||||
func AuthToken() string {
|
||||
|
@ -37,18 +37,19 @@ func IsAuthToken(s string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// AuthSecret returns a random, human-friendly string that can be used instead of a regular auth token.
|
||||
// It is separated by 3 dashes for better readability and has a total length of 27 characters.
|
||||
// AppPassword generates a random, human-friendly authentication token that can also be used as
|
||||
// password replacement for client applications. It is separated by 3 dashes for better readability
|
||||
// and has a total length of 27 characters.
|
||||
//
|
||||
// Example: OXiV72-wTtiL9-d04jO7-X7XP4p
|
||||
func AuthSecret() string {
|
||||
func AppPassword() string {
|
||||
m := big.NewInt(int64(len(CharsetBase62)))
|
||||
b := make([]byte, 0, AuthSecretLength)
|
||||
b := make([]byte, 0, AppPasswordLength)
|
||||
|
||||
for i := 0; i < AuthSecretLength; i++ {
|
||||
for i := 0; i < AppPasswordLength; i++ {
|
||||
if (i+1)%7 == 0 {
|
||||
b = append(b, AuthSecretSeparator)
|
||||
} else if i == AuthSecretLength-1 {
|
||||
b = append(b, AppPasswordSeparator)
|
||||
} else if i == AppPasswordLength-1 {
|
||||
b = append(b, CharsetBase62[crc32.ChecksumIEEE(b)%62])
|
||||
return string(b)
|
||||
} else if r, err := rand.Int(rand.Reader, m); err == nil {
|
||||
|
@ -59,17 +60,17 @@ func AuthSecret() string {
|
|||
return string(b)
|
||||
}
|
||||
|
||||
// IsAuthSecret checks if the string might be a valid auth secret.
|
||||
func IsAuthSecret(s string, verifyChecksum bool) bool {
|
||||
// IsAppPassword checks if the string might be a valid app password.
|
||||
func IsAppPassword(s string, verifyChecksum bool) bool {
|
||||
// Verify token length.
|
||||
if len(s) != AuthSecretLength {
|
||||
if len(s) != AppPasswordLength {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check characters.
|
||||
sep := 0
|
||||
for _, r := range s {
|
||||
if r == AuthSecretSeparator {
|
||||
if r == AppPasswordSeparator {
|
||||
sep++
|
||||
} else if (r < '0' || r > '9') && (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') {
|
||||
return false
|
||||
|
@ -77,25 +78,25 @@ func IsAuthSecret(s string, verifyChecksum bool) bool {
|
|||
}
|
||||
|
||||
// Check number of separators.
|
||||
if sep != AuthSecretLength/7 {
|
||||
if sep != AppPasswordLength/7 {
|
||||
return false
|
||||
} else if !verifyChecksum {
|
||||
return true
|
||||
}
|
||||
|
||||
// Verify token checksum.
|
||||
return s[AuthSecretLength-1] == CharsetBase62[crc32.ChecksumIEEE([]byte(s[:AuthSecretLength-1]))%62]
|
||||
return s[AppPasswordLength-1] == CharsetBase62[crc32.ChecksumIEEE([]byte(s[:AppPasswordLength-1]))%62]
|
||||
}
|
||||
|
||||
// IsAuthAny checks if the string might be a valid auth token or secret.
|
||||
// IsAuthAny checks if the string might be a valid auth token or app password.
|
||||
func IsAuthAny(s string) bool {
|
||||
// Check if string might be a regular auth token.
|
||||
if IsAuthToken(s) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if string might be a human-friendly auth secret.
|
||||
if IsAuthSecret(s, false) {
|
||||
// Check if string might be a human-friendly app password.
|
||||
if IsAppPassword(s, false) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -43,72 +43,72 @@ func BenchmarkIsAuthToken(b *testing.B) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAuthSecret(t *testing.T) {
|
||||
func TestAppPassword(t *testing.T) {
|
||||
for n := 0; n < 10; n++ {
|
||||
s := AuthSecret()
|
||||
t.Logf("AuthSecret %d: %s", n, s)
|
||||
assert.Equal(t, AuthSecretLength, len(s))
|
||||
s := AppPassword()
|
||||
t.Logf("AppPassword %d: %s", n, s)
|
||||
assert.Equal(t, AppPasswordLength, len(s))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAuthSecret(b *testing.B) {
|
||||
func BenchmarkAppPassword(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
AuthSecret()
|
||||
AppPassword()
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAuthSecret(t *testing.T) {
|
||||
func TestIsAppPasswordt(t *testing.T) {
|
||||
t.Run("VerifyChecksum", func(t *testing.T) {
|
||||
assert.True(t, IsAuthSecret("MPkOqm-RtKGOi-ctIvXm-Qv3XhN", true))
|
||||
assert.True(t, IsAuthSecret("9q2JHc-P0LzNE-xzvY9j-vMoefj", true))
|
||||
assert.False(t, IsAuthSecret("MPkOqm-RtKGOi-ctIvXm-Qv3Xha", true))
|
||||
assert.False(t, IsAuthSecret("9q2JHc-P0LzNE-xzvY9j-vMoef2", true))
|
||||
assert.True(t, IsAuthSecret(AuthSecret(), true))
|
||||
assert.True(t, IsAuthSecret(AuthSecret(), true))
|
||||
assert.False(t, IsAuthSecret(AuthToken(), true))
|
||||
assert.False(t, IsAuthSecret(AuthToken(), true))
|
||||
assert.False(t, IsAuthSecret(SessionID(AuthToken()), true))
|
||||
assert.False(t, IsAuthSecret(SessionID(AuthToken()), true))
|
||||
assert.False(t, IsAuthSecret("55785BAC-9H4B-4747-B090-EE123FFEE437", true))
|
||||
assert.False(t, IsAuthSecret("4B1FEF2D1CF4A5BE38B263E0637EDEAD", true))
|
||||
assert.False(t, IsAuthSecret("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2", true))
|
||||
assert.False(t, IsAuthSecret("", true))
|
||||
assert.True(t, IsAppPassword("MPkOqm-RtKGOi-ctIvXm-Qv3XhN", true))
|
||||
assert.True(t, IsAppPassword("9q2JHc-P0LzNE-xzvY9j-vMoefj", true))
|
||||
assert.False(t, IsAppPassword("MPkOqm-RtKGOi-ctIvXm-Qv3Xha", true))
|
||||
assert.False(t, IsAppPassword("9q2JHc-P0LzNE-xzvY9j-vMoef2", true))
|
||||
assert.True(t, IsAppPassword(AppPassword(), true))
|
||||
assert.True(t, IsAppPassword(AppPassword(), true))
|
||||
assert.False(t, IsAppPassword(AuthToken(), true))
|
||||
assert.False(t, IsAppPassword(AuthToken(), true))
|
||||
assert.False(t, IsAppPassword(SessionID(AuthToken()), true))
|
||||
assert.False(t, IsAppPassword(SessionID(AuthToken()), true))
|
||||
assert.False(t, IsAppPassword("55785BAC-9H4B-4747-B090-EE123FFEE437", true))
|
||||
assert.False(t, IsAppPassword("4B1FEF2D1CF4A5BE38B263E0637EDEAD", true))
|
||||
assert.False(t, IsAppPassword("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2", true))
|
||||
assert.False(t, IsAppPassword("", true))
|
||||
})
|
||||
t.Run("IgnoreChecksum", func(t *testing.T) {
|
||||
assert.True(t, IsAuthSecret("MPkOqm-RtKGOi-ctIvXm-Qv3XhN", false))
|
||||
assert.True(t, IsAuthSecret("9q2JHc-P0LzNE-xzvY9j-vMoefj", false))
|
||||
assert.True(t, IsAuthSecret("MPkOqm-RtKGOi-ctIvXm-Qv3Xha", false))
|
||||
assert.True(t, IsAuthSecret("9q2JHc-P0LzNE-xzvY9j-vMoef2", false))
|
||||
assert.True(t, IsAuthSecret(AuthSecret(), false))
|
||||
assert.True(t, IsAuthSecret(AuthSecret(), false))
|
||||
assert.False(t, IsAuthSecret(AuthToken(), false))
|
||||
assert.False(t, IsAuthSecret(AuthToken(), false))
|
||||
assert.False(t, IsAuthSecret(SessionID(AuthToken()), false))
|
||||
assert.False(t, IsAuthSecret(SessionID(AuthToken()), false))
|
||||
assert.False(t, IsAuthSecret("55785BAC-9H4B-4747-B090-EE123FFEE437", false))
|
||||
assert.False(t, IsAuthSecret("4B1FEF2D1CF4A5BE38B263E0637EDEAD", false))
|
||||
assert.False(t, IsAuthSecret("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2", false))
|
||||
assert.False(t, IsAuthSecret("", false))
|
||||
assert.True(t, IsAppPassword("MPkOqm-RtKGOi-ctIvXm-Qv3XhN", false))
|
||||
assert.True(t, IsAppPassword("9q2JHc-P0LzNE-xzvY9j-vMoefj", false))
|
||||
assert.True(t, IsAppPassword("MPkOqm-RtKGOi-ctIvXm-Qv3Xha", false))
|
||||
assert.True(t, IsAppPassword("9q2JHc-P0LzNE-xzvY9j-vMoef2", false))
|
||||
assert.True(t, IsAppPassword(AppPassword(), false))
|
||||
assert.True(t, IsAppPassword(AppPassword(), false))
|
||||
assert.False(t, IsAppPassword(AuthToken(), false))
|
||||
assert.False(t, IsAppPassword(AuthToken(), false))
|
||||
assert.False(t, IsAppPassword(SessionID(AuthToken()), false))
|
||||
assert.False(t, IsAppPassword(SessionID(AuthToken()), false))
|
||||
assert.False(t, IsAppPassword("55785BAC-9H4B-4747-B090-EE123FFEE437", false))
|
||||
assert.False(t, IsAppPassword("4B1FEF2D1CF4A5BE38B263E0637EDEAD", false))
|
||||
assert.False(t, IsAppPassword("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2", false))
|
||||
assert.False(t, IsAppPassword("", false))
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkIsAuthSecretVerifyChecksum(b *testing.B) {
|
||||
func BenchmarkAppPasswordtVerifyChecksum(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
IsAuthSecret("MPkOqm-RtKGOi-ctIvXm-Qv3XhN", true)
|
||||
IsAppPassword("MPkOqm-RtKGOi-ctIvXm-Qv3XhN", true)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsAuthSecretIgnoreChecksum(b *testing.B) {
|
||||
func BenchmarkAppPasswordIgnoreChecksum(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
IsAuthSecret("MPkOqm-RtKGOi-ctIvXm-Qv3XhN", false)
|
||||
IsAppPassword("MPkOqm-RtKGOi-ctIvXm-Qv3XhN", false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAuthAny(t *testing.T) {
|
||||
assert.True(t, IsAuthAny("MPkOqm-RtKGOi-ctIvXm-Qv3XhN"))
|
||||
assert.True(t, IsAuthAny("9q2JHc-P0LzNE-xzvY9j-vMoefj"))
|
||||
assert.True(t, IsAuthAny(AuthSecret()))
|
||||
assert.True(t, IsAuthAny(AuthSecret()))
|
||||
assert.True(t, IsAuthAny(AppPassword()))
|
||||
assert.True(t, IsAuthAny(AppPassword()))
|
||||
assert.True(t, IsAuthAny(AuthToken()))
|
||||
assert.True(t, IsAuthAny(AuthToken()))
|
||||
assert.False(t, IsAuthAny(SessionID(AuthToken())))
|
||||
|
|
Loading…
Reference in a new issue