All checks were successful
test-and-lint / test-and-lint (pull_request) Successful in 2m50s
581 lines
19 KiB
Go
581 lines
19 KiB
Go
package handlers
|
|
|
|
import (
|
|
"MiauInv/auth"
|
|
"MiauInv/models"
|
|
"MiauInv/storage"
|
|
utils "MiauInv/util"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
"github.com/go-webauthn/webauthn/webauthn"
|
|
)
|
|
|
|
const passkeyChallengeTTL = 5 * time.Minute
|
|
|
|
type passkeyWebAuthnUser struct {
|
|
user models.User
|
|
credentials []webauthn.Credential
|
|
}
|
|
|
|
func (user passkeyWebAuthnUser) WebAuthnID() []byte {
|
|
return []byte(user.user.ID)
|
|
}
|
|
|
|
func (user passkeyWebAuthnUser) WebAuthnName() string {
|
|
return user.user.Username
|
|
}
|
|
|
|
func (user passkeyWebAuthnUser) WebAuthnDisplayName() string {
|
|
return user.user.Username
|
|
}
|
|
|
|
func (user passkeyWebAuthnUser) WebAuthnCredentials() []webauthn.Credential {
|
|
return user.credentials
|
|
}
|
|
|
|
func Passkeys(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
listPasskeys(w, r)
|
|
case http.MethodDelete:
|
|
deletePasskey(w, r)
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func PasskeyRegisterOptions(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
log.Println("POST [api/passkeys/register/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(req.Password) == "" {
|
|
http.Error(w, "Current password required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
claims := r.Context().Value(auth.UserContextKey).(*auth.Claims)
|
|
user, err := storage.GetUserById(claims.UserID)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/register/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "User not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if !auth.CheckPasswordHash(req.Password, user.Password) {
|
|
http.Error(w, "Invalid password", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
waUser, err := loadPasskeyWebAuthnUser(user)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/register/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not load passkeys", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
wa, err := webAuthnForRequest(r)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/register/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid WebAuthn origin", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
creation, sessionData, err := wa.BeginRegistration(waUser,
|
|
webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
|
|
webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
|
|
RequireResidentKey: protocol.ResidentKeyRequired(),
|
|
ResidentKey: protocol.ResidentKeyRequirementRequired,
|
|
UserVerification: protocol.VerificationRequired,
|
|
}),
|
|
webauthn.WithExclusions(webauthn.Credentials(waUser.WebAuthnCredentials()).CredentialDescriptors()),
|
|
webauthn.WithExtensions(map[string]any{"credProps": true}),
|
|
)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/register/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not create passkey registration challenge", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
sessionToken, err := utils.GenerateOpaqueToken()
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/register/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not create passkey challenge", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := storage.SavePasskeyChallenge(sessionToken, user.ID, storage.PasskeyCeremonyRegister, *sessionData, passkeyChallengeTTL); err != nil {
|
|
log.Println("POST [api/passkeys/register/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not save passkey challenge", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
_ = storage.CleanupExpiredPasskeyChallenges()
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"session_token": sessionToken,
|
|
"options": creation,
|
|
})
|
|
log.Println("POST [api/passkeys/register/options] " + r.RemoteAddr + ": Created passkey registration challenge")
|
|
}
|
|
|
|
func PasskeyRegisterFinish(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
SessionToken string `json:"session_token"`
|
|
Name string `json:"name"`
|
|
Credential json.RawMessage `json:"credential"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
log.Println("POST [api/passkeys/register/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.SessionToken == "" || len(req.Credential) == 0 {
|
|
http.Error(w, "Session token and credential required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
claims := r.Context().Value(auth.UserContextKey).(*auth.Claims)
|
|
user, err := storage.GetUserById(claims.UserID)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/register/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "User not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
challenge, sessionData, err := storage.ConsumePasskeyChallenge(req.SessionToken, storage.PasskeyCeremonyRegister)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/register/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid or expired passkey registration challenge", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if challenge.UserID != user.ID {
|
|
http.Error(w, "Invalid passkey registration challenge", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
waUser, err := loadPasskeyWebAuthnUser(user)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/register/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not load passkeys", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
wa, err := webAuthnForRequest(r)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/register/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid WebAuthn origin", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
credentialRequest := cloneRequestWithJSONBody(r, req.Credential)
|
|
credential, err := wa.FinishRegistration(waUser, sessionData, credentialRequest)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/register/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not verify passkey registration", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
name := strings.TrimSpace(req.Name)
|
|
if name == "" {
|
|
name = "Passkey"
|
|
}
|
|
if _, err := storage.AddPasskeyCredential(user.ID, name, credential); err != nil {
|
|
log.Println("POST [api/passkeys/register/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not save passkey", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err := storage.RevokeAllRefreshTokensForUser(user.ID); err != nil {
|
|
log.Println("POST [api/passkeys/register/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not revoke old sessions", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
passkeys, _ := storage.ListPasskeyCredentials(user.ID)
|
|
issueLoginSessionWithExtra(w, r, user, map[string]interface{}{
|
|
"passkeys_enabled": true,
|
|
"passkey_count": len(passkeys),
|
|
"passkeys": publicPasskeys(passkeys),
|
|
})
|
|
log.Println("POST [api/passkeys/register/finish] " + r.RemoteAddr + ": Registered passkey and revoked old sessions")
|
|
}
|
|
|
|
func PasskeyLoginOptions(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
wa, err := webAuthnForRequest(r)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/login/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid WebAuthn origin", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
assertion, sessionData, err := wa.BeginDiscoverableLogin(webauthn.WithUserVerification(protocol.VerificationRequired))
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/login/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not create passkey login challenge", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
sessionToken, err := utils.GenerateOpaqueToken()
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/login/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not create passkey challenge", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := storage.SavePasskeyChallenge(sessionToken, "", storage.PasskeyCeremonyLogin, *sessionData, passkeyChallengeTTL); err != nil {
|
|
log.Println("POST [api/passkeys/login/options] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not save passkey challenge", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
_ = storage.CleanupExpiredPasskeyChallenges()
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"session_token": sessionToken,
|
|
"options": assertion,
|
|
})
|
|
log.Println("POST [api/passkeys/login/options] " + r.RemoteAddr + ": Created passkey login challenge")
|
|
}
|
|
|
|
func PasskeyLoginFinish(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
SessionToken string `json:"session_token"`
|
|
Credential json.RawMessage `json:"credential"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
log.Println("POST [api/passkeys/login/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.SessionToken == "" || len(req.Credential) == 0 {
|
|
http.Error(w, "Session token and credential required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
challenge, sessionData, err := storage.ConsumePasskeyChallenge(req.SessionToken, storage.PasskeyCeremonyLogin)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/login/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid or expired passkey login challenge", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
wa, err := webAuthnForRequest(r)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/login/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid WebAuthn origin", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
credentialRequest := cloneRequestWithJSONBody(r, req.Credential)
|
|
|
|
var user models.User
|
|
var credential *webauthn.Credential
|
|
if challenge.UserID == "" {
|
|
var webAuthnUser webauthn.User
|
|
webAuthnUser, credential, err = wa.FinishPasskeyLogin(passkeyDiscoverableUserHandler, sessionData, credentialRequest)
|
|
if err == nil {
|
|
resolvedUser, ok := webAuthnUser.(passkeyWebAuthnUser)
|
|
if !ok {
|
|
err = errors.New("invalid passkey user type")
|
|
} else {
|
|
user = resolvedUser.user
|
|
}
|
|
}
|
|
} else {
|
|
user, err = storage.GetUserById(challenge.UserID)
|
|
if err == nil {
|
|
waUser, loadErr := loadPasskeyWebAuthnUser(user)
|
|
if loadErr != nil {
|
|
err = loadErr
|
|
} else {
|
|
credential, err = wa.FinishLogin(waUser, sessionData, credentialRequest)
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/login/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not verify passkey login", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if err := storage.UpdatePasskeyCredentialAfterLogin(user.ID, credential); err != nil {
|
|
log.Println("POST [api/passkeys/login/finish] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not update passkey", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
issueLoginSession(w, r, user)
|
|
log.Println("POST [api/passkeys/login/finish] " + r.RemoteAddr + ": Successfully logged in with passkey")
|
|
}
|
|
|
|
func PasskeyDisable(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
log.Println("POST [api/passkeys/disable] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
claims := r.Context().Value(auth.UserContextKey).(*auth.Claims)
|
|
user, err := storage.GetUserById(claims.UserID)
|
|
if err != nil {
|
|
log.Println("POST [api/passkeys/disable] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "User not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if !auth.CheckPasswordHash(req.Password, user.Password) {
|
|
http.Error(w, "Invalid password", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if err := storage.DeleteAllPasskeyCredentials(user.ID); err != nil {
|
|
log.Println("POST [api/passkeys/disable] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not disable passkeys", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := storage.RevokeAllRefreshTokensForUser(user.ID); err != nil {
|
|
log.Println("POST [api/passkeys/disable] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not revoke old sessions", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
issueLoginSessionWithExtra(w, r, user, map[string]interface{}{
|
|
"passkeys_enabled": false,
|
|
"passkey_count": 0,
|
|
"passkeys": []map[string]interface{}{},
|
|
})
|
|
log.Println("POST [api/passkeys/disable] " + r.RemoteAddr + ": Disabled passkeys and revoked old sessions")
|
|
}
|
|
|
|
func listPasskeys(w http.ResponseWriter, r *http.Request) {
|
|
claims := r.Context().Value(auth.UserContextKey).(*auth.Claims)
|
|
passkeys, err := storage.ListPasskeyCredentials(claims.UserID)
|
|
if err != nil {
|
|
log.Println("GET [api/passkeys] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not load passkeys", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"passkeys": publicPasskeys(passkeys),
|
|
"passkeys_enabled": len(passkeys) > 0,
|
|
"passkey_count": len(passkeys),
|
|
})
|
|
}
|
|
|
|
func deletePasskey(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
ID string `json:"id"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
log.Println("DELETE [api/passkeys] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.ID == "" || req.Password == "" {
|
|
http.Error(w, "Passkey ID and password required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
claims := r.Context().Value(auth.UserContextKey).(*auth.Claims)
|
|
user, err := storage.GetUserById(claims.UserID)
|
|
if err != nil {
|
|
log.Println("DELETE [api/passkeys] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "User not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if !auth.CheckPasswordHash(req.Password, user.Password) {
|
|
http.Error(w, "Invalid password", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if _, err := storage.GetPasskeyCredentialByID(user.ID, req.ID); err != nil {
|
|
http.Error(w, "Passkey not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err := storage.DeletePasskeyCredential(user.ID, req.ID); err != nil {
|
|
log.Println("DELETE [api/passkeys] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not delete passkey", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := storage.RevokeAllRefreshTokensForUser(user.ID); err != nil {
|
|
log.Println("DELETE [api/passkeys] " + r.RemoteAddr + ": " + err.Error())
|
|
http.Error(w, "Could not revoke old sessions", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
passkeys, _ := storage.ListPasskeyCredentials(user.ID)
|
|
issueLoginSessionWithExtra(w, r, user, map[string]interface{}{
|
|
"passkeys_enabled": len(passkeys) > 0,
|
|
"passkey_count": len(passkeys),
|
|
"passkeys": publicPasskeys(passkeys),
|
|
})
|
|
log.Println("DELETE [api/passkeys] " + r.RemoteAddr + ": Deleted passkey and revoked old sessions")
|
|
}
|
|
|
|
func loadPasskeyWebAuthnUser(user models.User) (passkeyWebAuthnUser, error) {
|
|
rows, err := storage.ListPasskeyCredentials(user.ID)
|
|
if err != nil {
|
|
return passkeyWebAuthnUser{}, err
|
|
}
|
|
credentials, err := storage.DecodeWebAuthnCredentials(rows)
|
|
if err != nil {
|
|
return passkeyWebAuthnUser{}, err
|
|
}
|
|
return passkeyWebAuthnUser{user: user, credentials: credentials}, nil
|
|
}
|
|
|
|
func passkeyDiscoverableUserHandler(rawID, userHandle []byte) (webauthn.User, error) {
|
|
row, err := storage.GetPasskeyCredentialByCredentialID(utils.EncodeBase64URL(rawID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if row.UserID != string(userHandle) {
|
|
return nil, errors.New("passkey user handle mismatch")
|
|
}
|
|
user, err := storage.GetUserById(row.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return loadPasskeyWebAuthnUser(user)
|
|
}
|
|
|
|
func publicPasskeys(passkeys []models.PasskeyCredential) []map[string]interface{} {
|
|
out := make([]map[string]interface{}, 0, len(passkeys))
|
|
for _, passkey := range passkeys {
|
|
item := map[string]interface{}{
|
|
"id": passkey.ID,
|
|
"name": passkey.Name,
|
|
"credential_id": passkey.CredentialID,
|
|
"created_at": passkey.CreatedAt,
|
|
}
|
|
if passkey.LastUsedAt > 0 {
|
|
item["last_used_at"] = passkey.LastUsedAt
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func webAuthnForRequest(r *http.Request) (*webauthn.WebAuthn, error) {
|
|
origin, rpID, err := passkeyOriginAndRPID(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return webauthn.New(&webauthn.Config{
|
|
RPID: rpID,
|
|
RPDisplayName: "MiauInv",
|
|
RPOrigins: []string{origin},
|
|
RPTopOrigins: []string{origin},
|
|
RPTopOriginVerificationMode: protocol.TopOriginExplicitVerificationMode,
|
|
AuthenticatorSelection: protocol.AuthenticatorSelection{
|
|
RequireResidentKey: protocol.ResidentKeyRequired(),
|
|
ResidentKey: protocol.ResidentKeyRequirementRequired,
|
|
UserVerification: protocol.VerificationRequired,
|
|
},
|
|
})
|
|
}
|
|
|
|
func passkeyOriginAndRPID(r *http.Request) (string, string, error) {
|
|
origin := strings.TrimSpace(r.Header.Get("Origin"))
|
|
if origin == "" {
|
|
scheme := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto"))
|
|
if scheme == "" {
|
|
scheme = "https"
|
|
}
|
|
host := requestHost(r)
|
|
if host == "" {
|
|
return "", "", errors.New("missing request host")
|
|
}
|
|
origin = scheme + "://" + host
|
|
}
|
|
|
|
parsedOrigin, err := url.Parse(origin)
|
|
if err != nil || parsedOrigin.Scheme == "" || parsedOrigin.Host == "" {
|
|
return "", "", errors.New("invalid origin")
|
|
}
|
|
if parsedOrigin.Scheme != "https" && parsedOrigin.Hostname() != "localhost" {
|
|
return "", "", errors.New("passkeys require HTTPS except for localhost")
|
|
}
|
|
|
|
originHost := strings.ToLower(parsedOrigin.Hostname())
|
|
allowedHost := strings.ToLower(stripPort(requestHost(r)))
|
|
if allowedHost != "" && originHost != allowedHost {
|
|
return "", "", errors.New("origin host does not match request host")
|
|
}
|
|
|
|
return parsedOrigin.Scheme + "://" + parsedOrigin.Host, originHost, nil
|
|
}
|
|
|
|
func requestHost(r *http.Request) string {
|
|
if forwardedHost := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); forwardedHost != "" {
|
|
parts := strings.Split(forwardedHost, ",")
|
|
return strings.TrimSpace(parts[0])
|
|
}
|
|
return strings.TrimSpace(r.Host)
|
|
}
|
|
|
|
func stripPort(host string) string {
|
|
if host == "" {
|
|
return ""
|
|
}
|
|
if parsedHost, _, err := net.SplitHostPort(host); err == nil {
|
|
return parsedHost
|
|
}
|
|
if strings.Count(host, ":") == 0 {
|
|
return host
|
|
}
|
|
return host
|
|
}
|
|
|
|
func cloneRequestWithJSONBody(r *http.Request, raw json.RawMessage) *http.Request {
|
|
clone := r.Clone(r.Context())
|
|
clone.Body = io.NopCloser(bytes.NewReader(raw))
|
|
clone.ContentLength = int64(len(raw))
|
|
clone.Header = r.Header.Clone()
|
|
clone.Header.Set("Content-Type", "application/json")
|
|
return clone
|
|
}
|