mavweb: add in-process auth gate for POST /tools
Add a local PasskeySession that handleTools checks before processing any POST action (enable/disable). If the session hasn't been asserted within the 5-minute TTL, return 403 Forbidden. Changes: - webauthn/session.go: add IsStepUp() convenience method (nil-safe) - webauthn.go: PasskeyHandle holds a *PasskeySession; AssertFinish calls session.Assert() after IPC step-up - main.go: create stepUpSession, pass to handleTools and newPasskeyHandle; handleTools returns 403 if !session.IsStepUp() - handlers_test.go: update TestEnableTool_NoInProcessAuthGate to expect 403; add TestEnableTool_WithAuthGate_RequiresStepUp for the happy path with asserted session; update all 10 call sites
This commit is contained in:
@@ -60,11 +60,11 @@ Notes:
|
|||||||
|
|
||||||
| # | Task | Commit | Status |
|
| # | Task | Commit | Status |
|
||||||
|---|------|--------|--------|
|
|---|------|--------|--------|
|
||||||
| 12 | **Recurring reminders** — cron expression in reminder table. New `cron` column, `next_fire_ts` computed from cron. "trash every tuesday" | — | pending |
|
| 12 | **Recurring reminders** — cron+next_fire_ts cols, RescheduleReminder, dispatcher logic | 1eca17f | done |
|
||||||
| 13 | **Capability model** — `scope` column on tools table, migration, UI, tests | 6b80fd0 | done |
|
| 13 | **Capability model** — `scope` column on tools table, migration, UI, tests | 6b80fd0 | done |
|
||||||
| 14 | **Notification batching / digest mode** — morning/evening rollup instead of per-event nudges. Configurable window, accumulated messages in one delivery | — | pending |
|
| 14 | **Notification batching / digest mode** — morning/evening rollup instead of per-event nudges. Configurable window, accumulated messages in one delivery | — | pending |
|
||||||
| 15 | **Rule trace / explanation engine** — `why` query: "why did/didn't you nudge me?" Reads predicate eval log. New `/trace` page or CLI query | — | pending |
|
| 15 | **Rule trace / explanation engine** — `why` query: "why did/didn't you nudge me?" Reads predicate eval log. New `/trace` page or CLI query | — | pending |
|
||||||
| 16 | **Backup/restore automation** — script: `cp` the encrypted DB + re-encrypt-verify + restore flow | — | pending |
|
| 16 | **Backup/restore automation** — scripts/maven-backup.sh with backup/restore/verify/list | 3f09cdb | done |
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Recurring reminders: needs schema migration (#1). Add `cron TEXT` and `next_fire_ts INTEGER` to reminders table.
|
- Recurring reminders: needs schema migration (#1). Add `cron TEXT` and `next_fire_ts INTEGER` to reminders table.
|
||||||
@@ -82,7 +82,7 @@ Notes:
|
|||||||
| 19 | **Rule trace page** — new `/trace` route showing predicate eval results per rule per tick | — | pending |
|
| 19 | **Rule trace page** — new `/trace` route showing predicate eval results per rule per tick | — | pending |
|
||||||
| 20 | **Command history page** — new `/history` route showing recent facts/commands | f8ba396 | done |
|
| 20 | **Command history page** — new `/history` route showing recent facts/commands | f8ba396 | done |
|
||||||
| 21 | **PWA icons** — add proper icon array to `manifest.json` (generate or inline SVG) | — | pending |
|
| 21 | **PWA icons** — add proper icon array to `manifest.json` (generate or inline SVG) | — | pending |
|
||||||
| 22 | **Language unification** — pick Russian or add `lang` URL param toggle | — | pending |
|
| 22 | **Language unification** — bilingual cheatsheet with RU/EN toggle in nav + ?lang= param | c225ba3 | done |
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Current `/tools` has no in-process auth — `handlers_test.go` explicitly pins this behavior with `TestEnableTool_NoInProcessAuthGate`.
|
- Current `/tools` has no in-process auth — `handlers_test.go` explicitly pins this behavior with `TestEnableTool_NoInProcessAuthGate`.
|
||||||
|
|||||||
+48
-24
@@ -11,6 +11,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/auth"
|
||||||
"github.com/kami/maven/internal/ipc"
|
"github.com/kami/maven/internal/ipc"
|
||||||
"github.com/kami/maven/internal/webauthn"
|
"github.com/kami/maven/internal/webauthn"
|
||||||
)
|
)
|
||||||
@@ -138,7 +139,7 @@ func TestHandleTools_GET_RendersAndEscapes(t *testing.T) {
|
|||||||
enabled: []ipc.Tool{{Name: "svc", Scope: "", Cmd: []string{"systemctl", "restart"}, Destructive: true}},
|
enabled: []ipc.Tool{{Name: "svc", Scope: "", Cmd: []string{"systemctl", "restart"}, Destructive: true}},
|
||||||
}
|
}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleTools(rr, httptest.NewRequest(http.MethodGet, "/tools", nil), core)
|
handleTools(rr, httptest.NewRequest(http.MethodGet, "/tools", nil), core, nil)
|
||||||
|
|
||||||
if rr.Code != http.StatusOK {
|
if rr.Code != http.StatusOK {
|
||||||
t.Fatalf("status = %d, want 200", rr.Code)
|
t.Fatalf("status = %d, want 200", rr.Code)
|
||||||
@@ -158,7 +159,7 @@ func TestHandleTools_GET_RendersAndEscapes(t *testing.T) {
|
|||||||
|
|
||||||
func TestHandleTools_NilCore_503(t *testing.T) {
|
func TestHandleTools_NilCore_503(t *testing.T) {
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleTools(rr, httptest.NewRequest(http.MethodGet, "/tools", nil), nil)
|
handleTools(rr, httptest.NewRequest(http.MethodGet, "/tools", nil), nil, nil)
|
||||||
if rr.Code != http.StatusServiceUnavailable {
|
if rr.Code != http.StatusServiceUnavailable {
|
||||||
t.Fatalf("status = %d, want 503", rr.Code)
|
t.Fatalf("status = %d, want 503", rr.Code)
|
||||||
}
|
}
|
||||||
@@ -173,6 +174,14 @@ func postForm(action string, vals url.Values) *http.Request {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stepUpSession returns a PasskeySession that has already been asserted,
|
||||||
|
// so POST /tools calls can pass the in-process auth gate.
|
||||||
|
func stepUpSession() *webauthn.PasskeySession {
|
||||||
|
s := webauthn.NewPasskeySession(5 * time.Minute)
|
||||||
|
s.Assert(context.Background(), auth.Scope{})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleTools_POST_Enable_HappyPath(t *testing.T) {
|
func TestHandleTools_POST_Enable_HappyPath(t *testing.T) {
|
||||||
core := &fakeCore{}
|
core := &fakeCore{}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
@@ -180,7 +189,7 @@ func TestHandleTools_POST_Enable_HappyPath(t *testing.T) {
|
|||||||
"name": {"svc"},
|
"name": {"svc"},
|
||||||
"cmd": {"systemctl restart nginx"},
|
"cmd": {"systemctl restart nginx"},
|
||||||
"destructive": {"on"},
|
"destructive": {"on"},
|
||||||
}), core)
|
}), core, stepUpSession())
|
||||||
|
|
||||||
if rr.Code != http.StatusOK {
|
if rr.Code != http.StatusOK {
|
||||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||||
@@ -199,7 +208,7 @@ func TestHandleTools_POST_Enable_HappyPath(t *testing.T) {
|
|||||||
func TestHandleTools_POST_Enable_MissingName_400(t *testing.T) {
|
func TestHandleTools_POST_Enable_MissingName_400(t *testing.T) {
|
||||||
core := &fakeCore{}
|
core := &fakeCore{}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleTools(rr, postForm("enable", url.Values{"cmd": {"systemctl restart"}}), core)
|
handleTools(rr, postForm("enable", url.Values{"cmd": {"systemctl restart"}}), core, stepUpSession())
|
||||||
if rr.Code != http.StatusBadRequest {
|
if rr.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("status = %d, want 400", rr.Code)
|
t.Fatalf("status = %d, want 400", rr.Code)
|
||||||
}
|
}
|
||||||
@@ -208,7 +217,7 @@ func TestHandleTools_POST_Enable_MissingName_400(t *testing.T) {
|
|||||||
func TestHandleTools_POST_Enable_MissingCmd_400(t *testing.T) {
|
func TestHandleTools_POST_Enable_MissingCmd_400(t *testing.T) {
|
||||||
core := &fakeCore{}
|
core := &fakeCore{}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleTools(rr, postForm("enable", url.Values{"name": {"svc"}}), core)
|
handleTools(rr, postForm("enable", url.Values{"name": {"svc"}}), core, stepUpSession())
|
||||||
if rr.Code != http.StatusBadRequest {
|
if rr.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("status = %d, want 400", rr.Code)
|
t.Fatalf("status = %d, want 400", rr.Code)
|
||||||
}
|
}
|
||||||
@@ -219,7 +228,7 @@ func TestHandleTools_POST_Enable_CoreError_502(t *testing.T) {
|
|||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleTools(rr, postForm("enable", url.Values{
|
handleTools(rr, postForm("enable", url.Values{
|
||||||
"name": {"svc"}, "cmd": {"systemctl restart"},
|
"name": {"svc"}, "cmd": {"systemctl restart"},
|
||||||
}), core)
|
}), core, stepUpSession())
|
||||||
if rr.Code != http.StatusBadGateway {
|
if rr.Code != http.StatusBadGateway {
|
||||||
t.Fatalf("status = %d, want 502", rr.Code)
|
t.Fatalf("status = %d, want 502", rr.Code)
|
||||||
}
|
}
|
||||||
@@ -228,7 +237,7 @@ func TestHandleTools_POST_Enable_CoreError_502(t *testing.T) {
|
|||||||
func TestHandleTools_POST_UnknownAction_400(t *testing.T) {
|
func TestHandleTools_POST_UnknownAction_400(t *testing.T) {
|
||||||
core := &fakeCore{}
|
core := &fakeCore{}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleTools(rr, postForm("frobnicate", url.Values{"name": {"svc"}}), core)
|
handleTools(rr, postForm("frobnicate", url.Values{"name": {"svc"}}), core, stepUpSession())
|
||||||
if rr.Code != http.StatusBadRequest {
|
if rr.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("status = %d, want 400", rr.Code)
|
t.Fatalf("status = %d, want 400", rr.Code)
|
||||||
}
|
}
|
||||||
@@ -239,7 +248,7 @@ func TestHandleTools_POST_UnknownAction_400(t *testing.T) {
|
|||||||
func TestHandleTools_POST_Disable_HappyPath(t *testing.T) {
|
func TestHandleTools_POST_Disable_HappyPath(t *testing.T) {
|
||||||
core := &fakeCore{}
|
core := &fakeCore{}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleTools(rr, postForm("disable", url.Values{"name": {"svc"}}), core)
|
handleTools(rr, postForm("disable", url.Values{"name": {"svc"}}), core, stepUpSession())
|
||||||
if rr.Code != http.StatusOK {
|
if rr.Code != http.StatusOK {
|
||||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||||
}
|
}
|
||||||
@@ -251,7 +260,7 @@ func TestHandleTools_POST_Disable_HappyPath(t *testing.T) {
|
|||||||
func TestHandleTools_POST_Disable_MissingName_400(t *testing.T) {
|
func TestHandleTools_POST_Disable_MissingName_400(t *testing.T) {
|
||||||
core := &fakeCore{}
|
core := &fakeCore{}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleTools(rr, postForm("disable", url.Values{}), core)
|
handleTools(rr, postForm("disable", url.Values{}), core, stepUpSession())
|
||||||
if rr.Code != http.StatusBadRequest {
|
if rr.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("status = %d, want 400", rr.Code)
|
t.Fatalf("status = %d, want 400", rr.Code)
|
||||||
}
|
}
|
||||||
@@ -260,29 +269,44 @@ func TestHandleTools_POST_Disable_MissingName_400(t *testing.T) {
|
|||||||
func TestHandleTools_POST_Disable_CoreError_502(t *testing.T) {
|
func TestHandleTools_POST_Disable_CoreError_502(t *testing.T) {
|
||||||
core := &fakeCore{disableErr: ipc.ErrToolNotFound}
|
core := &fakeCore{disableErr: ipc.ErrToolNotFound}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleTools(rr, postForm("disable", url.Values{"name": {"svc"}}), core)
|
handleTools(rr, postForm("disable", url.Values{"name": {"svc"}}), core, stepUpSession())
|
||||||
if rr.Code != http.StatusBadGateway {
|
if rr.Code != http.StatusBadGateway {
|
||||||
t.Fatalf("status = %d, want 502", rr.Code)
|
t.Fatalf("status = %d, want 502", rr.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEnableTool_NoInProcessAuthGate documents CURRENT behavior: handleTools
|
// TestEnableTool_NoInProcessAuthGate verifies that a POST /tools without a
|
||||||
// performs the boundary-moving EnableTool with no in-process authentication —
|
// prior WebAuthn assertion is rejected with 403 Forbidden — the in-process
|
||||||
// there is no passkey/session check in the Go handler. A POST enable with no
|
// auth gate requires step-up before any mutation (enable/disable).
|
||||||
// prior WebAuthn AssertFinish succeeds (reaches core.EnableTool). Authorization
|
|
||||||
// is delegated entirely to the nginx+wg layer in front of mavweb. Whether that
|
|
||||||
// is the intended sole gate is a maintainer decision; this test only pins the
|
|
||||||
// observed behavior so a future auth gate change is a deliberate, visible edit.
|
|
||||||
func TestEnableTool_NoInProcessAuthGate(t *testing.T) {
|
func TestEnableTool_NoInProcessAuthGate(t *testing.T) {
|
||||||
core := &fakeCore{}
|
core := &fakeCore{}
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
// No passkey session, no cookie, no header — just the raw POST.
|
// No passkey session (nil) — no step-up, gate rejects.
|
||||||
handleTools(rr, postForm("enable", url.Values{
|
handleTools(rr, postForm("enable", url.Values{
|
||||||
"name": {"svc"}, "cmd": {"systemctl restart"},
|
"name": {"svc"}, "cmd": {"systemctl restart"},
|
||||||
}), core)
|
}), core, nil)
|
||||||
if rr.Code != http.StatusOK || core.gotEnableName != "svc" {
|
if rr.Code != http.StatusForbidden {
|
||||||
t.Fatalf("expected unauthenticated enable to reach core (status=%d, name=%q); "+
|
t.Fatalf("status = %d, want %d (expected auth gate to reject)", rr.Code, http.StatusForbidden)
|
||||||
"if this now fails, an in-process auth gate was added", rr.Code, core.gotEnableName)
|
}
|
||||||
|
if core.gotEnableName != "" {
|
||||||
|
t.Errorf("core.EnableTool was called with name=%q, but auth gate should have blocked it", core.gotEnableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEnableTool_WithAuthGate_RequiresStepUp verifies that a POST /tools with
|
||||||
|
// an asserted passkey session proceeds past the auth gate to core.EnableTool.
|
||||||
|
func TestEnableTool_WithAuthGate_RequiresStepUp(t *testing.T) {
|
||||||
|
core := &fakeCore{}
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
sess := stepUpSession()
|
||||||
|
handleTools(rr, postForm("enable", url.Values{
|
||||||
|
"name": {"svc"}, "cmd": {"systemctl restart"},
|
||||||
|
}), core, sess)
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
if core.gotEnableName != "svc" {
|
||||||
|
t.Errorf("name = %q, want svc", core.gotEnableName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +323,7 @@ func newTestPasskey(t *testing.T) *PasskeyHandle {
|
|||||||
Origin: "https://maven.example",
|
Origin: "https://maven.example",
|
||||||
RPID: "maven.example",
|
RPID: "maven.example",
|
||||||
RPName: "maven",
|
RPName: "maven",
|
||||||
}, nil, f.Name())
|
}, nil, f.Name(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -638,7 +662,7 @@ func TestHandleTools_ListToolsError_502(t *testing.T) {
|
|||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handleTools(rr, postForm("enable", url.Values{
|
handleTools(rr, postForm("enable", url.Values{
|
||||||
"name": {"svc"}, "cmd": {"systemctl restart"},
|
"name": {"svc"}, "cmd": {"systemctl restart"},
|
||||||
}), core)
|
}), core, stepUpSession())
|
||||||
if rr.Code != http.StatusBadGateway {
|
if rr.Code != http.StatusBadGateway {
|
||||||
t.Fatalf("status = %d, want 502; body=%s", rr.Code, rr.Body.String())
|
t.Fatalf("status = %d, want 502; body=%s", rr.Code, rr.Body.String())
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-3
@@ -142,12 +142,14 @@ func main() {
|
|||||||
// Passkey registration + assertion are the step-up mechanism for
|
// Passkey registration + assertion are the step-up mechanism for
|
||||||
// AuthStepUp actions (tool enable). Without -webauthn-origin, these
|
// AuthStepUp actions (tool enable). Without -webauthn-origin, these
|
||||||
// endpoints return 503 and step-up is unavailable (FloorSession).
|
// endpoints return 503 and step-up is unavailable (FloorSession).
|
||||||
|
stepUpSession := webauthn.NewPasskeySession(5 * time.Minute)
|
||||||
|
|
||||||
if *pkOrigin != "" && *pkRPID != "" && core != nil {
|
if *pkOrigin != "" && *pkRPID != "" && core != nil {
|
||||||
pk, err := newPasskeyHandle(webauthn.Config{
|
pk, err := newPasskeyHandle(webauthn.Config{
|
||||||
Origin: *pkOrigin,
|
Origin: *pkOrigin,
|
||||||
RPID: *pkRPID,
|
RPID: *pkRPID,
|
||||||
RPName: "maven",
|
RPName: "maven",
|
||||||
}, core, *pkFile)
|
}, core, *pkFile, stepUpSession)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("passkey store: %v", err)
|
log.Fatalf("passkey store: %v", err)
|
||||||
}
|
}
|
||||||
@@ -163,7 +165,7 @@ func main() {
|
|||||||
// Enabling is the boundary-moving act (maven.md), so it lives ONLY here,
|
// Enabling is the boundary-moving act (maven.md), so it lives ONLY here,
|
||||||
// behind wg+nginx+auth — never the voice/chat path.
|
// behind wg+nginx+auth — never the voice/chat path.
|
||||||
mux.HandleFunc("/tools", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/tools", func(w http.ResponseWriter, r *http.Request) {
|
||||||
handleTools(w, r, core)
|
handleTools(w, r, core, stepUpSession)
|
||||||
})
|
})
|
||||||
|
|
||||||
srv := &http.Server{Addr: *addr, Handler: mux}
|
srv := &http.Server{Addr: *addr, Handler: mux}
|
||||||
@@ -425,7 +427,7 @@ func handleRevert(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|||||||
// whitespace-split — argv with embedded spaces isn't supported (ponytail: no
|
// whitespace-split — argv with embedded spaces isn't supported (ponytail: no
|
||||||
// shell-word parsing; the box owner controls this input, quote a wrapper script
|
// shell-word parsing; the box owner controls this input, quote a wrapper script
|
||||||
// if an arg needs spaces).
|
// if an arg needs spaces).
|
||||||
func handleTools(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
func handleTools(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, session *webauthn.PasskeySession) {
|
||||||
if core == nil {
|
if core == nil {
|
||||||
http.Error(w, "tools disabled (no -core)", http.StatusServiceUnavailable)
|
http.Error(w, "tools disabled (no -core)", http.StatusServiceUnavailable)
|
||||||
return
|
return
|
||||||
@@ -433,6 +435,10 @@ func handleTools(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
var msg string
|
var msg string
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
|
if !session.IsStepUp() {
|
||||||
|
http.Error(w, "step-up required: assert a passkey first", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
action := r.FormValue("action")
|
action := r.FormValue("action")
|
||||||
name := strings.TrimSpace(r.FormValue("name"))
|
name := strings.TrimSpace(r.FormValue("name"))
|
||||||
switch action {
|
switch action {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/auth"
|
||||||
"github.com/kami/maven/internal/ipc"
|
"github.com/kami/maven/internal/ipc"
|
||||||
"github.com/kami/maven/internal/webauthn"
|
"github.com/kami/maven/internal/webauthn"
|
||||||
)
|
)
|
||||||
@@ -29,6 +30,7 @@ type PasskeyHandle struct {
|
|||||||
rp *webauthn.RP
|
rp *webauthn.RP
|
||||||
assertFn assertIPC // *ipc.Client when connected; nil ⇒ no step-up IPC
|
assertFn assertIPC // *ipc.Client when connected; nil ⇒ no step-up IPC
|
||||||
store *credentialStore
|
store *credentialStore
|
||||||
|
session *webauthn.PasskeySession
|
||||||
}
|
}
|
||||||
|
|
||||||
type localCred struct {
|
type localCred struct {
|
||||||
@@ -36,7 +38,7 @@ type localCred struct {
|
|||||||
SignCount int64
|
SignCount int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPasskeyHandle(cfg webauthn.Config, core ipc.CoreAPI, storePath string) (*PasskeyHandle, error) {
|
func newPasskeyHandle(cfg webauthn.Config, core ipc.CoreAPI, storePath string, session *webauthn.PasskeySession) (*PasskeyHandle, error) {
|
||||||
var af assertIPC
|
var af assertIPC
|
||||||
if c, ok := core.(assertIPC); ok {
|
if c, ok := core.(assertIPC); ok {
|
||||||
af = c
|
af = c
|
||||||
@@ -49,6 +51,7 @@ func newPasskeyHandle(cfg webauthn.Config, core ipc.CoreAPI, storePath string) (
|
|||||||
rp: webauthn.NewRP(cfg),
|
rp: webauthn.NewRP(cfg),
|
||||||
assertFn: af,
|
assertFn: af,
|
||||||
store: store,
|
store: store,
|
||||||
|
session: session,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +193,11 @@ func (h *PasskeyHandle) AssertFinish(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assert the in-process session so the POST /tools handler sees step-up.
|
||||||
|
if h.session != nil {
|
||||||
|
h.session.Assert(r.Context(), auth.Scope{})
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("webauthn: asserted credential %s", credID)
|
log.Printf("webauthn: asserted credential %s", credID)
|
||||||
json.NewEncoder(w).Encode(map[string]string{"credential_id": credID})
|
json.NewEncoder(w).Encode(map[string]string{"credential_id": credID})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ func (s *PasskeySession) CurrentLayer(_ context.Context, _ auth.Scope) auth.Laye
|
|||||||
return auth.Layer2
|
return auth.Layer2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsStepUp returns true if the session was recently asserted (within TTL).
|
||||||
|
func (s *PasskeySession) IsStepUp() bool {
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return !s.assertedAt.IsZero() && time.Since(s.assertedAt) < s.assertionTTL
|
||||||
|
}
|
||||||
|
|
||||||
// Assert records a successful step-up gesture. The session bumps to L3 for
|
// Assert records a successful step-up gesture. The session bumps to L3 for
|
||||||
// the assertion TTL. A nil receiver returns ErrStepUpUnsupported.
|
// the assertion TTL. A nil receiver returns ErrStepUpUnsupported.
|
||||||
func (s *PasskeySession) Assert(_ context.Context, _ auth.Scope) error {
|
func (s *PasskeySession) Assert(_ context.Context, _ auth.Scope) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user