Skip to content

Commit 5d632ef

Browse files
[VAULT-38600] Create TOTP Login MFA credential self-enrollment API endpoint (#8970) (#8999)
Co-authored-by: Kuba Wieczorek <kuba.wieczorek@hashicorp.com>
1 parent eaf949c commit 5d632ef

File tree

6 files changed

+200
-7
lines changed

6 files changed

+200
-7
lines changed

sdk/queue/priority_queue.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,19 @@ func (pq *PriorityQueue) PopByKey(key string) (*Item, error) {
153153
return nil, nil
154154
}
155155

156+
// PeekByKey returns the item with the given key without removing it from the queue.
157+
func (pq *PriorityQueue) PeekByKey(id string) *Item {
158+
pq.lock.RLock()
159+
defer pq.lock.RUnlock()
160+
161+
item, ok := pq.dataMap[id]
162+
if !ok {
163+
return nil
164+
}
165+
166+
return item
167+
}
168+
156169
// Len returns the number of items in the queue data structure. Do not use this
157170
// method directly on the queue, use PriorityQueue.Len() instead.
158171
func (q queue) Len() int { return len(q) }

sdk/queue/priority_queue_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,67 @@ func TestPriorityQueue_Pop(t *testing.T) {
141141
}
142142
}
143143

144+
// TestPriorityQueue_PeekByKey tests the PeekByKey method of PriorityQueue.
145+
// It verifies that PeekByKey returns the correct item without removing it from the queue,
146+
// handles non-existing keys appropriately, works correctly on empty queues, and
147+
// properly handles empty key strings.
148+
func TestPriorityQueue_PeekByKey(t *testing.T) {
149+
pq := New()
150+
tc := testCases()
151+
expectedLength := len(tc)
152+
153+
// Peek from empty queue
154+
peekedItem := pq.PeekByKey("item-2")
155+
if peekedItem != nil {
156+
t.Fatal("expected nil when peeking from empty queue, got item")
157+
}
158+
if pq.Len() != 0 {
159+
t.Fatalf("expected empty queue to remain size 0, got %d", pq.Len())
160+
}
161+
162+
// Push test items
163+
for _, item := range tc {
164+
if err := pq.Push(item); err != nil {
165+
t.Fatal(err)
166+
}
167+
}
168+
169+
// Peek with empty key
170+
peekedItem = pq.PeekByKey("")
171+
if peekedItem != nil {
172+
t.Fatal("expected nil for empty key, got item")
173+
}
174+
// Verify queue size unchanged
175+
if pq.Len() != expectedLength {
176+
t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len())
177+
}
178+
179+
// Peek at non-existing item
180+
peekedItem = pq.PeekByKey("non-existing-key")
181+
if peekedItem != nil {
182+
t.Fatal("expected nil for non-existing key, got item")
183+
}
184+
// Verify queue size unchanged
185+
if pq.Len() != expectedLength {
186+
t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len())
187+
}
188+
189+
// Peek at a specific item
190+
peekedItem = pq.PeekByKey("item-2")
191+
if peekedItem == nil {
192+
t.Fatal("expected to peek item-2, got nil")
193+
}
194+
// Verify queue size unchanged
195+
if pq.Len() != expectedLength {
196+
t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len())
197+
}
198+
// Verify item still exists in queue
199+
stillExists := pq.PeekByKey("item-2")
200+
if stillExists == nil {
201+
t.Fatal("item should still exist after peek")
202+
}
203+
}
204+
144205
func TestPriorityQueue_PopByKey(t *testing.T) {
145206
pq := New()
146207

vault/core.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3993,14 +3993,34 @@ func (c *Core) loadLoginMFAConfigs(ctx context.Context) error {
39933993
return nil
39943994
}
39953995

3996+
// MFACachedAuthResponse represents an authentication response that has been
3997+
// temporarily cached during a two-phase MFA (Multi-Factor Authentication) login flow.
3998+
//
3999+
// This struct is used when an MFA enforcement is configured and a login request
4000+
// lacks MFA credentials. Instead of completing the authentication immediately,
4001+
// Vault caches the auth response and returns an MFARequirement to the client.
4002+
// The client must then complete MFA validation using the mfa/validate endpoint
4003+
// to retrieve the cached authentication and receive their token.
4004+
//
4005+
// The cached response includes the original authentication details along with
4006+
// request metadata needed for MFA validation, such as the client's IP address
4007+
// for methods like Duo that require connection information.
4008+
//
4009+
// This struct is also used to cache self-enrollment TOTP MFA secrets generated
4010+
// during login when self-enrollment is enabled. This allows Vault to avoid
4011+
// persisting the newly generated MFA secret until it has been successfully used
4012+
// for validating an MFA-enforced login request.
39964013
type MFACachedAuthResponse struct {
3997-
CachedAuth *logical.Auth
3998-
RequestPath string
3999-
RequestNSID string
4000-
RequestNSPath string
4001-
RequestConnRemoteAddr string
4002-
TimeOfStorage time.Time
4003-
RequestID string
4014+
CachedAuth *logical.Auth
4015+
RequestPath string
4016+
RequestNSID string
4017+
RequestNSPath string
4018+
RequestConnRemoteAddr string
4019+
TimeOfStorage time.Time
4020+
RequestID string
4021+
SelfEnrollmentMFASecret *mfa.Secret
4022+
// Store the secret key string separately to avoid anyone accidentally persisting it on an Entity.
4023+
SelfEnrollmentMFASecretKey string
40044024
}
40054025

40064026
func (c *Core) setupCachedMFAResponseAuth() {

vault/identity_store.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ func (i *IdentityStore) paths() []*framework.Path {
165165
mfaDuoPaths(i),
166166
mfaPingIDPaths(i),
167167
mfaLoginEnforcementPaths(i),
168+
mfaLoginEnterprisePaths(i),
168169
)
169170
}
170171

vault/mfa_auth_resp_priority_queue.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package vault
55

66
import (
7+
"errors"
78
"sync"
89
"time"
910

@@ -71,6 +72,20 @@ func (pq *LoginMFAPriorityQueue) PopByKey(reqID string) (*MFACachedAuthResponse,
7172
return item.Value.(*MFACachedAuthResponse), nil
7273
}
7374

75+
// PeekByKey returns the item with the given key without removing it from the queue.
76+
func (pq *LoginMFAPriorityQueue) PeekByKey(reqID string) (*MFACachedAuthResponse, error) {
77+
pq.l.RLock()
78+
defer pq.l.RUnlock()
79+
80+
item := pq.wrapped.PeekByKey(reqID)
81+
if item == nil {
82+
return nil, errors.New("no item found with the given request ID")
83+
}
84+
85+
mfaResp := item.Value.(*MFACachedAuthResponse)
86+
return mfaResp, nil
87+
}
88+
7489
// RemoveExpiredMfaAuthResponse pops elements of the queue and check
7590
// if the entry has expired or not. If the entry has not expired, it pushes
7691
// back the entry to the queue. It returns false if there is no expired element

vault/mfa_auth_resp_priority_queue_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,89 @@ func TestLoginMFAPriorityQueue_PushPopByKey(t *testing.T) {
8787
}
8888
}
8989

90+
// TestLoginMFAPriorityQueue_PeekByKey tests the PeekByKey method of
91+
// LoginMFAPriorityQueue. It verifies that PeekByKey returns the correct item
92+
// without removing it from the queue, returns errors on unhappy paths, handles
93+
// non-existing keys appropriately, works correctly on empty queues, and
94+
// properly handles empty key strings.
95+
func TestLoginMFAPriorityQueue_PeekByKey(t *testing.T) {
96+
pq := NewLoginMFAPriorityQueue()
97+
tc := testCases()
98+
expectedLength := len(tc)
99+
100+
// Peek from empty queue
101+
peekedItem, err := pq.PeekByKey("item-2")
102+
if peekedItem != nil {
103+
t.Fatal("expected nil when peeking from empty queue, got item")
104+
}
105+
if err == nil {
106+
t.Fatal("expected an error when peeking from empty queue, got nil")
107+
}
108+
if pq.Len() != 0 {
109+
t.Fatalf("expected empty queue to remain size 0, got %d", pq.Len())
110+
}
111+
112+
// Push test items
113+
for _, item := range tc {
114+
if err := pq.Push(item); err != nil {
115+
t.Fatal(err)
116+
}
117+
}
118+
119+
// Peek with empty key
120+
peekedItem, err = pq.PeekByKey("")
121+
if peekedItem != nil {
122+
t.Fatal("expected nil for empty key, got item")
123+
}
124+
if err == nil {
125+
t.Fatal("expected error when peeking with empty key , got nil")
126+
}
127+
// Verify queue size unchanged
128+
if pq.Len() != expectedLength {
129+
t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len())
130+
}
131+
132+
// Peek at non-existing item
133+
peekedItem, err = pq.PeekByKey("non-existing-key")
134+
if peekedItem != nil {
135+
t.Fatal("expected nil for non-existing key, got item")
136+
}
137+
if err == nil {
138+
t.Fatal("expected error when peeking with non-existing key, got nil")
139+
}
140+
// Verify queue size unchanged
141+
if pq.Len() != expectedLength {
142+
t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len())
143+
}
144+
145+
// Peek at a specific item
146+
peekedItem, err = pq.PeekByKey(tc[2].RequestID)
147+
if peekedItem == nil {
148+
t.Fatal("expected to peek item-2, got nil")
149+
}
150+
if err != nil {
151+
t.Fatal("expected no error when peeking existing key, got", err)
152+
}
153+
if peekedItem.RequestID != tc[2].RequestID {
154+
t.Fatal("expected the same item on subsequent peeks, got different items")
155+
}
156+
// Verify queue size unchanged
157+
if pq.Len() != expectedLength {
158+
t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len())
159+
}
160+
// Verify item still exists in queue
161+
stillExists, err := pq.PeekByKey(tc[2].RequestID)
162+
if stillExists == nil {
163+
t.Fatal("item should still exist after peek")
164+
}
165+
if err != nil {
166+
t.Fatal("expected no error when peeking existing key for the second time, got", err)
167+
}
168+
if stillExists.RequestID != tc[2].RequestID {
169+
t.Fatal("expected the same item on subsequent peeks, got different items")
170+
}
171+
}
172+
90173
func TestLoginMFARemoveStaleEntries(t *testing.T) {
91174
pq := NewLoginMFAPriorityQueue()
92175

0 commit comments

Comments
 (0)