Skip to content

Commit c5950bc

Browse files
committed
use the authenticated Key as the "old" key for the rotation endpoint
1 parent cab4084 commit c5950bc

File tree

5 files changed

+63
-81
lines changed

5 files changed

+63
-81
lines changed

apikey.go

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ const ApiKeyTablePK = "value"
2525
const (
2626
paramNewKeyId = "newKeyId"
2727
paramNewKeySecret = "newKeySecret"
28-
paramOldKeyId = "oldKeyId"
29-
paramOldKeySecret = "oldKeySecret"
3028
)
3129

3230
const (
@@ -451,22 +449,28 @@ func (a *App) CreateApiKey(w http.ResponseWriter, r *http.Request) {
451449
// any number of times to continue the process. A status of 200 does not indicate that all keys were encrypted using the
452450
// new key. Check the response data to determine if the rotation process is complete.
453451
func (a *App) RotateApiKey(w http.ResponseWriter, r *http.Request) {
454-
requestBody, err := parseRotateKeyRequestBody(r.Body)
452+
var requestBody map[string]string
453+
err := json.NewDecoder(r.Body).Decode(&requestBody)
455454
if err != nil {
456-
if strings.HasSuffix(err.Error(), "is required") {
457-
jsonResponse(w, err, http.StatusBadRequest)
458-
} else {
459-
log.Printf("invalid request in RotateApiKey: %s", err)
460-
jsonResponse(w, invalidRequest, http.StatusBadRequest)
461-
}
455+
log.Printf("invalid request in ActivateApiKey: %s", err)
456+
jsonResponse(w, invalidRequest, http.StatusBadRequest)
457+
return
458+
}
459+
460+
if requestBody[paramNewKeyId] == "" {
461+
jsonResponse(w, paramNewKeyId+" is required", http.StatusBadRequest)
462462
return
463463
}
464464

465-
oldKey := ApiKey{Key: requestBody[paramOldKeyId], Store: a.GetDB()}
466-
err = oldKey.loadAndCheck(requestBody[paramOldKeySecret])
465+
if requestBody[paramNewKeySecret] == "" {
466+
jsonResponse(w, paramNewKeySecret+" is required", http.StatusBadRequest)
467+
return
468+
}
469+
470+
oldKey, err := getAPIKey(r)
467471
if err != nil {
468-
log.Printf("old key is not valid: %s", err)
469-
jsonResponse(w, apiKeyNotFound, http.StatusNotFound)
472+
log.Printf("Rotate API key error: %v", err)
473+
jsonResponse(w, internalServerError, http.StatusInternalServerError)
470474
return
471475
}
472476

@@ -502,22 +506,6 @@ func (a *App) RotateApiKey(w http.ResponseWriter, r *http.Request) {
502506
jsonResponse(w, responseBody, http.StatusOK)
503507
}
504508

505-
func parseRotateKeyRequestBody(body io.Reader) (map[string]string, error) {
506-
var requestBody map[string]string
507-
err := json.NewDecoder(body).Decode(&requestBody)
508-
if err != nil {
509-
return nil, fmt.Errorf("invalid request in RotateApiKey: %w", err)
510-
}
511-
512-
fields := []string{paramNewKeyId, paramNewKeySecret, paramOldKeyId, paramOldKeySecret}
513-
for _, field := range fields {
514-
if _, ok := requestBody[field]; !ok {
515-
return nil, fmt.Errorf("%s is required", field)
516-
}
517-
}
518-
return requestBody, nil
519-
}
520-
521509
func (k *ApiKey) loadAndCheck(secret string) error {
522510
err := k.Load()
523511
if err != nil {

apikey_test.go

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package mfa
22

33
import (
44
"bytes"
5+
"context"
56
"crypto/aes"
67
"crypto/rand"
78
"encoding/base64"
@@ -10,6 +11,7 @@ import (
1011
"fmt"
1112
"io"
1213
"net/http"
14+
"net/http/httptest"
1315
"regexp"
1416
"testing"
1517
"time"
@@ -364,78 +366,70 @@ func (ms *MfaSuite) TestAppRotateApiKey() {
364366
tests := []struct {
365367
name string
366368
body any
369+
key ApiKey
367370
wantStatus int
368-
wantError error
371+
wantError string
369372
}{
370373
{
371-
name: "missing oldKeyId",
372-
body: map[string]interface{}{
373-
paramNewKeyId: newKey.Key,
374-
paramNewKeySecret: newKey.Secret,
375-
paramOldKeySecret: key.Secret,
376-
},
377-
wantStatus: http.StatusBadRequest,
378-
wantError: errors.New("oldKeyId is required"),
379-
},
380-
{
381-
name: "missing oldKeySecret",
374+
name: "missing key",
382375
body: map[string]interface{}{
383376
paramNewKeyId: newKey.Key,
384377
paramNewKeySecret: newKey.Secret,
385-
paramOldKeyId: key.Key,
386378
},
387-
wantStatus: http.StatusBadRequest,
388-
wantError: errors.New("oldKeySecret is required"),
379+
wantStatus: http.StatusUnauthorized,
380+
wantError: "Unauthorized",
389381
},
390382
{
391383
name: "missing newKeyId",
392384
body: map[string]interface{}{
393385
paramNewKeySecret: newKey.Secret,
394-
paramOldKeyId: key.Key,
395-
paramOldKeySecret: key.Secret,
396386
},
387+
key: key,
397388
wantStatus: http.StatusBadRequest,
398-
wantError: errors.New("newKeyId is required"),
389+
wantError: "newKeyId is required",
399390
},
400391
{
401392
name: "missing newKeySecret",
402393
body: map[string]interface{}{
403-
paramNewKeyId: newKey.Key,
404-
paramOldKeyId: key.Key,
405-
paramOldKeySecret: key.Secret,
394+
paramNewKeyId: newKey.Key,
406395
},
396+
key: key,
407397
wantStatus: http.StatusBadRequest,
408-
wantError: errors.New("newKeySecret is required"),
398+
wantError: "newKeySecret is required",
409399
},
410400
{
411401
name: "good",
412402
body: map[string]interface{}{
413403
paramNewKeyId: newKey.Key,
414404
paramNewKeySecret: newKey.Secret,
415-
paramOldKeyId: user.ApiKey.Key,
416-
paramOldKeySecret: key.Secret,
417405
},
406+
key: key,
418407
wantStatus: http.StatusOK,
419408
},
420409
}
421410
for _, tt := range tests {
422411
ms.Run(tt.name, func() {
423-
res := &lambdaResponseWriter{Headers: http.Header{}}
424-
req := requestWithUser(tt.body, key)
425-
ms.app.RotateApiKey(res, req)
426-
427-
if tt.wantError != nil {
428-
ms.Equal(tt.wantStatus, res.Status, fmt.Sprintf("CreateApiKey response: %s", res.Body))
429-
var se simpleError
430-
ms.decodeBody(res.Body, &se)
431-
ms.ErrorIs(se, tt.wantError)
412+
jsonBody, err := json.Marshal(tt.body)
413+
must(err)
414+
b := io.NopCloser(bytes.NewReader(jsonBody))
415+
request, _ := http.NewRequest(http.MethodPost, "/api-key/rotate", b)
416+
request.Header.Set(HeaderAPIKey, tt.key.Key)
417+
request.Header.Set(HeaderAPISecret, tt.key.Secret)
418+
419+
ctxWithUser := context.WithValue(request.Context(), UserContextKey, tt.key)
420+
request = request.WithContext(ctxWithUser)
421+
422+
res := httptest.NewRecorder()
423+
Router(ms.app).ServeHTTP(res, request)
424+
ms.Equal(tt.wantStatus, res.Code, "incorrect http status, body: %s", res.Body.String())
425+
426+
if tt.wantError != "" {
427+
ms.Contains(res.Body.String(), tt.wantError)
432428
return
433429
}
434430

435-
ms.Equal(tt.wantStatus, res.Status, fmt.Sprintf("CreateApiKey response: %s", res.Body))
436-
437431
var response map[string]int
438-
ms.decodeBody(res.Body, &response)
432+
ms.decodeBody(res.Body.Bytes(), &response)
439433
ms.Equal(1, response["totpComplete"])
440434
ms.Equal(1, response["webauthnComplete"])
441435

auth.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ import (
77
"strings"
88
)
99

10+
const (
11+
HeaderAPIKey = "x-mfa-apikey"
12+
HeaderAPISecret = "x-mfa-apisecret"
13+
)
14+
1015
type User interface{}
1116

1217
// AuthenticateRequest checks the provided API key against the keys stored in the database. If the key is active and
1318
// valid, a Webauthn client and WebauthnUser are created and stored in the request context.
1419
func AuthenticateRequest(r *http.Request) (User, error) {
1520
// get key and secret from headers
16-
key := r.Header.Get("x-mfa-apikey")
17-
secret := r.Header.Get("x-mfa-apisecret")
21+
key := r.Header.Get(HeaderAPIKey)
22+
secret := r.Header.Get(HeaderAPISecret)
1823

1924
if key == "" || secret == "" {
2025
return nil, fmt.Errorf("x-mfa-apikey and x-mfa-apisecret are required")

openapi.yaml

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,8 @@ paths:
468468
operationId: rotateApiKey
469469
summary: Rotate API Key
470470
description: >
471-
All data in webauthn and totp tables that is encrypted by the old key will be re-encrypted using the new key.
471+
All data in webauthn and totp tables that is encrypted by the old key (identified by the request headers
472+
`x-mfa-apikey` and `x-mfa-apisecret`) will be re-encrypted using a new key identified in the request body.
472473
If the process does not run to completion, this endpoint can be called any number of times to continue the
473474
process. A status of 200 does not indicate that all keys were encrypted using the new key. Check the response
474475
data to determine if the rotation process is complete.
@@ -480,16 +481,6 @@ paths:
480481
schema:
481482
type: object
482483
properties:
483-
oldKeyId:
484-
type: string
485-
description: old API Key ID
486-
required: true
487-
example: 0123456789012345678901234567890123456789
488-
oldKeySecret:
489-
type: string
490-
description: old API Key secret
491-
required: true
492-
example: 0123456789012345678901234567890123456789012=
493484
newKeyId:
494485
type: string
495486
description: new API Key ID

webauthn_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
"fmt"
99
"io"
10+
"log"
1011
"net/http"
1112
"net/http/httptest"
1213
"strings"
@@ -744,6 +745,7 @@ func Test_GetPublicKeyAsBytes(t *testing.T) {
744745

745746
func Router(app *App) http.Handler {
746747
mux := &http.ServeMux{}
748+
mux.HandleFunc("POST /api-key/rotate", app.RotateApiKey)
747749
mux.HandleFunc(fmt.Sprintf("DELETE /webauthn/credential/{%s}", IDParam), app.DeleteCredential)
748750
// Ensure a request without an id gets handled properly
749751
mux.HandleFunc("DELETE /webauthn/credential/", app.DeleteCredential)
@@ -752,11 +754,13 @@ func Router(app *App) http.Handler {
752754
return testAuthnMiddleware(mux)
753755
}
754756

757+
// testAuthnMiddleware is a copy of the authenticationMiddleware function
755758
func testAuthnMiddleware(next http.Handler) http.Handler {
756759
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
757760
user, err := AuthenticateRequest(r)
758761
if err != nil {
759-
http.Error(w, fmt.Sprintf("unable to authenticate request: %s", err), http.StatusUnauthorized)
762+
log.Printf("unable to authenticate request: %s", err)
763+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
760764
return
761765
}
762766

@@ -841,8 +845,8 @@ func (ms *MfaSuite) Test_DeleteCredential() {
841845
ms.T().Run(tt.name, func(t *testing.T) {
842846
request, _ := http.NewRequest("DELETE", fmt.Sprintf("/webauthn/credential/%s", tt.credID), nil)
843847

844-
request.Header.Set("x-mfa-apikey", tt.user.ApiKeyValue)
845-
request.Header.Set("x-mfa-apisecret", tt.user.ApiKey.Secret)
848+
request.Header.Set(HeaderAPIKey, tt.user.ApiKeyValue)
849+
request.Header.Set(HeaderAPISecret, tt.user.ApiKey.Secret)
846850
request.Header.Set("x-mfa-RPDisplayName", "TestRPName")
847851
request.Header.Set("x-mfa-RPID", "111.11.11.11")
848852
request.Header.Set("x-mfa-UserUUID", tt.user.ID)

0 commit comments

Comments
 (0)