mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
OIDC: Improve auth api logs and user verification #782
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -25,12 +27,9 @@ import (
|
||||
// GET /api/v1/oidc/redirect
|
||||
func OIDCRedirect(router *gin.RouterGroup) {
|
||||
router.GET("/oidc/redirect", func(c *gin.Context) {
|
||||
// Get global config.
|
||||
conf := get.Config()
|
||||
|
||||
// Prevent CDNs from caching this endpoint.
|
||||
if header.IsCdn(c.Request) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, conf.LoginUri())
|
||||
AbortNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,16 +38,20 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
|
||||
// Get client IP address for logs and rate limiting checks.
|
||||
clientIp := ClientIP(c)
|
||||
actor := "unknown user"
|
||||
userAgent := UserAgent(c)
|
||||
userName := "unknown user"
|
||||
action := "sign in"
|
||||
|
||||
// Get global config.
|
||||
conf := get.Config()
|
||||
|
||||
// Abort in public mode and if OIDC is disabled.
|
||||
if get.Config().Public() {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrDisabledInPublicMode.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, authn.ErrDisabledInPublicMode.Error()})
|
||||
c.Redirect(http.StatusTemporaryRedirect, conf.LoginUri())
|
||||
return
|
||||
} else if !conf.OIDCEnabled() {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrAuthenticationDisabled.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, authn.ErrAuthenticationDisabled.Error()})
|
||||
c.Redirect(http.StatusTemporaryRedirect, conf.LoginUri())
|
||||
return
|
||||
}
|
||||
@@ -65,7 +68,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
|
||||
// Check if the required request parameters are present.
|
||||
if c.Query("state") == "" || c.Query("code") == "" {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrAuthCodeRequired.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, authn.ErrAuthCodeRequired.Error()})
|
||||
c.Redirect(http.StatusTemporaryRedirect, conf.LoginUri())
|
||||
return
|
||||
}
|
||||
@@ -74,7 +77,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
provider := get.OIDC()
|
||||
|
||||
if provider == nil {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrAuthenticationDisabled.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, authn.ErrAuthenticationDisabled.Error()})
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
}
|
||||
@@ -82,7 +85,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
userInfo, tokens, claimErr := provider.CodeExchangeUserInfo(c)
|
||||
|
||||
if claimErr != nil {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, claimErr.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, claimErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,24 +93,47 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
var user *entity.User
|
||||
var err error
|
||||
|
||||
userEmail := clean.Email(userInfo.GetEmail())
|
||||
|
||||
// Optionally check if the email domain matches.
|
||||
if domain := conf.OIDCDomain(); domain == "" {
|
||||
// Do nothing.
|
||||
} else if _, emailDomain, _ := strings.Cut(userEmail, "@"); emailDomain == "" || !userInfo.IsEmailVerified() {
|
||||
event.AuditErr([]string{clientIp, "oidc", action, authn.ErrVerifiedEmailRequired.Error()})
|
||||
event.LoginError(clientIp, "oidc", userEmail, userAgent, authn.ErrVerifiedEmailRequired.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrForbidden)))
|
||||
return
|
||||
} else if !strings.HasSuffix("."+emailDomain, "."+domain) {
|
||||
message := fmt.Sprintf("domain must match '%s'", domain)
|
||||
event.AuditErr([]string{clientIp, "oidc", action, userEmail, message})
|
||||
event.LoginError(clientIp, "oidc", userEmail, userAgent, message)
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrForbidden)))
|
||||
return
|
||||
}
|
||||
|
||||
// Find existing user record and update it, if necessary.
|
||||
if oidcUser := entity.OidcUser(userInfo, conf.OIDCUsername()); oidcUser.UserName == "" || authn.ProviderOIDC.NotEqual(oidcUser.AuthProvider) {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrInvalidUsername.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, authn.ErrInvalidUsername.Error()})
|
||||
event.LoginError(clientIp, "oidc", oidcUser.UserName, userAgent, authn.ErrInvalidUsername.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
} else if user = entity.FindUser(oidcUser); user != nil {
|
||||
// Check if username and subject UID match.
|
||||
if user.Username() == "" || oidcUser.UserName == "" || user.Username() != oidcUser.UserName {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrInvalidUsername.Error()})
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
} else if user.AuthID == "" || oidcUser.AuthID == "" || user.AuthID != oidcUser.AuthID {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrInvalidAuthID.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, authn.ErrInvalidUsername.Error()})
|
||||
event.LoginError(clientIp, "oidc", oidcUser.UserName, userAgent, authn.ErrInvalidUsername.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
}
|
||||
|
||||
actor = user.Username()
|
||||
userName = user.Username()
|
||||
|
||||
if user.AuthID == "" || oidcUser.AuthID == "" || user.AuthID != oidcUser.AuthID {
|
||||
event.AuditErr([]string{clientIp, "oidc", action, userName, authn.ErrInvalidAuthID.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrInvalidAuthID.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
}
|
||||
|
||||
// Update user profile information.
|
||||
details := user.Details()
|
||||
@@ -163,9 +189,10 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
user.VerifiedAt = entity.TimeStamp()
|
||||
}
|
||||
|
||||
// Update user account.
|
||||
// Update existing user account.
|
||||
if err = user.Save(); err != nil {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrAccountUpdateFailed.Error(), err.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, userName, authn.ErrAccountUpdateFailed.Error(), err.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountUpdateFailed.Error()+": "+err.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
}
|
||||
@@ -174,14 +201,14 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
if avatarUrl := userInfo.GetPicture(); avatarUrl == "" || user.HasAvatar() {
|
||||
// Do nothing.
|
||||
} else if err = avatar.SetUserImageURL(user, avatarUrl, entity.SrcOIDC); err != nil {
|
||||
event.AuditWarn([]string{clientIp, "oidc", actor, action, "failed to set avatar image", err.Error()})
|
||||
event.AuditWarn([]string{clientIp, "oidc", action, userName, "failed to set avatar image", err.Error()})
|
||||
}
|
||||
} else if conf.OIDCRegister() {
|
||||
action = "sign up"
|
||||
|
||||
// Create new user record.
|
||||
user = &oidcUser
|
||||
actor = user.Username()
|
||||
userName = user.Username()
|
||||
|
||||
// Set user profile information.
|
||||
user.SetDisplayName(userInfo.GetName(), entity.SrcOIDC)
|
||||
@@ -227,28 +254,31 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
user.CanLogin = true
|
||||
user.WebDAV = conf.OIDCWebDAV()
|
||||
|
||||
// Create user account.
|
||||
// Create new user account.
|
||||
if err = user.Create(); err != nil {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrAccountCreateFailed.Error(), err.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, userName, authn.ErrAccountCreateFailed.Error(), err.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountCreateFailed.Error()+": "+err.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
}
|
||||
|
||||
// Set user avatar image.
|
||||
if avatarUrl := userInfo.GetPicture(); avatarUrl == "" {
|
||||
event.AuditDebug([]string{clientIp, "oidc", actor, action, "no avatar image provided"})
|
||||
event.AuditDebug([]string{clientIp, "oidc", action, userName, "no avatar image provided"})
|
||||
} else if err = avatar.SetUserImageURL(user, avatarUrl, entity.SrcOIDC); err != nil {
|
||||
event.AuditWarn([]string{clientIp, "oidc", actor, action, "failed to set avatar image", err.Error()})
|
||||
event.AuditWarn([]string{clientIp, "oidc", action, userName, "failed to set avatar image", err.Error()})
|
||||
}
|
||||
} else {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrRegistrationDisabled.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, userName, authn.ErrRegistrationDisabled.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrRegistrationDisabled.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
}
|
||||
|
||||
// Login allowed?
|
||||
if !user.CanLogIn() {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, authn.ErrAccountDisabled.Error()})
|
||||
event.AuditErr([]string{clientIp, "oidc", action, userName, authn.ErrAccountDisabled.Error()})
|
||||
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountDisabled.Error())
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
}
|
||||
@@ -271,10 +301,11 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
|
||||
// Save session after successful authentication.
|
||||
if sess, err = get.Session().Save(sess); err != nil {
|
||||
event.AuditErr([]string{clientIp, "oidc", actor, action, "%s"}, err)
|
||||
event.AuditErr([]string{clientIp, "oidc", action, userName, "%s"}, err)
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
} else if sess == nil {
|
||||
event.AuditErr([]string{clientIp, "oidc", action, userName, "session is nil"})
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrUnexpected)))
|
||||
return
|
||||
}
|
||||
@@ -286,7 +317,8 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
response := CreateSessionResponse(sess.AuthToken(), sess, conf.ClientSession(sess))
|
||||
|
||||
// Log success.
|
||||
event.AuditInfo([]string{clientIp, "oidc", actor, action, authn.Succeeded})
|
||||
event.AuditInfo([]string{clientIp, "oidc", action, userName, authn.Succeeded})
|
||||
event.LoginInfo(clientIp, "oidc", userName, userAgent)
|
||||
|
||||
// Update login timestamp.
|
||||
user.UpdateLoginTime()
|
||||
|
||||
Reference in New Issue
Block a user