Skip to content

Commit 7df0fd4

Browse files
authored
Add "Reauthenticate" /v1/dnclient command (#28)
* Fix comment * Add reauthenticate dnclient message * Fix the response unwrapping
1 parent 608413e commit 7df0fd4

4 files changed

Lines changed: 158 additions & 1 deletion

File tree

client.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,43 @@ func (c *Client) StreamCommandResponse(ctx context.Context, creds keys.Credentia
409409
return c.streamingPostDNClient(ctx, message.CommandResponse, value, creds.HostID, creds.Counter, creds.PrivateKey)
410410
}
411411

412+
func (c *Client) Reauthenticate(ctx context.Context, creds keys.Credentials) (*message.ReauthenticateResponse, error) {
413+
value, err := json.Marshal(message.ReauthenticateRequest{})
414+
if err != nil {
415+
return nil, fmt.Errorf("failed to marshal DNClient message: %s", err)
416+
}
417+
418+
resp, err := c.postDNClient(ctx, message.Reauthenticate, value, creds.HostID, creds.Counter, creds.PrivateKey)
419+
if err != nil {
420+
return nil, err
421+
}
422+
423+
resultWrapper := message.SignedResponseWrapper{}
424+
err = json.Unmarshal(resp, &resultWrapper)
425+
if err != nil {
426+
return nil, fmt.Errorf("failed to unmarshal signed response wrapper: %s", err)
427+
}
428+
429+
// Verify the signature
430+
valid := false
431+
for _, caPubkey := range creds.TrustedKeys {
432+
if caPubkey.Verify(resultWrapper.Data.Message, resultWrapper.Data.Signature) {
433+
valid = true
434+
break
435+
}
436+
}
437+
if !valid {
438+
return nil, fmt.Errorf("failed to verify signed API result")
439+
}
440+
441+
var response message.ReauthenticateResponse
442+
if err := json.Unmarshal(resultWrapper.Data.Message, &response); err != nil {
443+
return nil, fmt.Errorf("failed to unmarshal DNClient response: %s", err)
444+
}
445+
446+
return &response, nil
447+
}
448+
412449
// streamingPostDNClient wraps and signs the given dnclientRequestWrapper message, and makes a streaming API call.
413450
// On success, it returns a StreamController to interact with the request. On error, the error is returned.
414451
func (c *Client) streamingPostDNClient(ctx context.Context, reqType string, value []byte, hostID string, counter uint, privkey keys.PrivateKey) (*StreamController, error) {

client_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,106 @@ func TestStreamCommandResponse(t *testing.T) {
898898
assert.Equal(t, 0, ts.RequestsRemaining(), ts.ExpectedRequests())
899899
}
900900

901+
func TestReauthenticate(t *testing.T) {
902+
t.Parallel()
903+
904+
useragent := "testClient"
905+
ts := dnapitest.NewServer(useragent)
906+
t.Cleanup(func() { ts.Close() })
907+
908+
ca, caPrivkey := dnapitest.NebulaCACert()
909+
caPEM, err := ca.MarshalToPEM()
910+
require.NoError(t, err)
911+
912+
c := NewClient(useragent, ts.URL)
913+
914+
code := "foobar"
915+
ts.ExpectEnrollment(code, message.NetworkCurve25519, func(req message.EnrollRequest) []byte {
916+
cfg, err := yaml.Marshal(m{
917+
// we need to send this or we'll get an error from the api client
918+
"pki": m{"ca": string(caPEM)},
919+
// here we reflect values back to the client for test purposes
920+
"test": m{"code": req.Code, "dhPubkey": req.NebulaPubkeyX25519},
921+
})
922+
if err != nil {
923+
return jsonMarshal(message.EnrollResponse{
924+
Errors: message.APIErrors{{
925+
Code: "ERR_FAILED_TO_MARSHAL_YAML",
926+
Message: "failed to marshal test response config",
927+
}},
928+
})
929+
}
930+
931+
return jsonMarshal(message.EnrollResponse{
932+
Data: message.EnrollResponseData{
933+
HostID: "foobar",
934+
Counter: 1,
935+
Config: cfg,
936+
TrustedKeys: marshalCAPublicKey(ca.Details.Curve, ca.Details.PublicKey),
937+
Organization: message.HostOrgMetadata{
938+
ID: "foobaz",
939+
Name: "foobar's foo org",
940+
},
941+
Network: message.HostNetworkMetadata{
942+
ID: "qux",
943+
Name: "the best network",
944+
Curve: message.NetworkCurve25519,
945+
CIDR: "192.168.100.0/24",
946+
},
947+
Host: message.HostHostMetadata{
948+
ID: "quux",
949+
Name: "foo host",
950+
IPAddress: "192.168.100.2",
951+
},
952+
},
953+
})
954+
})
955+
956+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
957+
defer cancel()
958+
config, pkey, creds, _, err := c.Enroll(ctx, testutil.NewTestLogger(), "foobar")
959+
require.NoError(t, err)
960+
961+
// make sure all credential values were set
962+
assert.NotEmpty(t, creds.HostID)
963+
assert.NotEmpty(t, creds.PrivateKey)
964+
assert.NotEmpty(t, creds.TrustedKeys)
965+
assert.NotEmpty(t, creds.Counter)
966+
967+
// make sure we got a config back
968+
assert.NotEmpty(t, config)
969+
assert.NotEmpty(t, pkey)
970+
971+
// This time sign the response with the correct CA key.
972+
ts.ExpectDNClientRequest(message.Reauthenticate, http.StatusOK, func(r message.RequestWrapper) []byte {
973+
newConfigResponse := message.ReauthenticateResponse{
974+
LoginURL: "https://auth.example.com/login?authcode=123",
975+
}
976+
rawRes := jsonMarshal(newConfigResponse)
977+
978+
return jsonMarshal(message.SignedResponseWrapper{
979+
Data: message.SignedResponse{
980+
Version: 1,
981+
Message: rawRes,
982+
Signature: ed25519.Sign(caPrivkey, rawRes),
983+
},
984+
})
985+
})
986+
987+
ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
988+
defer cancel()
989+
resp, err := c.Reauthenticate(ctx, *creds)
990+
require.NoError(t, err)
991+
assert.Empty(t, ts.Errors())
992+
assert.Equal(t, 0, ts.RequestsRemaining())
993+
994+
// make sure we got a login URL back
995+
assert.NotEmpty(t, resp)
996+
assert.NotEmpty(t, resp.LoginURL)
997+
assert.Equal(t, "https://auth.example.com/login?authcode=123", resp.LoginURL)
998+
999+
}
1000+
9011001
func jsonMarshal(v interface{}) []byte {
9021002
b, err := json.Marshal(v)
9031003
if err != nil {

dnapitest/dnapitest.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,15 @@ func (s *Server) handlerDNClient(w http.ResponseWriter, r *http.Request) {
294294
return
295295
}
296296

297+
case message.Reauthenticate:
298+
var reauth message.ReauthenticateRequest
299+
err = json.Unmarshal(msg.Value, &reauth)
300+
if err != nil {
301+
s.errors = append(s.errors, fmt.Errorf("failed to unmarshal ReauthenticateRequest: %w", err))
302+
http.Error(w, "failed to unmarshal ReauthenticateRequest", http.StatusInternalServerError)
303+
return
304+
}
305+
297306
}
298307

299308
if res.isStreamingRequest {

message/message.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313
DoUpdate = "DoUpdate"
1414
LongPollWait = "LongPollWait"
1515
CommandResponse = "CommandResponse"
16+
Reauthenticate = "Reauthenticate"
1617
)
1718

1819
// EndpointV1 is the version 1 DNClient API endpoint.
@@ -108,7 +109,7 @@ type CommandResponseRequest struct {
108109
Response any `json:"response"`
109110
}
110111

111-
// DNClientCommandResponseResponse is the response message associated with a CommandResponse call.
112+
// CommandResponseResponse is the response message associated with a CommandResponse call.
112113
type CommandResponseResponse struct{}
113114

114115
type ClientInfo struct {
@@ -118,6 +119,16 @@ type ClientInfo struct {
118119
Architecture string `json:"architecture"`
119120
}
120121

122+
// ReauthenticateRequest is the request sent for a Reauthenticate request.
123+
type ReauthenticateRequest struct {
124+
// Add fields as needed
125+
}
126+
127+
// ReauthenticateResponse is the response message associated with a Reauthenticate request.
128+
type ReauthenticateResponse struct {
129+
LoginURL string `json:"loginURL"`
130+
}
131+
121132
// EnrollEndpoint is the REST enrollment endpoint.
122133
const EnrollEndpoint = "/v2/enroll"
123134

0 commit comments

Comments
 (0)