This commit is contained in:
@@ -2,6 +2,13 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## v0.1.0-preview.11 — 2026-09-03
|
||||
|
||||
- Add expected-user completion for authenticated self-service passkey
|
||||
enrollment. A mismatched ceremony is consumed and fails before credential
|
||||
persistence, closing an authorization seam found while dogfooding Gamertan's
|
||||
account security page.
|
||||
|
||||
## v0.1.0-preview.10 — 2026-09-03
|
||||
|
||||
- Add atomic public-account registration with required canonical email,
|
||||
|
||||
@@ -17,7 +17,7 @@ router, handlers, HTML, authorization decisions, cache behavior, and
|
||||
deployment. Adopt one boundary at a time; Go compiles and links only the
|
||||
packages you import.
|
||||
|
||||
> **Public preview:** `v0.1.0-preview.10`. APIs may change before a stable
|
||||
> **Public preview:** `v0.1.0-preview.11`. APIs may change before a stable
|
||||
> release. Linux is the maintained release platform.
|
||||
|
||||
## Why Web Foundations?
|
||||
@@ -56,14 +56,14 @@ owns—and, just as importantly, what remains application policy.
|
||||
Pin the preview in an application module:
|
||||
|
||||
```bash
|
||||
go get gamertan.com/web@v0.1.0-preview.10
|
||||
go get gamertan.com/web@v0.1.0-preview.11
|
||||
go mod verify
|
||||
```
|
||||
|
||||
An application may name the first package it intends to adopt:
|
||||
|
||||
```bash
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.10
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.11
|
||||
```
|
||||
|
||||
The version belongs to the `gamertan.com/web` module. See the
|
||||
|
||||
+18
-3
@@ -262,7 +262,19 @@ func (service *Service) beginRegistration(ctx context.Context, user auth.User, l
|
||||
}
|
||||
|
||||
func (service *Service) FinishRegistration(ctx context.Context, ceremonyToken string, response []byte) (Credential, error) {
|
||||
return service.finishRegistration(ctx, ceremonyToken, CeremonyRegistration, [32]byte{}, response, false, false, nil)
|
||||
return service.finishRegistration(ctx, ceremonyToken, CeremonyRegistration, "", [32]byte{}, response, false, false, nil)
|
||||
}
|
||||
|
||||
// FinishRegistrationForUser verifies an ordinary self-service enrollment only
|
||||
// when the ceremony belongs to the authenticated user selected by the
|
||||
// application. The ceremony is consumed on mismatch so a leaked token cannot
|
||||
// be retried through another account session.
|
||||
func (service *Service) FinishRegistrationForUser(ctx context.Context, ceremonyToken, expectedUserID string, response []byte) (Credential, error) {
|
||||
expectedUserID = strings.TrimSpace(expectedUserID)
|
||||
if expectedUserID == "" {
|
||||
return Credential{}, ErrOperationBinding
|
||||
}
|
||||
return service.finishRegistration(ctx, ceremonyToken, CeremonyRegistration, expectedUserID, [32]byte{}, response, false, false, nil)
|
||||
}
|
||||
|
||||
// FinishAccountRegistration verifies an initial credential and delegates its
|
||||
@@ -274,7 +286,7 @@ func (service *Service) FinishAccountRegistration(ctx context.Context, ceremonyT
|
||||
if len(binding) < 16 || len(binding) > 4096 || commit == nil {
|
||||
return Credential{}, ErrOperationBinding
|
||||
}
|
||||
return service.finishRegistration(ctx, ceremonyToken, CeremonyRegistration, BindingDigest(binding), response, false, true, commit)
|
||||
return service.finishRegistration(ctx, ceremonyToken, CeremonyRegistration, "", BindingDigest(binding), response, false, true, commit)
|
||||
}
|
||||
|
||||
// FinishPasswordMigration verifies the new passkey and persists it together
|
||||
@@ -287,11 +299,14 @@ func (service *Service) FinishPasswordMigration(ctx context.Context, ceremonyTok
|
||||
return service.finishRegistrationCeremony(ctx, ceremony, passwordMigrationBinding(ceremony.UserID), response, true, false, nil)
|
||||
}
|
||||
|
||||
func (service *Service) finishRegistration(ctx context.Context, ceremonyToken, kind string, expectedBinding [32]byte, response []byte, retirePassword, allowPending bool, commit RegistrationCommit) (Credential, error) {
|
||||
func (service *Service) finishRegistration(ctx context.Context, ceremonyToken, kind, expectedUserID string, expectedBinding [32]byte, response []byte, retirePassword, allowPending bool, commit RegistrationCommit) (Credential, error) {
|
||||
ceremony, err := service.takeCeremony(ctx, ceremonyToken, kind)
|
||||
if err != nil {
|
||||
return Credential{}, err
|
||||
}
|
||||
if expectedUserID != "" && ceremony.UserID != expectedUserID {
|
||||
return Credential{}, ErrOperationBinding
|
||||
}
|
||||
return service.finishRegistrationCeremony(ctx, ceremony, expectedBinding, response, retirePassword, allowPending, commit)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,12 @@ func TestBootstrapEnrollmentAndApprovalPolicy(t *testing.T) {
|
||||
if !begin.ExpiresAt.Equal(now.Add(5 * time.Minute)) {
|
||||
t.Fatalf("registration expiry=%v", begin.ExpiresAt)
|
||||
}
|
||||
if _, err = service.FinishRegistrationForUser(t.Context(), begin.CeremonyToken, "another-user", []byte(`{}`)); !errors.Is(err, authwebauthn.ErrOperationBinding) {
|
||||
t.Fatalf("cross-account registration completion err=%v", err)
|
||||
}
|
||||
if _, err = service.FinishRegistrationForUser(t.Context(), begin.CeremonyToken, user.ID, []byte(`{}`)); !errors.Is(err, authwebauthn.ErrCeremonyNotFound) {
|
||||
t.Fatalf("mismatched completion did not consume ceremony: %v", err)
|
||||
}
|
||||
|
||||
if err = service.RequireReady(t.Context(), user.ID); !errors.Is(err, authwebauthn.ErrPasskeyReadiness) {
|
||||
t.Fatalf("readiness without credentials err=%v", err)
|
||||
|
||||
@@ -34,3 +34,7 @@ application concern belongs in the shared module.
|
||||
explicit operator command.
|
||||
- Commerce remains a separately versioned nested module so payment-provider
|
||||
policy and catalog evolution do not enlarge the authentication core.
|
||||
- Self-service enrollment exposed an authorization seam: completing a valid
|
||||
ceremony and checking its user only after persistence is too late.
|
||||
`FinishRegistrationForUser` now consumes mismatched ceremonies and checks
|
||||
the application-authenticated user before storing a credential.
|
||||
|
||||
@@ -25,7 +25,7 @@ The packages are ordinary Go imports. Pin the current preview and verify its
|
||||
module checksum:
|
||||
|
||||
```bash
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.10
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.11
|
||||
go mod verify
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import "gamertan.com/web/requestmeta"
|
||||
and request the containing module at an exact version:
|
||||
|
||||
```bash
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.10
|
||||
go get gamertan.com/web/requestmeta@v0.1.0-preview.11
|
||||
```
|
||||
|
||||
Only imported packages are compiled and linked. The packages nevertheless
|
||||
|
||||
+3
-1
@@ -31,7 +31,9 @@ timestamp, UUID, or counter for the random challenge.
|
||||
2. A server-rendered enrollment page calls `BeginEnrollment`; the browser uses
|
||||
`navigator.credentials.create` with the returned `public_key` value.
|
||||
3. The browser posts the credential and opaque ceremony token to a bounded JSON
|
||||
endpoint; `FinishRegistration` verifies and stores the public credential.
|
||||
endpoint; authenticated self-service flows use
|
||||
`FinishRegistrationForUser` so the application session's user ID is checked
|
||||
before any public credential is stored.
|
||||
4. Login uses `BeginLogin`, `navigator.credentials.get`, and `FinishLogin`.
|
||||
The successful result contains an ordinary opaque `auth` session token.
|
||||
5. Sensitive operations call `BeginApproval` with a canonical application
|
||||
|
||||
Reference in New Issue
Block a user