diff --git a/cmd/nanomdm/main.go b/cmd/nanomdm/main.go
index 01fecdc..60fb3ea 100644
--- a/cmd/nanomdm/main.go
+++ b/cmd/nanomdm/main.go
@@ -71,6 +71,7 @@ func main() {
flRetro = flag.Bool("retro", false, "Allow retroactive certificate-authorization association")
flDMURLPfx = flag.String("dm", "", "URL to send Declarative Management requests to")
flAuthProxy = flag.String("auth-proxy-url", "", "Reverse proxy URL target for MDM-authenticated HTTP requests")
+ flUAZLChal = flag.Bool("ua-zl-dc", false, "reply with zero-length DigestChallenge for UserAuthenticate")
)
flag.Parse()
@@ -110,7 +111,10 @@ func main() {
}
// create 'core' MDM service
- nanoOpts := []nanomdm.Option{nanomdm.WithLogger(logger.With("service", "nanomdm"))}
+ nanoOpts := []nanomdm.Option{
+ nanomdm.WithLogger(logger.With("service", "nanomdm")),
+ nanomdm.WithUserAuthenticate(nanomdm.NewUAService(mdmStorage, *flUAZLChal)),
+ }
if *flDMURLPfx != "" {
logger.Debug("msg", "declarative management setup", "url", *flDMURLPfx)
dm, err := nanomdm.NewDeclarativeManagementHTTPCaller(*flDMURLPfx, http.DefaultClient)
diff --git a/docs/operations-guide.md b/docs/operations-guide.md
index 95f5f1c..258c281 100644
--- a/docs/operations-guide.md
+++ b/docs/operations-guide.md
@@ -169,6 +169,14 @@ NanoMDM supports a MicroMDM-compatible [webhook callback](https://github.com/mic
Enables the authentication proxy and reverse proxies HTTP requests from the server's `/authproxy/` endpoint to this URL if the client provides the device's enrollment authentication. See below for more information.
+### -ua-zl-dc
+
+* reply with zero-length DigestChallenge for UserAuthenticate
+
+By default NanoMDM will respond to a `UserAuthenticate` message with an HTTP 410. This effectively declines management of that the user channel for that MDM user. Enabling this option turns on the "zero-length" Digest Challenge mode where NanoMDM replies with an empty Digest Challenge to enable management each time a client enrolls.
+
+Note that the `UserAuthenticate` message is only for "directory" MDM users and not the "primary" MDM user enrollment. See also [Apple's discussion of UserAthenticate](https://developer.apple.com/documentation/devicemanagement/userauthenticate#discussion) for more information.
+
## HTTP endpoints & APIs
### MDM
diff --git a/service/nanomdm/service.go b/service/nanomdm/service.go
index b6643d8..2925ef0 100644
--- a/service/nanomdm/service.go
+++ b/service/nanomdm/service.go
@@ -4,7 +4,6 @@ package nanomdm
import (
"errors"
"fmt"
- "net/http"
"github.com/micromdm/nanomdm/log"
"github.com/micromdm/nanomdm/log/ctxlog"
@@ -19,16 +18,11 @@ type Service struct {
normalizer func(e *mdm.Enrollment) *mdm.EnrollID
store storage.ServiceStore
- // By default the UserAuthenticate message will be rejected (410
- // response). If this is set true then a static zero-length
- // digest challenge will be supplied to the first UserAuthenticate
- // check-in message. See the Discussion section of
- // https://developer.apple.com/documentation/devicemanagement/userauthenticate
- sendEmptyDigestChallenge bool
- storeRejectedUserAuth bool
-
// Declarative Management
dm service.DeclarativeManagement
+
+ // UserAuthenticate processor
+ ua service.UserAuthenticate
}
// normalize generates enrollment IDs that are used by other
@@ -72,6 +66,13 @@ func WithDeclarativeManagement(dm service.DeclarativeManagement) Option {
}
}
+// WithUserAuthenticate configures a UserAuthenticate check-in message handler.
+func WithUserAuthenticate(ua service.UserAuthenticate) Option {
+ return func(s *Service) {
+ s.ua = ua
+ }
+}
+
// New returns a new NanoMDM main service.
func New(store storage.ServiceStore, opts ...Option) *Service {
nanomdm := &Service{
@@ -144,45 +145,15 @@ func (s *Service) CheckOut(r *mdm.Request, message *mdm.CheckOut) error {
return s.store.Disable(r)
}
-const emptyDigestChallenge = `
-
-
-
- DigestChallenge
-
-
-`
-
-var emptyDigestChallengeBytes = []byte(emptyDigestChallenge)
-
+// UserAuthenticate Check-in message implementation
func (s *Service) UserAuthenticate(r *mdm.Request, message *mdm.UserAuthenticate) ([]byte, error) {
if err := s.setupRequest(r, &message.Enrollment); err != nil {
return nil, err
}
- logger := ctxlog.Logger(r.Context, s.logger)
- if s.sendEmptyDigestChallenge || s.storeRejectedUserAuth {
- if err := s.store.StoreUserAuthenticate(r, message); err != nil {
- return nil, err
- }
- }
- // if the DigestResponse is empty then this is the first (of two)
- // UserAuthenticate messages depending on our response
- if message.DigestResponse == "" {
- if s.sendEmptyDigestChallenge {
- logger.Info(
- "msg", "sending empty DigestChallenge response to UserAuthenticate",
- )
- return emptyDigestChallengeBytes, nil
- }
- return nil, service.NewHTTPStatusError(
- http.StatusGone,
- fmt.Errorf("declining management of user: %s", r.ID),
- )
+ if s.ua == nil {
+ return nil, errors.New("no UserAuthenticate handler")
}
- logger.Debug(
- "msg", "sending empty response to second UserAuthenticate",
- )
- return nil, nil
+ return s.ua.UserAuthenticate(r, message)
}
func (s *Service) SetBootstrapToken(r *mdm.Request, message *mdm.SetBootstrapToken) error {
diff --git a/service/nanomdm/ua.go b/service/nanomdm/ua.go
new file mode 100644
index 0000000..83a8225
--- /dev/null
+++ b/service/nanomdm/ua.go
@@ -0,0 +1,78 @@
+package nanomdm
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/micromdm/nanomdm/log"
+ "github.com/micromdm/nanomdm/log/ctxlog"
+ "github.com/micromdm/nanomdm/mdm"
+ "github.com/micromdm/nanomdm/service"
+ "github.com/micromdm/nanomdm/storage"
+)
+
+// UAService is a basic UserAuthenticate service that optionally implements
+// the "zero-length" UserAuthenticate protocol.
+// See https://developer.apple.com/documentation/devicemanagement/userauthenticate
+type UAService struct {
+ logger log.Logger
+ store storage.UserAuthenticateStore
+
+ // By default the UserAuthenticate message will be rejected (410
+ // response). If this is set true then a static zero-length
+ // digest challenge will be supplied to the first UserAuthenticate
+ // check-in message. See the Discussion section of
+ // https://developer.apple.com/documentation/devicemanagement/userauthenticate
+ sendEmptyDigestChallenge bool
+ storeRejectedUserAuth bool
+}
+
+// NewUAService creates a new UserAuthenticate check-in message handler.
+func NewUAService(store storage.UserAuthenticateStore, sendEmptyDigestChallenge bool) *UAService {
+ return &UAService{
+ logger: log.NopLogger,
+ store: store,
+ sendEmptyDigestChallenge: sendEmptyDigestChallenge,
+ }
+}
+
+const emptyDigestChallenge = `
+
+
+
+ DigestChallenge
+
+
+`
+
+var emptyDigestChallengeBytes = []byte(emptyDigestChallenge)
+
+// UserAuthenticate will decline management of a user unless configured
+// for the empty digest 2-step UserAuthenticate protocol.
+// It implements the NanoMDM service method for UserAuthenticate check-in messages.
+func (s *UAService) UserAuthenticate(r *mdm.Request, message *mdm.UserAuthenticate) ([]byte, error) {
+ logger := ctxlog.Logger(r.Context, s.logger)
+ if s.sendEmptyDigestChallenge || s.storeRejectedUserAuth {
+ if err := s.store.StoreUserAuthenticate(r, message); err != nil {
+ return nil, err
+ }
+ }
+ // if the DigestResponse is empty then this is the first (of two)
+ // UserAuthenticate messages depending on our response
+ if message.DigestResponse == "" {
+ if s.sendEmptyDigestChallenge {
+ logger.Info(
+ "msg", "sending empty DigestChallenge response to UserAuthenticate",
+ )
+ return emptyDigestChallengeBytes, nil
+ }
+ return nil, service.NewHTTPStatusError(
+ http.StatusGone,
+ fmt.Errorf("declining management of user: %s", r.ID),
+ )
+ }
+ logger.Debug(
+ "msg", "sending empty response to second UserAuthenticate",
+ )
+ return nil, nil
+}
diff --git a/service/nanomdm/ua_test.go b/service/nanomdm/ua_test.go
new file mode 100644
index 0000000..f4fc317
--- /dev/null
+++ b/service/nanomdm/ua_test.go
@@ -0,0 +1,62 @@
+package nanomdm
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+
+ "github.com/micromdm/nanomdm/mdm"
+ "github.com/micromdm/nanomdm/service"
+)
+
+type fauxStore struct {
+ ua *mdm.UserAuthenticate
+}
+
+func (f *fauxStore) StoreUserAuthenticate(_ *mdm.Request, msg *mdm.UserAuthenticate) error {
+ f.ua = msg
+ return nil
+}
+
+func newMDMReq() *mdm.Request {
+ return &mdm.Request{EnrollID: &mdm.EnrollID{ID: ""}}
+}
+
+func TestUAServiceReject(t *testing.T) {
+ store := &fauxStore{}
+ s := NewUAService(store, false)
+ _, err := s.UserAuthenticate(newMDMReq(), &mdm.UserAuthenticate{})
+ var httpErr *service.HTTPStatusError
+ if !errors.As(err, &httpErr) {
+ // should be returning a HTTPStatusError (to deny management)
+ t.Fatalf("no error or incorrect error type")
+ }
+ if httpErr.Status != 410 {
+ // if we've kept the "send-empty" false this needs to return a 410
+ // i.e. decline management of the user.
+ t.Error("status not 410")
+ }
+}
+
+func TestUAService(t *testing.T) {
+ store := &fauxStore{}
+ s := NewUAService(store, true)
+ ret, err := s.UserAuthenticate(newMDMReq(), &mdm.UserAuthenticate{})
+ if err != nil {
+ // should be no error
+ t.Fatal(err)
+ }
+ if !bytes.Equal(ret, emptyDigestChallengeBytes) {
+ t.Error("response bytes not equal")
+ }
+ // second request with DigestResponse
+ ret, err = s.UserAuthenticate(newMDMReq(), &mdm.UserAuthenticate{DigestResponse: "test"})
+ if err != nil {
+ // should be no error
+ t.Fatal(err)
+ }
+ if ret != nil {
+ t.Error("response bytes not empty")
+ }
+
+}
diff --git a/service/service.go b/service/service.go
index 8c57ef9..41f7225 100644
--- a/service/service.go
+++ b/service/service.go
@@ -11,6 +11,11 @@ type DeclarativeManagement interface {
DeclarativeManagement(*mdm.Request, *mdm.DeclarativeManagement) ([]byte, error)
}
+// UserAuthenticate is an interface for processing the UserAuthenticate MDM check-in message.
+type UserAuthenticate interface {
+ UserAuthenticate(*mdm.Request, *mdm.UserAuthenticate) ([]byte, error)
+}
+
// Checkin represents the various check-in requests.
// See https://developer.apple.com/documentation/devicemanagement/check-in
type Checkin interface {
@@ -19,7 +24,7 @@ type Checkin interface {
CheckOut(*mdm.Request, *mdm.CheckOut) error
SetBootstrapToken(*mdm.Request, *mdm.SetBootstrapToken) error
GetBootstrapToken(*mdm.Request, *mdm.GetBootstrapToken) (*mdm.BootstrapToken, error)
- UserAuthenticate(*mdm.Request, *mdm.UserAuthenticate) ([]byte, error)
+ UserAuthenticate
DeclarativeManagement
}
diff --git a/storage/storage.go b/storage/storage.go
index d813cbc..d33293c 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -9,12 +9,16 @@ import (
"github.com/micromdm/nanomdm/mdm"
)
+type UserAuthenticateStore interface {
+ StoreUserAuthenticate(r *mdm.Request, msg *mdm.UserAuthenticate) error
+}
+
// CheckinStore stores MDM check-in data.
type CheckinStore interface {
StoreAuthenticate(r *mdm.Request, msg *mdm.Authenticate) error
StoreTokenUpdate(r *mdm.Request, msg *mdm.TokenUpdate) error
- StoreUserAuthenticate(r *mdm.Request, msg *mdm.UserAuthenticate) error
Disable(r *mdm.Request) error
+ UserAuthenticateStore
}
// CommandAndReportResultsStore stores and retrieves MDM command queue data.