diff --git a/classes/backend_links.go b/classes/backend_links.go index 974df15..053ab62 100644 --- a/classes/backend_links.go +++ b/classes/backend_links.go @@ -242,7 +242,12 @@ func requestUsable(req *models.BackendLinkRequest, now time.Time) error { // session token's auth_time claim (bouncer.VerifyAuthTime): the time of the // sign-in itself, which a refresh copies and never renews. It is compared in // whole seconds with the request's created_at because the claim has no -// fraction; a zero time is ErrStaleSession. Neither +// fraction, which leaves a one-second blind spot: a sign-in earlier in the +// same second as the request, but before it, counts as fresh. Telling the +// two apart would need a sub-second claim; refusing that whole second +// instead would make a sign-in in the request's own second sign in once +// more. The window is accepted (it needs a sign-in in the very second the +// administrator starts the link). A zero time is ErrStaleSession. Neither // account may be linked already (ErrAlreadyLinked) and the administrator // must still be activated (ErrBackendUserUnavailable). The request is used // up with the link; BackendLinkedEvent fires inside the transaction and the @@ -273,6 +278,7 @@ func (s *BackendLinks) ConfirmLinkRequest(ctx context.Context, plain string, fro if err := requestUsable(req, now); err != nil { return err } + // Whole seconds on both sides: see the blind spot above. if sessionAuthTime.IsZero() || sessionAuthTime.Before(req.CreatedAt.Truncate(time.Second)) { return ErrStaleSession }