package controllers import ( "bytes" "encoding/json" "io" "net/http" "time" "code.crute.us/mcrute/golib/echo/session" "code.crute.us/mcrute/ssh-proxy/app" "code.crute.us/mcrute/ssh-proxy/app/models" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/labstack/echo/v4" ) type LoginController[T app.AppSession] struct { Logger echo.Logger Sessions session.Store[T] Users models.UserStore AuthSessions models.AuthSessionStore Webauthn *webauthn.WebAuthn SessionExpiration time.Duration } func (a *LoginController[T]) HandleStart(c echo.Context) error { user, err := a.Users.Get(c.Request().Context(), c.Param("username")) if err != nil { a.Logger.Errorf("Error getting user: %s", err) return c.NoContent(http.StatusNotFound) } request, sessionData, err := a.Webauthn.BeginLogin(user) if err != nil { a.Logger.Errorf("Error creating webauthn request: %s", err) return c.NoContent(http.StatusInternalServerError) } session := a.Sessions.Get(c) s := session.Self() s.WebauthnSession = sessionData a.Sessions.Update(c, session) return c.JSON(http.StatusOK, request) } func (a *LoginController[T]) HandleFinish(c echo.Context) error { ctx := c.Request().Context() body, err := io.ReadAll(c.Request().Body) if err != nil { a.Logger.Errorf("Error reading request body:", err) return c.NoContent(http.StatusInternalServerError) } user, err := a.Users.Get(ctx, c.Param("username")) if err != nil { a.Logger.Errorf("Error getting user: %s", err) return c.NoContent(http.StatusNotFound) } response, err := protocol.ParseCredentialRequestResponseBody(bytes.NewBuffer(body)) if err != nil { a.Logger.Errorf("Error parsing credential response: %s", err) return c.NoContent(http.StatusBadRequest) } session := a.Sessions.Get(c) s := session.Self() if s.WebauthnSession == nil { a.Logger.Errorf("Webauthn session is not set") return c.NoContent(http.StatusBadRequest) } if _, err := a.Webauthn.ValidateLogin(user, *s.WebauthnSession, response); err != nil { a.Logger.Errorf("Error validating login: %s", err) return c.NoContent(http.StatusBadRequest) } // Don't check the clone warning or the auth count because these are // meaningless for Passkeys since they are synced across devices // (presumably securely). This would only matter for hard tokens like // Yubikeys and since we're also allowing Passkey support there is no // need to be more strict for that class of device. var code struct { Code string `json:"code"` } if err := json.Unmarshal(body, &code); err != nil { a.Logger.Errorf("Error decoding json body") return c.NoContent(http.StatusBadRequest) } authSession, err := a.AuthSessions.GetByUserCode(ctx, code.Code) if err != nil { a.Logger.Errorf("No auth session exists") return c.NoContent(http.StatusUnauthorized) } if authSession.AccessCode != "" { a.Logger.Errorf("Session is already authenticated") return c.NoContent(http.StatusUnauthorized) } authSession.GenerateAccessCode() authSession.UserId = user.Username authSession.Expires = time.Now().Add(a.SessionExpiration) if err := a.AuthSessions.Upsert(ctx, authSession); err != nil { a.Logger.Errorf("Error saving auth session") return c.NoContent(http.StatusInternalServerError) } return c.NoContent(http.StatusOK) }