diff --git a/client.go b/client.go index 19c2be4..3a8b133 100644 --- a/client.go +++ b/client.go @@ -581,3 +581,91 @@ func nonce() []byte { } return nonce } + +func (c *Client) EndpointPreAuth(ctx context.Context) (*message.PreAuthData, error) { + dest, err := url.JoinPath(c.dnServer, message.PreAuthEndpoint) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", dest, nil) + if err != nil { + return nil, err + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + reqID := resp.Header.Get("X-Request-ID") + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, &APIError{e: fmt.Errorf("failed to read the response body: %s", err), ReqID: reqID} + } + + switch resp.StatusCode { + case http.StatusOK: + r := message.PreAuthResponse{} + if err = json.Unmarshal(respBody, &r); err != nil { + return nil, &APIError{e: fmt.Errorf("error decoding JSON response: %s\nbody: %s", err, respBody), ReqID: reqID} + } + + if r.Data.PollToken == "" || r.Data.LoginURL == "" { + return nil, &APIError{e: fmt.Errorf("missing pollToken or loginURL"), ReqID: reqID} + } + + return &r.Data, nil + default: + var errors struct { + Errors message.APIErrors + } + if err := json.Unmarshal(respBody, &errors); err != nil { + return nil, fmt.Errorf("bad status code '%d', body: %s", resp.StatusCode, respBody) + } + return nil, &APIError{e: errors.Errors.ToError(), ReqID: reqID} + } +} + +func (c *Client) EndpointAuthPoll(ctx context.Context, pollCode string) (*message.EndpointAuthPollData, error) { + pollURL, err := url.JoinPath(c.dnServer, message.EndpointAuthPoll) + if err != nil { + return nil, err + } + pollURL = fmt.Sprintf("%s?pollToken=%s", pollURL, url.QueryEscape(pollCode)) + + req, err := http.NewRequestWithContext(ctx, "GET", pollURL, nil) + if err != nil { + return nil, err + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + reqID := resp.Header.Get("X-Request-ID") + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, &APIError{e: fmt.Errorf("failed to read the response body: %s", err), ReqID: reqID} + } + + switch resp.StatusCode { + case http.StatusOK: + r := message.EndpointAuthPollResponse{} + if err = json.Unmarshal(respBody, &r); err != nil { + return nil, &APIError{e: fmt.Errorf("error decoding JSON response: %s\nbody: %s", err, respBody), ReqID: reqID} + } + return &r.Data, nil + default: + var errors struct { + Errors message.APIErrors + } + if err := json.Unmarshal(respBody, &errors); err != nil { + return nil, fmt.Errorf("bad status code '%d', body: %s", resp.StatusCode, respBody) + } + return nil, &APIError{e: errors.Errors.ToError(), ReqID: reqID} + } +} diff --git a/client_test.go b/client_test.go index 70daffe..b3faa67 100644 --- a/client_test.go +++ b/client_test.go @@ -241,7 +241,7 @@ func TestDoUpdate(t *testing.T) { assert.NotEmpty(t, pkey) // Invalid request signature should return a specific error - ts.ExpectRequest(message.CheckForUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { + ts.ExpectDNClientRequest(message.CheckForUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { return []byte("") }) @@ -265,7 +265,7 @@ func TestDoUpdate(t *testing.T) { require.Len(t, serverErrs, 1) // Invalid signature - ts.ExpectRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { + ts.ExpectDNClientRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { newConfigResponse := message.DoUpdateResponse{ Config: dnapitest.NebulaCfg(caPEM), Counter: 2, @@ -320,7 +320,7 @@ func TestDoUpdate(t *testing.T) { require.Nil(t, pkey) // Invalid counter - ts.ExpectRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { + ts.ExpectDNClientRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { newConfigResponse := message.DoUpdateResponse{ Config: dnapitest.NebulaCfg(caPEM), Counter: 0, @@ -379,7 +379,7 @@ func TestDoUpdate(t *testing.T) { hostIP := "192.168.100.1" // This time sign the response with the correct CA key. - ts.ExpectRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { + ts.ExpectDNClientRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { newConfigResponse := message.DoUpdateResponse{ Config: dnapitest.NebulaCfg(caPEM), Counter: 3, @@ -505,7 +505,7 @@ func TestDoUpdate_P256(t *testing.T) { assert.NotEmpty(t, pkey) // Invalid request signature should return a specific error - ts.ExpectRequest(message.CheckForUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { + ts.ExpectDNClientRequest(message.CheckForUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { return []byte("") }) @@ -528,7 +528,7 @@ func TestDoUpdate_P256(t *testing.T) { require.Len(t, serverErrs, 1) // Invalid signature - ts.ExpectRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { + ts.ExpectDNClientRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { newConfigResponse := message.DoUpdateResponse{ Config: dnapitest.NebulaCfg(caPEM), Counter: 2, @@ -574,7 +574,7 @@ func TestDoUpdate_P256(t *testing.T) { require.Nil(t, pkey) // Invalid counter - ts.ExpectRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { + ts.ExpectDNClientRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { newConfigResponse := message.DoUpdateResponse{ Config: dnapitest.NebulaCfg(caPEM), Counter: 0, @@ -618,7 +618,7 @@ func TestDoUpdate_P256(t *testing.T) { require.Nil(t, pkey) // This time sign the response with the correct CA key. - ts.ExpectRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { + ts.ExpectDNClientRequest(message.DoUpdate, http.StatusOK, func(r message.RequestWrapper) []byte { newConfigResponse := message.DoUpdateResponse{ Config: dnapitest.NebulaCfg(caPEM), Counter: 3, @@ -743,7 +743,7 @@ func TestCommandResponse(t *testing.T) { // This time sign the response with the correct CA key. responseToken := "abc123" res := map[string]any{"msg": "Hello, world!"} - ts.ExpectRequest(message.CommandResponse, http.StatusOK, func(r message.RequestWrapper) []byte { + ts.ExpectDNClientRequest(message.CommandResponse, http.StatusOK, func(r message.RequestWrapper) []byte { var val map[string]any err := json.Unmarshal(r.Value, &val) require.NoError(t, err) @@ -759,7 +759,7 @@ func TestCommandResponse(t *testing.T) { // Test error handling errorMsg := "sample error" - ts.ExpectRequest(message.CommandResponse, http.StatusBadRequest, func(r message.RequestWrapper) []byte { + ts.ExpectDNClientRequest(message.CommandResponse, http.StatusBadRequest, func(r message.RequestWrapper) []byte { return jsonMarshal(message.EnrollResponse{ Errors: message.APIErrors{{ Code: "ERR_INVALID_VALUE", @@ -954,3 +954,87 @@ func marshalCAPublicKey(curve cert.Curve, pubkey []byte) []byte { panic("unsupported curve") } } + +func TestGetOidcPollCode(t *testing.T) { + t.Parallel() + + useragent := "dnclientUnitTests/1.0.0 (not a real client)" + ts := dnapitest.NewServer(useragent) + client := NewClient(useragent, ts.URL) + // attempting to defer ts.Close() will trigger early due to parallel testing - use T.Cleanup instead + t.Cleanup(func() { ts.Close() }) + const expectedCode = "123456" + ts.ExpectAPIRequest(http.StatusOK, func(req any) []byte { + return jsonMarshal(message.PreAuthResponse{Data: message.PreAuthData{PollToken: expectedCode, LoginURL: "https://example.com"}}) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + resp, err := client.EndpointPreAuth(ctx) + require.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, expectedCode, resp.PollToken) + assert.Equal(t, "https://example.com", resp.LoginURL) + assert.Empty(t, ts.Errors()) + assert.Equal(t, 0, ts.RequestsRemaining()) + + //unhappy path + ts.ExpectAPIRequest(http.StatusBadGateway, func(req any) []byte { + return jsonMarshal(message.PreAuthResponse{Data: message.PreAuthData{PollToken: expectedCode, LoginURL: "https://example.com"}}) + }) + resp, err = client.EndpointPreAuth(ctx) + require.Error(t, err) + require.Nil(t, resp) + assert.Empty(t, ts.Errors()) + assert.Equal(t, 0, ts.RequestsRemaining()) +} + +func TestDoOidcPoll(t *testing.T) { + t.Parallel() + + useragent := "dnclientUnitTests/1.0.0 (not a real client)" + ts := dnapitest.NewServer(useragent) + client := NewClient(useragent, ts.URL) + // attempting to defer ts.Close() will trigger early due to parallel testing - use T.Cleanup instead + t.Cleanup(func() { ts.Close() }) + const expectedCode = "123456" + ts.ExpectAPIRequest(http.StatusOK, func(r any) []byte { + return jsonMarshal(message.EndpointAuthPollResponse{Data: message.EndpointAuthPollData{ + Status: message.EndpointAuthStarted, + EnrollmentCode: "", + }}) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + resp, err := client.EndpointAuthPoll(ctx, expectedCode) + require.NoError(t, err) + assert.Equal(t, resp.Status, message.EndpointAuthStarted) + assert.Equal(t, resp.EnrollmentCode, "") + assert.Empty(t, ts.Errors()) + assert.Equal(t, 0, ts.RequestsRemaining()) + + //unhappy path + ts.ExpectAPIRequest(http.StatusBadRequest, func(r any) []byte { + return nil + }) + resp, err = client.EndpointAuthPoll(ctx, "") //blank code should error! + require.Error(t, err) + assert.Nil(t, resp) + assert.Empty(t, ts.Errors()) + assert.Equal(t, 0, ts.RequestsRemaining()) + + //complete path + ts.ExpectAPIRequest(http.StatusOK, func(r any) []byte { + return jsonMarshal(message.EndpointAuthPollResponse{Data: message.EndpointAuthPollData{ + Status: message.EndpointAuthCompleted, + EnrollmentCode: "deadbeef", + }}) + }) + resp, err = client.EndpointAuthPoll(ctx, expectedCode) + require.NoError(t, err) + assert.Equal(t, resp.Status, message.EndpointAuthCompleted) + assert.Equal(t, resp.EnrollmentCode, "deadbeef") + assert.Empty(t, ts.Errors()) + assert.Equal(t, 0, ts.RequestsRemaining()) +} diff --git a/dnapitest/dnapitest.go b/dnapitest/dnapitest.go index 4d3e363..8076d1e 100644 --- a/dnapitest/dnapitest.go +++ b/dnapitest/dnapitest.go @@ -35,7 +35,7 @@ type Server struct { streamedBody []byte - expectedRequests []requestResponse + expectedRequests []Responder expectedEdPubkey ed25519.PublicKey expectedP256Pubkey *ecdsa.PublicKey @@ -67,6 +67,13 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) { s.handlerEnroll(w, r) case message.EndpointV1: s.handlerDNClient(w, r) + case message.PreAuthEndpoint: + expected := s.expectedRequests[0] + s.expectedRequests = s.expectedRequests[1:] + w.WriteHeader(expected.StatusCode()) + _, _ = w.Write(expected.Respond(nil)) + case message.EndpointAuthPoll: + s.handlerDoOidcPoll(w, r) default: s.errors = append(s.errors, fmt.Errorf("invalid request path %s", r.URL.Path)) http.NotFound(w, r) @@ -75,14 +82,9 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) { func (s *Server) handlerEnroll(w http.ResponseWriter, r *http.Request) { // Get the test case to validate - expected := s.expectedRequests[0] + expectedRaw := s.expectedRequests[0] s.expectedRequests = s.expectedRequests[1:] - if expected.dnclientAPI { - s.errors = append(s.errors, fmt.Errorf("unexpected enrollment request - expected dnclient API request")) - http.Error(w, "unexpected enrollment request", http.StatusInternalServerError) - return - } - res := expected.enrollRequestResponse + res := expectedRaw.(*enrollRequestResponse) // read and unmarshal body var req message.EnrollRequest @@ -113,7 +115,7 @@ func (s *Server) handlerEnroll(w http.ResponseWriter, r *http.Request) { s.curve = res.curve - w.Write(res.response(req)) + w.Write(res.Respond(req)) } func (s *Server) SetCurve(curve message.NetworkCurve) { @@ -152,16 +154,28 @@ func (s *Server) SetP256Pubkey(p256PubkeyPEM []byte) error { return nil } -func (s *Server) handlerDNClient(w http.ResponseWriter, r *http.Request) { +func (s *Server) handlerDoOidcPoll(w http.ResponseWriter, r *http.Request) { // Get the test case to validate - expected := s.expectedRequests[0] + res := s.expectedRequests[0] s.expectedRequests = s.expectedRequests[1:] - if !expected.dnclientAPI { - s.errors = append(s.errors, fmt.Errorf("unexpected dnclient API request - expected enrollment request")) - http.Error(w, "unexpected dnclient API request", http.StatusInternalServerError) + + token := r.URL.Query()["pollToken"] + if len(token) == 0 { + s.errors = append(s.errors, fmt.Errorf("missing pollToken")) + http.Error(w, "missing pollToken", http.StatusBadRequest) return } - res := expected.dncRequestResponse + + // return the associated response + w.WriteHeader(res.StatusCode()) + w.Write(res.Respond(nil)) +} + +func (s *Server) handlerDNClient(w http.ResponseWriter, r *http.Request) { + // Get the test case to validate + expected := s.expectedRequests[0] + s.expectedRequests = s.expectedRequests[1:] + res := expected.(*dncRequestResponse) jd := json.NewDecoder(r.Body) @@ -282,7 +296,7 @@ func (s *Server) handlerDNClient(w http.ResponseWriter, r *http.Request) { } - if expected.isStreamingRequest { + if res.isStreamingRequest { s.streamedBody, err = io.ReadAll(io.MultiReader(jd.Buffered(), r.Body)) if err != nil { s.errors = append(s.errors, fmt.Errorf("failed to read body: %w", err)) @@ -292,41 +306,40 @@ func (s *Server) handlerDNClient(w http.ResponseWriter, r *http.Request) { } // return the associated response - w.WriteHeader(res.statusCode) - w.Write(res.response(msg)) + w.WriteHeader(res.StatusCode()) + w.Write(res.Respond(msg)) } func (s *Server) ExpectEnrollment(code string, curve message.NetworkCurve, response func(req message.EnrollRequest) []byte) { - s.expectedRequests = append(s.expectedRequests, requestResponse{ - dnclientAPI: false, - enrollRequestResponse: enrollRequestResponse{ + s.expectedRequests = append(s.expectedRequests, + &enrollRequestResponse{ expectedCode: code, response: response, curve: curve, - }, + }) +} + +func (s *Server) ExpectAPIRequest(statusCode int, cb func(r any) []byte) { + s.expectedRequests = append(s.expectedRequests, &response{ + statusCode: statusCode, + cb: cb, }) } -func (s *Server) ExpectRequest(msgType string, statusCode int, response func(r message.RequestWrapper) []byte) { - s.expectedRequests = append(s.expectedRequests, requestResponse{ - dnclientAPI: true, - dncRequestResponse: dncRequestResponse{ - statusCode: statusCode, - expectedType: msgType, - response: response, - }, +func (s *Server) ExpectDNClientRequest(msgType string, statusCode int, response func(r message.RequestWrapper) []byte) { + s.expectedRequests = append(s.expectedRequests, &dncRequestResponse{ + statusCode: statusCode, + expectedType: msgType, + response: response, }) } func (s *Server) ExpectStreamingRequest(msgType string, statusCode int, response func(r message.RequestWrapper) []byte) { - s.expectedRequests = append(s.expectedRequests, requestResponse{ - dnclientAPI: true, + s.expectedRequests = append(s.expectedRequests, &dncRequestResponse{ isStreamingRequest: true, - dncRequestResponse: dncRequestResponse{ - statusCode: statusCode, - expectedType: msgType, - response: response, - }, + statusCode: statusCode, + expectedType: msgType, + response: response, }) } @@ -342,7 +355,7 @@ func (s *Server) RequestsRemaining() int { return len(s.expectedRequests) } -func (s *Server) ExpectedRequests() []requestResponse { +func (s *Server) ExpectedRequests() []Responder { return s.expectedRequests } @@ -353,7 +366,7 @@ func (s *Server) LastStreamedBody() []byte { func NewServer(expectedUserAgent string) *Server { s := &Server{ errors: []error{}, - expectedRequests: []requestResponse{}, + expectedRequests: []Responder{}, expectedUserAgent: expectedUserAgent, curve: message.NetworkCurve25519, // default for legacy tests } @@ -363,25 +376,53 @@ func NewServer(expectedUserAgent string) *Server { return s } -type requestResponse struct { - dnclientAPI bool - dncRequestResponse dncRequestResponse - enrollRequestResponse enrollRequestResponse - isStreamingRequest bool +type Responder interface { + StatusCode() int + Respond(r any) []byte +} + +type response struct { + statusCode int + cb func(r any) []byte +} + +func (r *response) StatusCode() int { + return r.statusCode +} + +func (r *response) Respond(x any) []byte { + return r.cb(x) } type enrollRequestResponse struct { expectedCode string curve message.NetworkCurve + response func(r message.EnrollRequest) []byte +} + +func (r *enrollRequestResponse) StatusCode() int { + return http.StatusOK //TODO? +} - response func(r message.EnrollRequest) []byte +func (r *enrollRequestResponse) Respond(x any) []byte { + y := x.(message.EnrollRequest) + return r.response(y) } type dncRequestResponse struct { - expectedType string + expectedType string + isStreamingRequest bool + statusCode int + response func(r message.RequestWrapper) []byte +} - statusCode int - response func(r message.RequestWrapper) []byte +func (r *dncRequestResponse) StatusCode() int { + return r.statusCode +} + +func (r *dncRequestResponse) Respond(x any) []byte { + y := x.(message.RequestWrapper) + return r.response(y) } func GetNonce(r message.RequestWrapper) []byte { diff --git a/go.mod b/go.mod index 7ad3471..7774303 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/DefinedNet/dnapi -go 1.22 +go 1.24.0 require ( github.com/sirupsen/logrus v1.9.2 github.com/slackhq/nebula v1.7.1 github.com/stretchr/testify v1.8.2 - golang.org/x/crypto v0.9.0 + golang.org/x/crypto v0.42.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -15,7 +15,7 @@ require ( github.com/google/go-cmp v0.5.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.36.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5099b5f..b83b7e6 100644 --- a/go.sum +++ b/go.sum @@ -31,13 +31,13 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= diff --git a/message/message.go b/message/message.go index 9fe0eb2..0324db6 100644 --- a/message/message.go +++ b/message/message.go @@ -218,3 +218,37 @@ func (nc *NetworkCurve) UnmarshalJSON(b []byte) error { return nil } + +const PreAuthEndpoint = "/v1/endpoint-auth/preauth" + +type PreAuthResponse struct { + // Only one of Data or Errors should be set in a response + Data PreAuthData `json:"data"` + Errors APIErrors `json:"errors"` +} + +type PreAuthData struct { + PollToken string `json:"pollToken"` + LoginURL string `json:"loginURL"` +} + +const EndpointAuthPoll = "/v1/endpoint-auth/poll" + +type EndpointAuthState string + +const ( + EndpointAuthWaiting EndpointAuthState = "WAITING" + EndpointAuthStarted EndpointAuthState = "STARTED" + EndpointAuthCompleted EndpointAuthState = "COMPLETED" +) + +type EndpointAuthPollResponse struct { + // Only one of Data or Errors should be set in a response + Data EndpointAuthPollData `json:"data"` + Errors APIErrors `json:"errors"` +} + +type EndpointAuthPollData struct { + Status EndpointAuthState `json:"state"` + EnrollmentCode string `json:"enrollmentCode"` +}