photoprism/internal/entity/auth_session_login.go
Michael Mayer 70f8c3be6c WebDAV: Re-enable authentication with account password #782 #808
Signed-off-by: Michael Mayer <michael@photoprism.app>
2024-01-29 14:48:15 +01:00

232 lines
8.2 KiB
Go

package entity
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
)
// Auth checks if the credentials are valid and returns the user and authentication provider.
var Auth = func(f form.Login, m *Session, c *gin.Context) (user *User, provider authn.ProviderType, err error) {
// Get username from login form.
nameName := f.Username()
// Find registered user account.
user = FindUserByName(nameName)
// Try local authentication.
provider, err = AuthLocal(user, f, m, c)
if err != nil {
return user, authn.ProviderNone, err
}
// Update login timestamp.
user.UpdateLoginTime()
return user, provider, err
}
// AuthSession returns the client session that belongs to the auth token provided, or returns nil if it was not found.
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 password provided")
} else if !rnd.IsAppPassword(f.Password, true) {
// Abort authentication if token doesn't match expected format.
return nil, nil, fmt.Errorf("password does not match expected format")
}
// Get session ID for the auth token provided.
sid := rnd.SessionID(f.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, fmt.Errorf("invalid password")
}
// 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.User(), nil
}
// AuthLocal authenticates against the local user database with the specified username and password.
func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (authn.ProviderType, error) {
// Get client IP from request context.
clientIp := header.ClientIP(c)
// Get username from login form.
userName := f.Username()
// Check if user account exists.
if user == nil {
message := "account not found"
limiter.Login.Reserve(clientIp)
if m != nil {
event.AuditWarn([]string{clientIp, "session %s", "login as %s", 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)
}
// Login allowed?
if !user.Provider().IsDefault() && !user.Provider().IsLocal() {
message := fmt.Sprintf("%s authentication disabled", authn.ProviderLocal.String())
if m != nil {
event.AuditWarn([]string{clientIp, "session %s", "login as %s", 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 !user.CanLogIn() {
message := "account disabled"
if m != nil {
event.AuditWarn([]string{clientIp, "session %s", "login as %s", 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)
}
// Authentication with personal access token if a valid secret has been provided as password.
if authSess, authUser, err := AuthSession(f, c); authSess != nil && authUser != nil && err == nil {
if !authUser.IsRegistered() || authUser.UserUID != user.UserUID {
message := "incorrect user"
limiter.Login.Reserve(clientIp)
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 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 {
m.ClientUID = authSess.ClientUID
m.ClientName = authSess.ClientName
m.SetScope(authSess.Scope())
m.SetMethod(authn.MethodSession)
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.ProviderApplication, err
}
}
// Otherwise, check account password.
if user.WrongPassword(f.Password) {
message := "incorrect password"
limiter.Login.Reserve(clientIp)
if m != nil {
event.AuditErr([]string{clientIp, "session %s", "login as %s", 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 m != nil {
event.AuditInfo([]string{clientIp, "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(userName))
event.LoginInfo(clientIp, "api", userName, m.UserAgent)
}
return authn.ProviderLocal, nil
}
// LogIn performs authentication checks against the specified login form.
func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
if c != nil {
m.SetContext(c)
}
var user *User
var provider authn.ProviderType
// Try to login with user credentials, if provided.
if f.HasCredentials() {
if m.IsRegistered() {
m.Regenerate()
}
user, provider, err = Auth(f, m, c)
if err != nil {
return err
}
m.SetUser(user)
m.SetProvider(provider)
}
// Try to redeem link share token, if provided.
if f.HasShareToken() {
user = m.User()
// Redeem token.
if user.IsRegistered() {
if shares := user.RedeemToken(f.ShareToken); shares == 0 {
limiter.Login.Reserve(m.IP())
event.AuditWarn([]string{m.IP(), "session %s", "share token %s is invalid"}, m.RefID, clean.LogQuote(f.ShareToken))
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(f.ShareToken))
}
} else if data := m.Data(); data == nil {
m.Status = http.StatusInternalServerError
return i18n.Error(i18n.ErrUnexpected)
} else if shares := data.RedeemToken(f.ShareToken); shares == 0 {
limiter.Login.Reserve(m.IP())
event.AuditWarn([]string{m.IP(), "session %s", "share token %s is invalid"}, m.RefID, clean.LogQuote(f.ShareToken))
event.LoginError(m.IP(), "api", "", m.UserAgent, "invalid share token")
m.Status = http.StatusNotFound
return i18n.Error(i18n.ErrInvalidLink)
} else {
m.SetData(data)
m.SetProvider(authn.ProviderLink)
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.User().NotRegistered() && m.Data().NoShares() {
m.Status = http.StatusUnauthorized
return i18n.Error(i18n.ErrInvalidCredentials)
}
m.Status = http.StatusOK
return nil
}