mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
319 lines
10 KiB
Go
319 lines
10 KiB
Go
package entity
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/photoprism/photoprism/internal/auth/acl"
|
|
"github.com/photoprism/photoprism/internal/event"
|
|
"github.com/photoprism/photoprism/internal/form"
|
|
"github.com/photoprism/photoprism/pkg/authn"
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
|
"github.com/photoprism/photoprism/pkg/i18n"
|
|
"github.com/photoprism/photoprism/pkg/rnd"
|
|
"github.com/photoprism/photoprism/pkg/service/http/header"
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
|
)
|
|
|
|
// Auth validates login credentials and returns the authenticated user plus provider/method metadata.
|
|
var Auth = func(frm form.Login, s *Session, c *gin.Context) (user *User, provider authn.ProviderType, method authn.MethodType, err error) {
|
|
// Get sanitized username from login form.
|
|
nameName := frm.CleanUsername()
|
|
|
|
// Find registered user account.
|
|
user = FindUserByName(nameName)
|
|
|
|
// Try local authentication.
|
|
provider, method, err = AuthLocal(user, frm, s, c)
|
|
|
|
if err != nil {
|
|
return user, provider, method, err
|
|
}
|
|
|
|
// Update login timestamp.
|
|
user.UpdateLoginTime()
|
|
|
|
return user, provider, method, err
|
|
}
|
|
|
|
// AuthSession resolves an existing session from an app password token embedded in the login form.
|
|
func AuthSession(frm form.Login, c *gin.Context) (sess *Session, user *User, err error) {
|
|
if frm.Password == "" {
|
|
// Abort authentication if no token was provided.
|
|
return nil, nil, authn.ErrPasscodeRequired
|
|
} else if !rnd.IsAppPassword(frm.Password, true) {
|
|
// Abort authentication if token doesn't match expected format.
|
|
return nil, nil, authn.ErrInvalidPassword
|
|
}
|
|
|
|
// Get session ID for the auth token provided.
|
|
sid := rnd.SessionID(frm.Password)
|
|
|
|
// Find the session based on the hashed token used as session ID and return it.
|
|
sess, err = FindSession(sid)
|
|
|
|
// Log error and return nil if no matching session was found.
|
|
if sess == nil || err != nil {
|
|
return nil, nil, authn.ErrInvalidPassword
|
|
}
|
|
|
|
// Update the client IP and the user agent from
|
|
// the request context if they have changed.
|
|
sess.UpdateContext(c)
|
|
|
|
// Returns session and user if all checks have passed.
|
|
return sess, sess.GetUser(), nil
|
|
}
|
|
|
|
// AuthLocal authenticates against the local user database, handling OTP and app-password flows.
|
|
func AuthLocal(user *User, frm form.Login, s *Session, c *gin.Context) (provider authn.ProviderType, method authn.MethodType, err error) {
|
|
// Set defaults.
|
|
provider = authn.ProviderNone
|
|
method = authn.MethodUndefined
|
|
|
|
// Get client IP from request context.
|
|
clientIp := header.ClientIP(c)
|
|
|
|
// Get sanitized username from login form.
|
|
username := frm.CleanUsername()
|
|
|
|
// Check if user account exists.
|
|
if user == nil {
|
|
message := authn.ErrAccountNotFound.Error()
|
|
|
|
if s != nil {
|
|
event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, s.RefID, clean.LogQuote(username))
|
|
event.LoginError(clientIp, "api", username, s.UserAgent, message)
|
|
s.Status = http.StatusUnauthorized
|
|
}
|
|
|
|
return provider, method, i18n.Error(i18n.ErrInvalidCredentials)
|
|
}
|
|
|
|
// Login allowed?
|
|
if !user.CanLogIn() {
|
|
message := authn.ErrAccountDisabled.Error()
|
|
|
|
if s != nil {
|
|
event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, s.RefID, clean.LogQuote(username))
|
|
event.LoginError(clientIp, "api", username, s.UserAgent, message)
|
|
s.Status = http.StatusUnauthorized
|
|
}
|
|
|
|
return provider, method, i18n.Error(i18n.ErrInvalidCredentials)
|
|
}
|
|
|
|
// Authentication with personal access token if a valid secret has been provided as password.
|
|
if authSess, authUser, authErr := AuthSession(frm, c); authSess != nil && authUser != nil && authErr == nil {
|
|
if !authUser.IsRegistered() || authUser.UserUID != user.UserUID {
|
|
message := authn.ErrInvalidUser.Error()
|
|
|
|
if s != nil {
|
|
event.AuditErr([]string{clientIp, "session %s", "login as %s", "app password", message}, s.RefID, clean.LogQuote(username))
|
|
event.LoginError(clientIp, "api", username, s.UserAgent, message)
|
|
s.Status = http.StatusUnauthorized
|
|
}
|
|
|
|
return provider, method, i18n.Error(i18n.ErrInvalidCredentials)
|
|
} else if insufficientScope := authSess.InsufficientScope(acl.ResourceSessions, acl.Permissions{acl.ActionCreate}); insufficientScope || !authSess.IsClient() {
|
|
var message string
|
|
if insufficientScope {
|
|
message = authn.ErrInsufficientScope.Error()
|
|
} else {
|
|
message = authn.ErrUnauthorized.Error()
|
|
}
|
|
|
|
if s != nil {
|
|
event.AuditErr([]string{clientIp, "session %s", "login as %s", "app password", message}, s.RefID, clean.LogQuote(username))
|
|
event.LoginError(clientIp, "api", username, s.UserAgent, message)
|
|
s.Status = http.StatusUnauthorized
|
|
}
|
|
|
|
return provider, method, i18n.Error(i18n.ErrInvalidCredentials)
|
|
} else {
|
|
provider = authn.ProviderApplication
|
|
method = authn.MethodSession
|
|
|
|
if s != nil {
|
|
// Set scope, client UID, and client name to that of the parent session.
|
|
s.ClientUID = authSess.ClientUID
|
|
s.ClientName = authSess.GetClientName()
|
|
s.SetScope(authSess.Scope())
|
|
|
|
// Set provider and method to help identify the session type.
|
|
s.SetProvider(provider)
|
|
s.SetMethod(method)
|
|
|
|
// Set auth ID to the ID of the parent session.
|
|
s.SetAuthID(authSess.ID, user.UserUID)
|
|
|
|
// Limit lifetime of the session to that of the parent session, if any.
|
|
if authSess.SessExpires > 0 && s.SessExpires > authSess.SessExpires {
|
|
s.SessExpires = authSess.SessExpires
|
|
}
|
|
|
|
event.AuditInfo([]string{clientIp, "session %s", "login as %s", "app password", authn.Succeeded}, s.RefID, clean.LogQuote(username))
|
|
event.LoginInfo(clientIp, "api", username, s.UserAgent)
|
|
}
|
|
|
|
return provider, method, authErr
|
|
}
|
|
}
|
|
|
|
// Abort if user does not have a local authentication providers (signing up with OIDC creates a local user account).
|
|
if !user.Provider().IsDefault() && !user.Provider().IsLocal() {
|
|
message := fmt.Sprintf("%s authentication disabled", authn.ProviderLocal.String())
|
|
|
|
if s != nil {
|
|
event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, s.RefID, clean.LogQuote(username))
|
|
event.LoginError(clientIp, "api", username, s.UserAgent, message)
|
|
s.Status = http.StatusUnauthorized
|
|
}
|
|
|
|
return provider, method, i18n.Error(i18n.ErrInvalidCredentials)
|
|
}
|
|
|
|
// Check password.
|
|
if user.InvalidPassword(frm.Password) {
|
|
message := authn.ErrInvalidPassword.Error()
|
|
|
|
if s != nil {
|
|
event.AuditErr([]string{clientIp, "session %s", "login as %s", message}, s.RefID, clean.LogQuote(username))
|
|
event.LoginError(clientIp, "api", username, s.UserAgent, message)
|
|
s.Status = http.StatusUnauthorized
|
|
}
|
|
|
|
return provider, method, i18n.Error(i18n.ErrInvalidCredentials)
|
|
}
|
|
|
|
provider = authn.ProviderLocal
|
|
|
|
// Check two-factor authentication, if enabled.
|
|
if method = user.Method(); method.Is(authn.Method2FA) {
|
|
if code := frm.Passcode(); code == "" {
|
|
err = authn.ErrPasscodeRequired
|
|
|
|
if s != nil {
|
|
event.AuditInfo([]string{clientIp, "session %s", "login as %s", err.Error()}, s.RefID, clean.LogQuote(username))
|
|
s.Status = http.StatusUnauthorized
|
|
}
|
|
|
|
return provider, method, err
|
|
} else if valid, _, codeErr := user.VerifyPasscode(code); codeErr != nil {
|
|
if s != nil {
|
|
event.AuditWarn([]string{clientIp, "session %s", "login as %s", codeErr.Error()}, s.RefID, clean.LogQuote(username))
|
|
event.LoginError(clientIp, "api", username, s.UserAgent, codeErr.Error())
|
|
s.Status = http.StatusUnauthorized
|
|
}
|
|
|
|
return provider, method, codeErr
|
|
} else if !valid {
|
|
err = authn.ErrInvalidPasscode
|
|
|
|
if s != nil {
|
|
event.AuditErr([]string{clientIp, "session %s", "login as %s", err.Error()}, s.RefID, clean.LogQuote(username))
|
|
event.LoginError(clientIp, "api", username, s.UserAgent, err.Error())
|
|
s.Status = http.StatusUnauthorized
|
|
}
|
|
|
|
return provider, method, err
|
|
}
|
|
} else if method == authn.MethodUndefined {
|
|
method = authn.MethodDefault
|
|
}
|
|
|
|
if s != nil {
|
|
event.AuditInfo([]string{clientIp, "session %s", "login as %s", authn.Succeeded}, s.RefID, clean.LogQuote(username))
|
|
event.LoginInfo(clientIp, "api", username, s.UserAgent)
|
|
}
|
|
|
|
return provider, method, nil
|
|
}
|
|
|
|
// LogIn performs authentication checks against the specified login form.
|
|
func (m *Session) LogIn(frm form.Login, c *gin.Context) (err error) {
|
|
if c != nil {
|
|
m.SetContext(c)
|
|
}
|
|
|
|
// r := limiter.Login.Reserve(m.IP())
|
|
// r.Cancel()
|
|
|
|
var user *User
|
|
var provider authn.ProviderType
|
|
var method authn.MethodType
|
|
|
|
// Log in with username and password?
|
|
if frm.HasCredentials() {
|
|
if m.IsRegistered() {
|
|
m.Regenerate()
|
|
}
|
|
|
|
user, provider, method, err = Auth(frm, m, c)
|
|
|
|
m.SetProvider(provider)
|
|
m.SetMethod(method)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m.SetUser(user)
|
|
m.SetGrantType(authn.GrantPassword)
|
|
}
|
|
|
|
// Try to redeem link share token, if provided.
|
|
if frm.HasShareToken() {
|
|
user = m.GetUser()
|
|
|
|
// Redeem token.
|
|
if user.IsRegistered() {
|
|
if shares := user.RedeemToken(frm.Token); shares == 0 {
|
|
message := authn.ErrInvalidShareToken.Error()
|
|
event.AuditWarn([]string{m.IP(), "session %s", message}, m.RefID)
|
|
m.Status = http.StatusNotFound
|
|
return i18n.Error(i18n.ErrInvalidLink)
|
|
} else {
|
|
event.AuditInfo([]string{m.IP(), "session %s", "token redeemed for %d shares"}, m.RefID, user.RedeemToken(frm.Token))
|
|
}
|
|
} else if data := m.GetData(); data == nil {
|
|
m.Status = http.StatusInternalServerError
|
|
return i18n.Error(i18n.ErrUnexpected)
|
|
} else if shares := data.RedeemToken(frm.Token); shares == 0 {
|
|
message := authn.ErrInvalidShareToken.Error()
|
|
event.AuditWarn([]string{m.IP(), "session %s", message}, m.RefID)
|
|
event.LoginError(m.IP(), "api", "", m.UserAgent, message)
|
|
m.Status = http.StatusNotFound
|
|
return i18n.Error(i18n.ErrInvalidLink)
|
|
} else {
|
|
m.SetData(data)
|
|
m.SetProvider(authn.ProviderLink)
|
|
m.SetGrantType(authn.GrantShareToken)
|
|
event.AuditInfo([]string{m.IP(), "session %s", "token redeemed for %d shares"}, m.RefID, shares, data)
|
|
}
|
|
|
|
// Upgrade the session user role to visitor if a valid share token has been provided.
|
|
if user.IsUnknown() {
|
|
user = &Visitor
|
|
event.AuditDebug([]string{m.IP(), "session %s", "role upgraded to %s"}, m.RefID, user.AclRole().String())
|
|
expires := UTC().Add(time.Hour * 24)
|
|
m.Expires(expires)
|
|
event.AuditDebug([]string{m.IP(), "session %s", "expires at %s"}, m.RefID, txt.DateTime(&expires))
|
|
}
|
|
|
|
m.SetUser(user)
|
|
}
|
|
|
|
// Unregistered visitors must use a valid share link to obtain a session.
|
|
if m.GetUser().NotRegistered() && m.GetData().NoShares() {
|
|
m.Status = http.StatusUnauthorized
|
|
return i18n.Error(i18n.ErrInvalidCredentials)
|
|
}
|
|
|
|
m.Status = http.StatusOK
|
|
|
|
return nil
|
|
}
|