2024-02-07 01:18:17 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2024-02-07 10:54:37 +00:00
|
|
|
"context"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/sha256"
|
2024-02-08 01:16:14 +00:00
|
|
|
"database/sql"
|
2024-02-10 16:23:50 +00:00
|
|
|
"encoding/hex"
|
2024-02-07 10:54:37 +00:00
|
|
|
"encoding/json"
|
2024-02-08 01:16:14 +00:00
|
|
|
"errors"
|
2024-02-10 11:59:45 +00:00
|
|
|
"fmt"
|
2024-02-08 01:16:14 +00:00
|
|
|
"github.com/1f349/lavender/database"
|
|
|
|
"github.com/1f349/lavender/issuer"
|
2024-02-07 01:18:17 +00:00
|
|
|
"github.com/1f349/lavender/pages"
|
2024-02-15 15:23:10 +00:00
|
|
|
"github.com/1f349/mjwt/auth"
|
|
|
|
"github.com/1f349/mjwt/claims"
|
|
|
|
"github.com/golang-jwt/jwt/v4"
|
2024-02-07 01:18:17 +00:00
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2024-02-07 10:54:37 +00:00
|
|
|
func (h *HttpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
|
|
|
if !auth.IsGuest() {
|
|
|
|
h.SafeRedirect(rw, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-07 01:18:17 +00:00
|
|
|
cookie, err := req.Cookie("lavender-login-name")
|
|
|
|
if err == nil && cookie.Valid() == nil {
|
|
|
|
pages.RenderPageTemplate(rw, "login-memory", map[string]any{
|
|
|
|
"ServiceName": h.conf.ServiceName,
|
|
|
|
"LoginName": cookie.Value,
|
2024-02-10 02:53:58 +00:00
|
|
|
"Redirect": req.URL.Query().Get("redirect"),
|
2024-02-07 01:18:17 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
pages.RenderPageTemplate(rw, "login", map[string]any{
|
|
|
|
"ServiceName": h.conf.ServiceName,
|
2024-02-10 02:53:58 +00:00
|
|
|
"Redirect": req.URL.Query().Get("redirect"),
|
2024-02-07 01:18:17 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-02-07 10:54:37 +00:00
|
|
|
func (h *HttpServer) loginPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
|
|
|
if !auth.IsGuest() {
|
|
|
|
h.SafeRedirect(rw, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-07 01:18:17 +00:00
|
|
|
if req.PostFormValue("not-you") == "1" {
|
|
|
|
http.SetCookie(rw, &http.Cookie{
|
|
|
|
Name: "lavender-login-name",
|
|
|
|
Value: "",
|
|
|
|
Path: "/",
|
|
|
|
MaxAge: -1,
|
|
|
|
Secure: true,
|
2024-02-15 14:44:58 +00:00
|
|
|
SameSite: http.SameSiteLaxMode,
|
2024-02-07 01:18:17 +00:00
|
|
|
})
|
|
|
|
http.Redirect(rw, req, (&url.URL{
|
|
|
|
Path: "/login",
|
|
|
|
}).String(), http.StatusFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
loginName := req.PostFormValue("loginname")
|
|
|
|
login := h.manager.FindServiceFromLogin(loginName)
|
|
|
|
if login == nil {
|
|
|
|
http.Error(rw, "No login service defined for this username", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// the @ must exist if the service is defined
|
|
|
|
n := strings.IndexByte(loginName, '@')
|
|
|
|
loginUn := loginName[:n]
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
future := now.AddDate(1, 0, 0)
|
|
|
|
http.SetCookie(rw, &http.Cookie{
|
|
|
|
Name: "lavender-login-name",
|
|
|
|
Value: loginName,
|
|
|
|
Path: "/",
|
|
|
|
Expires: future,
|
|
|
|
MaxAge: int(future.Sub(now).Seconds()),
|
|
|
|
Secure: true,
|
2024-02-15 14:44:58 +00:00
|
|
|
SameSite: http.SameSiteLaxMode,
|
2024-02-07 01:18:17 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// save state for use later
|
|
|
|
state := login.Config.Namespace + ":" + uuid.NewString()
|
2024-02-10 02:53:58 +00:00
|
|
|
h.flowState.Set(state, flowStateData{login, req.PostFormValue("redirect")}, time.Now().Add(15*time.Minute))
|
2024-02-07 01:18:17 +00:00
|
|
|
|
|
|
|
// generate oauth2 config and redirect to authorize URL
|
|
|
|
oa2conf := login.OAuth2Config
|
|
|
|
oa2conf.RedirectURL = h.conf.BaseUrl + "/callback"
|
|
|
|
nextUrl := oa2conf.AuthCodeURL(state, oauth2.SetAuthURLParam("login_name", loginUn))
|
|
|
|
http.Redirect(rw, req, nextUrl, http.StatusFound)
|
|
|
|
}
|
2024-02-07 10:54:37 +00:00
|
|
|
|
|
|
|
func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
|
|
|
flowState, ok := h.flowState.Get(req.FormValue("state"))
|
|
|
|
if !ok {
|
|
|
|
http.Error(rw, "Invalid flow state", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
token, err := flowState.sso.OAuth2Config.Exchange(context.Background(), req.FormValue("code"), oauth2.SetAuthURLParam("redirect_uri", h.conf.BaseUrl+"/callback"))
|
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, "Failed to exchange code for token", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-10 11:59:45 +00:00
|
|
|
sessionData, err := h.fetchUserInfo(flowState.sso, token)
|
2024-02-09 15:25:56 +00:00
|
|
|
if sessionData.ID == "" {
|
|
|
|
http.Error(rw, "Failed to fetch user info", http.StatusInternalServerError)
|
2024-02-07 10:54:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-08 01:16:14 +00:00
|
|
|
if h.DbTx(rw, func(tx *database.Tx) error {
|
|
|
|
_, err := tx.GetUser(sessionData.ID)
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
uEmail := sessionData.UserInfo.GetStringOrDefault("email", "unknown@localhost")
|
|
|
|
uEmailVerified, _ := sessionData.UserInfo.GetBoolean("email_verified")
|
|
|
|
return tx.InsertUser(sessionData.ID, uEmail, uEmailVerified, "", true)
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}) {
|
2024-02-07 10:54:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// only continues if the above tx succeeds
|
2024-02-15 15:09:14 +00:00
|
|
|
auth = sessionData
|
2024-02-07 10:54:37 +00:00
|
|
|
|
2024-02-10 11:59:45 +00:00
|
|
|
if h.DbTx(rw, func(tx *database.Tx) error {
|
2024-02-15 15:09:14 +00:00
|
|
|
return tx.UpdateUserToken(auth.ID, token.AccessToken, token.RefreshToken, token.Expiry)
|
2024-02-10 11:59:45 +00:00
|
|
|
}) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-15 15:23:10 +00:00
|
|
|
if h.setLoginDataCookie(rw, auth) {
|
2024-02-10 16:23:50 +00:00
|
|
|
http.Error(rw, "Failed to save login cookie", http.StatusInternalServerError)
|
2024-02-07 10:54:37 +00:00
|
|
|
return
|
|
|
|
}
|
2024-02-10 02:53:58 +00:00
|
|
|
if flowState.redirect != "" {
|
|
|
|
req.Form.Set("redirect", flowState.redirect)
|
|
|
|
}
|
2024-02-07 10:54:37 +00:00
|
|
|
h.SafeRedirect(rw, req)
|
|
|
|
}
|
|
|
|
|
2024-02-15 15:23:10 +00:00
|
|
|
const oneYear = 365 * 24 * time.Hour
|
|
|
|
|
|
|
|
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, authData UserAuth) bool {
|
|
|
|
ps := claims.NewPermStorage()
|
|
|
|
gen, err := h.signingKey.GenerateJwt(authData.ID, uuid.NewString(), jwt.ClaimStrings{h.conf.BaseUrl}, oneYear, auth.AccessTokenClaims{Perms: ps})
|
2024-02-08 01:16:14 +00:00
|
|
|
if err != nil {
|
2024-02-15 15:23:10 +00:00
|
|
|
http.Error(rw, "Failed to generate cookie token", http.StatusInternalServerError)
|
2024-02-08 01:16:14 +00:00
|
|
|
return true
|
|
|
|
}
|
2024-02-07 10:54:37 +00:00
|
|
|
http.SetCookie(rw, &http.Cookie{
|
2024-02-08 01:16:14 +00:00
|
|
|
Name: "lavender-login-data",
|
2024-02-15 15:23:10 +00:00
|
|
|
Value: gen,
|
2024-02-07 10:54:37 +00:00
|
|
|
Path: "/",
|
|
|
|
Expires: time.Now().AddDate(0, 3, 0),
|
|
|
|
Secure: true,
|
2024-02-15 14:44:58 +00:00
|
|
|
SameSite: http.SameSiteLaxMode,
|
2024-02-07 10:54:37 +00:00
|
|
|
})
|
|
|
|
return false
|
|
|
|
}
|
2024-02-08 01:16:14 +00:00
|
|
|
|
2024-02-10 16:23:50 +00:00
|
|
|
func (h *HttpServer) readLoginDataCookie(req *http.Request, u *UserAuth) {
|
2024-02-08 01:16:14 +00:00
|
|
|
loginCookie, err := req.Cookie("lavender-login-data")
|
|
|
|
if err != nil {
|
2024-02-09 15:25:56 +00:00
|
|
|
return
|
2024-02-08 01:16:14 +00:00
|
|
|
}
|
2024-02-10 16:23:50 +00:00
|
|
|
hexData, err := hex.DecodeString(loginCookie.Value)
|
2024-02-08 01:16:14 +00:00
|
|
|
if err != nil {
|
2024-02-09 15:25:56 +00:00
|
|
|
return
|
2024-02-08 01:16:14 +00:00
|
|
|
}
|
2024-02-10 16:23:50 +00:00
|
|
|
decData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), hexData, []byte("lavender-login-data"))
|
2024-02-08 01:16:14 +00:00
|
|
|
if err != nil {
|
2024-02-09 15:25:56 +00:00
|
|
|
return
|
2024-02-08 01:16:14 +00:00
|
|
|
}
|
|
|
|
|
2024-02-10 16:23:50 +00:00
|
|
|
userId := string(decData)
|
|
|
|
var token oauth2.Token
|
|
|
|
if h.DbTxRaw(func(tx *database.Tx) error {
|
|
|
|
return tx.GetUserToken(userId, &token.AccessToken, &token.RefreshToken, &token.Expiry)
|
|
|
|
}) {
|
2024-02-09 15:25:56 +00:00
|
|
|
return
|
2024-02-08 01:16:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sso := h.manager.FindServiceFromLogin(userId)
|
|
|
|
if sso == nil {
|
2024-02-09 15:25:56 +00:00
|
|
|
return
|
2024-02-08 01:16:14 +00:00
|
|
|
}
|
|
|
|
|
2024-02-15 15:09:14 +00:00
|
|
|
*u, _ = h.fetchUserInfo(sso, &token)
|
2024-02-08 01:16:14 +00:00
|
|
|
}
|
|
|
|
|
2024-02-15 15:09:14 +00:00
|
|
|
func (h *HttpServer) fetchUserInfo(sso *issuer.WellKnownOIDC, token *oauth2.Token) (UserAuth, error) {
|
2024-02-08 01:16:14 +00:00
|
|
|
res, err := sso.OAuth2Config.Client(context.Background(), token).Get(sso.UserInfoEndpoint)
|
|
|
|
if err != nil || res.StatusCode != http.StatusOK {
|
2024-02-15 15:09:14 +00:00
|
|
|
return UserAuth{}, fmt.Errorf("request failed")
|
2024-02-08 01:16:14 +00:00
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
var userInfoJson UserInfoFields
|
|
|
|
if err := json.NewDecoder(res.Body).Decode(&userInfoJson); err != nil {
|
2024-02-15 15:09:14 +00:00
|
|
|
return UserAuth{}, err
|
2024-02-08 01:16:14 +00:00
|
|
|
}
|
|
|
|
subject, ok := userInfoJson.GetString("sub")
|
|
|
|
if !ok {
|
2024-02-15 15:09:14 +00:00
|
|
|
return UserAuth{}, fmt.Errorf("invalid subject")
|
2024-02-08 01:16:14 +00:00
|
|
|
}
|
|
|
|
subject += "@" + sso.Config.Namespace
|
|
|
|
|
|
|
|
displayName := userInfoJson.GetStringOrDefault("name", "Unknown Name")
|
2024-02-15 15:09:14 +00:00
|
|
|
return UserAuth{
|
2024-02-08 01:16:14 +00:00
|
|
|
ID: subject,
|
|
|
|
DisplayName: displayName,
|
|
|
|
UserInfo: userInfoJson,
|
2024-02-10 11:59:45 +00:00
|
|
|
}, nil
|
2024-02-08 01:16:14 +00:00
|
|
|
}
|