Skip to content

Commit 5c66fbc

Browse files
authored
Merge pull request #158 from sil-org/simplify-rotate
use the authenticated Key as the "old" key for the rotation endpoint
2 parents bddd35a + 45e52a9 commit 5c66fbc

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 (
@@ -458,22 +456,28 @@ func (a *App) CreateApiKey(w http.ResponseWriter, r *http.Request) {
458456
// any number of times to continue the process. A status of 200 does not indicate that all keys were encrypted using the
459457
// new key. Check the response data to determine if the rotation process is complete.
460458
func (a *App) RotateApiKey(w http.ResponseWriter, r *http.Request) {
461-
requestBody, err := parseRotateKeyRequestBody(r.Body)
459+
var requestBody map[string]string
460+
err := json.NewDecoder(r.Body).Decode(&requestBody)
462461
if err != nil {
463-
if strings.HasSuffix(err.Error(), "is required") {
464-
jsonResponse(w, err, http.StatusBadRequest)
465-
} else {
466-
log.Printf("invalid request in RotateApiKey: %s", err)
467-
jsonResponse(w, invalidRequest, http.StatusBadRequest)
468-
}
462+
log.Printf("invalid request in ActivateApiKey: %s", err)
463+
jsonResponse(w, invalidRequest, http.StatusBadRequest)
464+
return
465+
}
466+
467+
if requestBody[paramNewKeyId] == "" {
468+
jsonResponse(w, paramNewKeyId+" is required", http.StatusBadRequest)
469469
return
470470
}
471471

472-
oldKey := ApiKey{Key: requestBody[paramOldKeyId], Store: a.GetDB()}
473-
err = oldKey.loadAndCheck(requestBody[paramOldKeySecret])
472+
if requestBody[paramNewKeySecret] == "" {
473+
jsonResponse(w, paramNewKeySecret+" is required", http.StatusBadRequest)
474+
return
475+
}
476+
477+
oldKey, err := getAPIKey(r)
474478
if err != nil {
475-
log.Printf("old key is not valid: %s", err)
476-
jsonResponse(w, apiKeyNotFound, http.StatusNotFound)
479+
log.Printf("Rotate API key error: %v", err)
480+
jsonResponse(w, internalServerError, http.StatusInternalServerError)
477481
return
478482
}
479483

@@ -509,22 +513,6 @@ func (a *App) RotateApiKey(w http.ResponseWriter, r *http.Request) {
509513
jsonResponse(w, responseBody, http.StatusOK)
510514
}
511515

512-
func parseRotateKeyRequestBody(body io.Reader) (map[string]string, error) {
513-
var requestBody map[string]string
514-
err := json.NewDecoder(body).Decode(&requestBody)
515-
if err != nil {
516-
return nil, fmt.Errorf("invalid request in RotateApiKey: %w", err)
517-
}
518-
519-
fields := []string{paramNewKeyId, paramNewKeySecret, paramOldKeyId, paramOldKeySecret}
520-
for _, field := range fields {
521-
if _, ok := requestBody[field]; !ok {
522-
return nil, fmt.Errorf("%s is required", field)
523-
}
524-
}
525-
return requestBody, nil
526-
}
527-
528516
func (k *ApiKey) loadAndCheck(secret string) error {
529517
err := k.Load()
530518
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"
@@ -372,78 +374,70 @@ func (ms *MfaSuite) TestAppRotateApiKey() {
372374
tests := []struct {
373375
name string
374376
body any
377+
key ApiKey
375378
wantStatus int
376-
wantError error
379+
wantError string
377380
}{
378381
{
379-
name: "missing oldKeyId",
380-
body: map[string]interface{}{
381-
paramNewKeyId: newKey.Key,
382-
paramNewKeySecret: newKey.Secret,
383-
paramOldKeySecret: key.Secret,
384-
},
385-
wantStatus: http.StatusBadRequest,
386-
wantError: errors.New("oldKeyId is required"),
387-
},
388-
{
389-
name: "missing oldKeySecret",
382+
name: "missing key",
390383
body: map[string]interface{}{
391384
paramNewKeyId: newKey.Key,
392385
paramNewKeySecret: newKey.Secret,
393-
paramOldKeyId: key.Key,
394386
},
395-
wantStatus: http.StatusBadRequest,
396-
wantError: errors.New("oldKeySecret is required"),
387+
wantStatus: http.StatusUnauthorized,
388+
wantError: "Unauthorized",
397389
},
398390
{
399391
name: "missing newKeyId",
400392
body: map[string]interface{}{
401393
paramNewKeySecret: newKey.Secret,
402-
paramOldKeyId: key.Key,
403-
paramOldKeySecret: key.Secret,
404394
},
395+
key: key,
405396
wantStatus: http.StatusBadRequest,
406-
wantError: errors.New("newKeyId is required"),
397+
wantError: "newKeyId is required",
407398
},
408399
{
409400
name: "missing newKeySecret",
410401
body: map[string]interface{}{
411-
paramNewKeyId: newKey.Key,
412-
paramOldKeyId: key.Key,
413-
paramOldKeySecret: key.Secret,
402+
paramNewKeyId: newKey.Key,
414403
},
404+
key: key,
415405
wantStatus: http.StatusBadRequest,
416-
wantError: errors.New("newKeySecret is required"),
406+
wantError: "newKeySecret is required",
417407
},
418408
{
419409
name: "good",
420410
body: map[string]interface{}{
421411
paramNewKeyId: newKey.Key,
422412
paramNewKeySecret: newKey.Secret,
423-
paramOldKeyId: user.ApiKey.Key,
424-
paramOldKeySecret: key.Secret,
425413
},
414+
key: key,
426415
wantStatus: http.StatusOK,
427416
},
428417
}
429418
for _, tt := range tests {
430419
ms.Run(tt.name, func() {
431-
res := &lambdaResponseWriter{Headers: http.Header{}}
432-
req := requestWithUser(tt.body, key)
433-
ms.app.RotateApiKey(res, req)
434-
435-
if tt.wantError != nil {
436-
ms.Equal(tt.wantStatus, res.Status, fmt.Sprintf("CreateApiKey response: %s", res.Body))
437-
var se simpleError
438-
ms.decodeBody(res.Body, &se)
439-
ms.ErrorIs(se, tt.wantError)
420+
jsonBody, err := json.Marshal(tt.body)
421+
must(err)
422+
b := io.NopCloser(bytes.NewReader(jsonBody))
423+
request, _ := http.NewRequest(http.MethodPost, "/api-key/rotate", b)
424+
request.Header.Set(HeaderAPIKey, tt.key.Key)
425+
request.Header.Set(HeaderAPISecret, tt.key.Secret)
426+
427+
ctxWithUser := context.WithValue(request.Context(), UserContextKey, tt.key)
428+
request = request.WithContext(ctxWithUser)
429+
430+
res := httptest.NewRecorder()
431+
Router(ms.app).ServeHTTP(res, request)
432+
ms.Equal(tt.wantStatus, res.Code, "incorrect http status, body: %s", res.Body.String())
433+
434+
if tt.wantError != "" {
435+
ms.Contains(res.Body.String(), tt.wantError)
440436
return
441437
}
442438

443-
ms.Equal(tt.wantStatus, res.Status, fmt.Sprintf("CreateApiKey response: %s", res.Body))
444-
445439
var response map[string]int
446-
ms.decodeBody(res.Body, &response)
440+
ms.decodeBody(res.Body.Bytes(), &response)
447441
ms.Equal(1, response["totpComplete"])
448442
ms.Equal(1, response["webauthnComplete"])
449443

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
@@ -488,7 +488,8 @@ paths:
488488
operationId: rotateApiKey
489489
summary: Rotate API Key
490490
description: >
491-
All data in webauthn and totp tables that is encrypted by the old key will be re-encrypted using the new key.
491+
All data in webauthn and totp tables that is encrypted by the old key (identified by the request headers
492+
`x-mfa-apikey` and `x-mfa-apisecret`) will be re-encrypted using a new key identified in the request body.
492493
If the process does not run to completion, this endpoint can be called any number of times to continue the
493494
process. A status of 200 does not indicate that all keys were encrypted using the new key. Check the response
494495
data to determine if the rotation process is complete.
@@ -500,16 +501,6 @@ paths:
500501
schema:
501502
type: object
502503
properties:
503-
oldKeyId:
504-
type: string
505-
description: old API Key ID
506-
required: true
507-
example: 0123456789012345678901234567890123456789
508-
oldKeySecret:
509-
type: string
510-
description: old API Key secret
511-
required: true
512-
example: 0123456789012345678901234567890123456789012=
513504
newKeyId:
514505
type: string
515506
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)