2024-11-02 21:36:07 +10:00

247 lines
7.7 KiB
Go

package handler
import (
"encoding/json"
"net/http"
h "npm/internal/api/http"
"npm/internal/errors"
"npm/internal/logger"
"slices"
"time"
c "npm/internal/api/context"
"npm/internal/entity/auth"
"npm/internal/entity/setting"
"npm/internal/entity/user"
njwt "npm/internal/jwt"
"gorm.io/gorm"
)
// tokenPayload is the structure we expect from a incoming login request
type tokenPayload struct {
Type string `json:"type"`
Identity string `json:"identity"`
Secret string `json:"secret"`
}
// GetAuthConfig is anonymous and returns the types of authentication
// enabled for this site
func GetAuthConfig() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
val, err := setting.GetAuthMethods()
if err == gorm.ErrRecordNotFound {
h.ResultResponseJSON(w, r, http.StatusOK, nil)
return
} else if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, val)
}
}
// NewToken Also known as a Login, requesting a new token with credentials
// Route: POST /auth
func NewToken() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// Read the bytes from the body
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var payload tokenPayload
err := json.Unmarshal(bodyBytes, &payload)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Check that this auth type is enabled
if authMethods, err := setting.GetAuthMethods(); err == gorm.ErrRecordNotFound {
h.ResultResponseJSON(w, r, http.StatusOK, nil)
return
} else if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
} else if !slices.Contains(authMethods, payload.Type) {
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidAuthType.Error(), nil)
return
}
switch payload.Type {
case "ldap":
newTokenLDAP(w, r, payload)
case "oidc":
newTokenOIDC(w, r, payload)
case "local":
newTokenLocal(w, r, payload)
}
}
}
func newTokenLocal(w http.ResponseWriter, r *http.Request, payload tokenPayload) {
// Find user by email
userObj, userErr := user.GetByEmail(payload.Identity)
if userErr != nil {
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), userErr.Error())
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
return
}
if userObj.IsDisabled {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
return
}
// Get Auth
authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type)
if authErr != nil {
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error())
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
return
}
// Verify Auth
validateErr := authObj.ValidateSecret(payload.Secret)
if validateErr != nil {
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error())
// Sleep for 1 second to prevent brute force password guessing
time.Sleep(time.Second)
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
return
}
if response, err := njwt.Generate(&userObj, false); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, response)
}
}
func newTokenLDAP(w http.ResponseWriter, r *http.Request, payload tokenPayload) {
// Get LDAP settings
ldapSettings, err := setting.GetLDAPSettings()
if err != nil {
logger.Error("LDAP settings not found", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
return
}
// Lets try to authenticate with LDAP
ldapUser, err := auth.LDAPAuthenticate(payload.Identity, payload.Secret)
if err != nil {
logger.Error("LDAP Auth Error", err)
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
return
}
// Get Auth by identity
authObj, authErr := auth.GetByIdenityType(ldapUser.Username, payload.Type)
if authErr == gorm.ErrRecordNotFound {
// Auth is not found for this identity. We can create it
if !ldapSettings.AutoCreateUser {
// LDAP Login was successful, but user does not have an auth record
// and auto create is disabled. Showing account disabled error
// for the time being
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrUserDisabled.Error(), nil)
return
}
// Attempt to find user by email
foundUser, err := user.GetByEmail(ldapUser.Email)
if err == gorm.ErrRecordNotFound {
// User not found, create user
foundUser, err = user.CreateFromLDAPUser(ldapUser)
if err != nil {
logger.Error("user.CreateFromLDAPUser", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
return
}
logger.Info("Created user from LDAP: %s, %s", ldapUser.Username, foundUser.Email)
} else if err != nil {
logger.Error("user.GetByEmail", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
return
}
// Create auth record and attach to this user
authObj = auth.Model{
UserID: foundUser.ID,
Type: auth.TypeLDAP,
Identity: ldapUser.Username,
}
if err := authObj.Save(); err != nil {
logger.Error("auth.Save", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
return
}
logger.Info("Created LDAP auth for user: %s, %s", ldapUser.Username, foundUser.Email)
} else if authErr != nil {
logger.Error("auth.GetByIdenityType", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, authErr.Error(), nil)
return
}
userObj, userErr := user.GetByID(authObj.UserID)
if userErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, userErr.Error(), nil)
return
}
if userObj.IsDisabled {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
return
}
if response, err := njwt.Generate(&userObj, false); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, response)
}
}
func newTokenOIDC(w http.ResponseWriter, r *http.Request, _ tokenPayload) {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "NOT YET SUPPORTED", nil)
}
// RefreshToken an existing token by given them a new one with the same claims
// Route: POST /auth/refresh
func RefreshToken() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// TODO: Use your own methods to verify an existing user is
// able to refresh their token and then give them a new one
userObj, _ := user.GetByEmail("jc@jc21.com")
if response, err := njwt.Generate(&userObj, false); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, response)
}
}
}
// NewSSEToken will generate and return a very short lived token for
// use by the /sse/* endpoint. It requires an app token to generate this
// Route: POST /auth/sse
func NewSSEToken() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value(c.UserIDCtxKey).(uint)
// Find user
userObj, userErr := user.GetByID(userID)
if userErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
return
}
if userObj.IsDisabled {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
return
}
if response, err := njwt.Generate(&userObj, true); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, response)
}
}
}