mirror of
https://github.com/knadh/listmonk.git
synced 2024-11-14 19:45:26 +08:00
207 lines
5.2 KiB
Go
207 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/knadh/listmonk/internal/auth"
|
|
"github.com/knadh/listmonk/internal/utils"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/zerodha/simplesessions/v3"
|
|
)
|
|
|
|
type loginTpl struct {
|
|
Title string
|
|
Description string
|
|
|
|
NextURI string
|
|
Nonce string
|
|
PasswordEnabled bool
|
|
OIDCProvider string
|
|
OIDCProviderLogo string
|
|
Error string
|
|
}
|
|
|
|
var oidcProviders = map[string]bool{
|
|
"google.com": true,
|
|
"microsoftonline.com": true,
|
|
"auth0.com": true,
|
|
"github.com": true,
|
|
}
|
|
|
|
// handleLoginPage renders the login page and handles the login form.
|
|
func handleLoginPage(c echo.Context) error {
|
|
var (
|
|
app = c.Get("app").(*App)
|
|
next = utils.SanitizeURI(c.FormValue("next"))
|
|
)
|
|
|
|
if next == "/" {
|
|
next = uriAdmin
|
|
}
|
|
|
|
oidcProvider := ""
|
|
oidcProviderLogo := ""
|
|
if app.constants.Security.OIDC.Enabled {
|
|
oidcProviderLogo = "oidc.png"
|
|
u, err := url.Parse(app.constants.Security.OIDC.Provider)
|
|
if err == nil {
|
|
h := strings.Split(u.Hostname(), ".")
|
|
|
|
// Get the last two h for the root domain
|
|
if len(h) >= 2 {
|
|
oidcProvider = h[len(h)-2] + "." + h[len(h)-1]
|
|
} else {
|
|
oidcProvider = u.Hostname()
|
|
}
|
|
|
|
if _, ok := oidcProviders[oidcProvider]; ok {
|
|
oidcProviderLogo = oidcProvider + ".png"
|
|
}
|
|
}
|
|
}
|
|
|
|
out := loginTpl{
|
|
Title: app.i18n.T("users.login"),
|
|
PasswordEnabled: true,
|
|
OIDCProvider: oidcProvider,
|
|
OIDCProviderLogo: oidcProviderLogo,
|
|
NextURI: next,
|
|
}
|
|
|
|
// Login request.
|
|
if c.Request().Method == http.MethodPost {
|
|
err := doLogin(c)
|
|
if err == nil {
|
|
return c.Redirect(http.StatusFound, utils.SanitizeURI(c.FormValue("next")))
|
|
}
|
|
|
|
if e, ok := err.(*echo.HTTPError); ok {
|
|
out.Error = e.Message.(string)
|
|
} else {
|
|
out.Error = err.Error()
|
|
}
|
|
}
|
|
|
|
// Generate and set a nonce for preventing CSRF requests.
|
|
nonce, err := utils.GenerateRandomString(16)
|
|
if err != nil {
|
|
app.log.Printf("error generating OIDC nonce: %v", err)
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.internalError"))
|
|
}
|
|
c.SetCookie(&http.Cookie{
|
|
Name: "nonce",
|
|
Value: nonce,
|
|
Secure: true,
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteStrictMode,
|
|
})
|
|
out.Nonce = nonce
|
|
|
|
return c.Render(http.StatusOK, "admin-login", out)
|
|
}
|
|
|
|
// handleLogout logs a user out.
|
|
func handleLogout(c echo.Context) error {
|
|
var (
|
|
sess = c.Get(auth.SessionKey).(*simplesessions.Session)
|
|
)
|
|
|
|
// Clear the session.
|
|
_ = sess.Destroy()
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
}
|
|
|
|
// doLogin logs a user in with a username and password.
|
|
func doLogin(c echo.Context) error {
|
|
var (
|
|
app = c.Get("app").(*App)
|
|
)
|
|
|
|
// Verify that the request came from the login page (CSRF).
|
|
// nonce, err := c.Cookie("nonce")
|
|
// if err != nil || nonce.Value == "" || nonce.Value != c.FormValue("nonce") {
|
|
// return echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest"))
|
|
// }
|
|
|
|
var (
|
|
username = strings.TrimSpace(c.FormValue("username"))
|
|
password = strings.TrimSpace(c.FormValue("password"))
|
|
)
|
|
|
|
if !strHasLen(username, 3, stdInputMaxLen) {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
|
}
|
|
|
|
if !strHasLen(password, 8, stdInputMaxLen) {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
user, err := app.core.LoginUser(username, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Resist potential constant-time-comparison attacks with a min response time.
|
|
if ms := time.Now().Sub(start).Milliseconds(); ms < 100 {
|
|
time.Sleep(time.Duration(ms))
|
|
}
|
|
|
|
// Set the session.
|
|
if err := app.auth.SetSession(user, "", c); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleOIDCLogin initializes an OIDC request and redirects to the OIDC provider for login.
|
|
func handleOIDCLogin(c echo.Context) error {
|
|
app := c.Get("app").(*App)
|
|
|
|
// Verify that the request came from the login page (CSRF).
|
|
nonce, err := c.Cookie("nonce")
|
|
if err != nil || nonce.Value == "" || nonce.Value != c.FormValue("nonce") {
|
|
return echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest"))
|
|
}
|
|
|
|
return c.Redirect(http.StatusFound, app.auth.GetOIDCAuthURL(c.Request().URL.RequestURI(), nonce.Value))
|
|
}
|
|
|
|
// handleOIDCFinish receives the redirect callback from the OIDC provider and completes the handshake.
|
|
func handleOIDCFinish(c echo.Context) error {
|
|
app := c.Get("app").(*App)
|
|
|
|
nonce, err := c.Cookie("nonce")
|
|
if err != nil || nonce.Value == "" {
|
|
return echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest"))
|
|
}
|
|
|
|
code := c.Request().URL.Query().Get("code")
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest"))
|
|
}
|
|
|
|
oidcToken, u, err := app.auth.ExchangeOIDCToken(code, nonce.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the user by e-mail received from OIDC.
|
|
user, err := app.core.GetUser(0, "", u.Email.String)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the session.
|
|
if err := app.auth.SetSession(user, oidcToken, c); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.Redirect(http.StatusFound, utils.SanitizeURI(c.QueryParam("state")))
|
|
}
|