Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support generating MAID Access Management JWTs #24

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/depserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
endpointConfig = "/v1/config/"
endpointTokenPKI = "/v1/tokenpki/"
endpointAssigner = "/v1/assigner/"
endpointMAIDJWT = "/v1/maidjwt/"
endpointProxy = "/proxy/"
)

Expand Down Expand Up @@ -91,6 +92,11 @@ func main() {
assignerMux.Handle("PUT", api.StoreAssignerProfileHandler(storage, logger.With("handler", "store-assigner-profile")))
handleStrippedAPI(assignerMux, endpointAssigner)

handleStrippedAPI(
api.MAIDJWTHandler(storage, logger.With("handler", "get-maid-jwt")),
endpointMAIDJWT,
)

p := proxy.New(
client.NewTransport(http.DefaultTransport, http.DefaultClient, storage, nil),
storage,
Expand Down
21 changes: 21 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,27 @@ paths:
$ref: '#/components/responses/JSONAPIError'
parameters:
- $ref: '#/components/parameters/depName'
/v1/maidjwt/{name}:
get:
description: Generate Managed Apple ID Managed Access JWT. This JWT is for use in response to a device's MDM `GetToken` Check-in message with a `TokenServiceType` of `com.apple.maid`. Note this endpoint queries the DEP Account Details endpoint to retrieve the Server UUID.
security:
- basicAuth: []
responses:
'200':
description: JWT which includes a claim containing the DEP server UUID and is signed by the private key of the DEP OAuth tokens.
content:
application/jwt:
schema:
type: string
example: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2ODY5MzE5NzIsImlzcyI6IjdERDczOUEyQTQ3MjRBMzI4MkFBODY0RUJBRDMwRkZGIiwianRpIjoiYmJiOGFkYWYtMDRiZS00NWNiLWFlMjgtNWI2NTc4NTdkZDg4Iiwic2VydmljZV90eXBlIjoiY29tLmFwcGxlLm1haWQifQ.IhPEK_jgJe3hxSyzBNYxGyqgeqmiT24MFJa5z6GQa-0vJ1FyEah8uG2ui9bfFKBM7HfbY122pKjEBbsv-oYMiRm9kOWvLb-GzDIb0WCx2c12al5OgIyR6c0cGINVALIDhm7vchMu-MrRS6TYp79FcXUcxHe8i2JxPlKJA9I4De4fCHZb4pgbMVpXbAklglCl7REwnP62BGEJUjSyqYNJTHTsgTFkEmLUgBulw99Yz1tXcJEgu5vnKtEWINVALIDcEyK9e6ek4GhfkzeEHd6WlML0wvZiiFG_8XUI4X3a4GfaAXHwTWHbOxyOGbD5GjhE1tPkT9tt5u6w
'401':
$ref: '#/components/responses/UnauthorizedError'
'400':
$ref: '#/components/responses/BadRequest'
'500':
$ref: '#/components/responses/JSONAPIError'
parameters:
- $ref: '#/components/parameters/depName'
components:
parameters:
depName:
Expand Down
17 changes: 17 additions & 0 deletions docs/operations-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ The `/v1/assigner/{name}` endpoints deal with storing and retrieving the assigne

The `/v1/config/{name}` endpoints deal with storing and retrieving configuration for a given DEP name. At this time the only configuration available is the base URL of the DEP name. This is really only useful when talking to the DEP simulator `depsim` or perhaps directing DEP server requests through another reverse proxy.

#### MAID JWT

* Endpoint: `GET /v1/maidjwt/{name}`

The `/v1/maidjwt/{name}` endpoint generates a JWT for Managed Apple ID Access Management. The responses is intended for a device's MDM `GetToken` Check-in message with a `TokenServiceType` of `com.apple.maid`. Note this endpoint queries the DEP Account Details endpoint to retrieve the Server UUID so be aware of the frequency it is called.

### Reverse proxy

In addition to individually handling some of various Apple DEP API endpoints in its `godep` library NanoDEP provides a transparently-authenticating HTTP reverse proxy to the Apple DEP servers. This allows us to simply provide `depserver` with the Apple DEP endpoint, the NanoDEP "DEP name" and the API key, and we can talk to any of the Apple DEP endpoint APIs (including the Roster, Class, and People Management). `depserver` will authenticate to the Apple DEP server and keep track of session management transparently behind the scenes. To be clear: this means you do not have to call to the `/session` endpoint to authenticate nor to manage and update the session tokens with each request. NanoDEP does this for you.
Expand Down Expand Up @@ -338,6 +344,17 @@ $ ./dep-remove-profile.sh 07AAD449616F566C12
}
```

#### cfg-maidjwt.sh

For the DEP "MDM server" in the environment variable $DEP_NAME (see above) this script queries the DEP API and generates a JWT. This is intended as a response to a device's MDM `GetToken` Check-in message with a `TokenServiceType` of `com.apple.maid`

##### Example usage

```bash
$ ./tools/cfg-maidjwt.sh
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2ODY5MzE5NzIsImlzcyI6IjdERDczOUEyQTQ3MjRBMzI4MkFBODY0RUJBRDMwRkZGIiwianRpIjoiYmJiOGFkYWYtMDRiZS00NWNiLWFlMjgtNWI2NTc4NTdkZDg4Iiwic2VydmljZV90eXBlIjoiY29tLmFwcGxlLm1haWQifQ.IhPEK_jgJe3hxSyzBNYxGyqgeqmiT24MFJa5z6GQa-0vJ1FyEah8uG2ui9bfFKBM7HfbY122pKjEBbsv-oYMiRm9kOWvLb-GzDIb0WCx2c12al5OgIyR6c0cGINVALIDhm7vchMu-MrRS6TYp79FcXUcxHe8i2JxPlKJA9I4De4fCHZb4pgbMVpXbAklglCl7REwnP62BGEJUjSyqYNJTHTsgTFkEmLUgBulw99Yz1tXcJEgu5vnKtEWINVALIDcEyK9e6ek4GhfkzeEHd6WlML0wvZiiFG_8XUI4X3a4GfaAXHwTWHbOxyOGbD5GjhE1tPkT9tt5u6w
```

### Troubleshooting

Sometimes something goes wrong with the API or the scripts. Sometimes the API will tell you exactly the problem you have and how you can fix the input. Other times you may need to inspect the HTTP request details. To do that you can turn on `curl` verbose output by setting the `CURL_OPTS` environment variable which all the scripts utilize:
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
module github.com/micromdm/nanodep

go 1.17
go 1.18

require (
github.com/go-sql-driver/mysql v1.8.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/gomodule/oauth1 v0.2.0
github.com/google/uuid v1.3.0
github.com/smallstep/pkcs7 v0.0.0-20231107075624-be1870d87d13
)

Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/gomodule/oauth1 v0.2.0 h1:/nNHAD99yipOEspQFbAnNmwGTZ1UNXiD/+JLxwx79fo=
github.com/gomodule/oauth1 v0.2.0/go.mod h1:4r/a8/3RkhMBxJQWL5qzbOEcaQmNPIkNoI7P8sXeI08=
github.com/smallstep/pkcs7 v0.0.0-20231107075624-be1870d87d13 h1:qRxEt9ESQhAg1kjmgJ8oyyzlc9zkAjOooe7bcKjKORQ=
github.com/smallstep/pkcs7 v0.0.0-20231107075624-be1870d87d13/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
88 changes: 88 additions & 0 deletions http/api/maid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package api

import (
"encoding/json"
"net/http"
"os"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/micromdm/nanodep/godep"
"github.com/micromdm/nanodep/log"
"github.com/micromdm/nanodep/log/ctxlog"
"github.com/micromdm/nanodep/tokenpki"
)

const maidJWTserviceType = "com.apple.maid"

func newMAIDCheckinJWT(depUUID string, key interface{}) (string, error) {
tok := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"iss": depUUID,
"iat": time.Now().Unix(),
"jti": uuid.NewString(),
"service_type": maidJWTserviceType,
})
return tok.SignedString(key)
}

type MAIDJWTStorage interface {
TokenPKIRetriever
godep.ClientStorage
}

// MAIDJWTHandler returns a JWT for DEP Access Management. This JWT should
// be returned to an MDM client's CheckIn "GetToken" message. Note:
// this queries the DEP API "live." A cache of some sort may be a future
// strategy.
func MAIDJWTHandler(store MAIDJWTStorage, logger log.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger := ctxlog.Logger(r.Context(), logger)
if r.URL.Path == "" {
logger.Info("msg", "DEP name check", "err", "missing DEP name")
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}

name := r.URL.Path
logger = logger.With("name", name)

client := godep.NewClient(store, nil)
detail, err := client.AccountDetail(r.Context(), name)
if err != nil {
logger.Info("msg", "getting account detail", "err", err)
jsonError(w, err)
return
}

json.NewEncoder(os.Stdout).Encode(detail)

_, keyBytes, err := store.RetrieveTokenPKI(r.Context(), name)
if err != nil {
logger.Info("msg", "retrieving token keypair", "err", err)
jsonError(w, err)
return
}

key, err := tokenpki.RSAKeyFromPEM(keyBytes)
if err != nil {
logger.Info("msg", "decoding retrieved private key", "err", err)
jsonError(w, err)
return
}

jwt, err := newMAIDCheckinJWT(detail.ServerUUID, key)
if err != nil {
logger.Info("msg", "creating MAID JWT", "err", err)
jsonError(w, err)
return
}

w.Header().Set("Content-type", "application/jwt")
_, err = w.Write([]byte(jwt))
if err != nil {
logger.Info("msg", "writing response body", "err", err)
return
}
}
}
33 changes: 33 additions & 0 deletions http/api/maid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package api

import (
"crypto/rand"
"crypto/rsa"
"testing"

"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)

func TestMAIDCheckinJWT(t *testing.T) {
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
u := uuid.NewString()
s, err := newMAIDCheckinJWT(u, privKey)
if err != nil {
t.Fatal(err)
}
tok, err := jwt.Parse(s, func(_ *jwt.Token) (interface{}, error) { return &privKey.PublicKey, nil })
if err != nil {
t.Fatal(err)
}
iss, err := tok.Claims.GetIssuer()
if err != nil {
t.Fatal(err)
}
if have, want := iss, u; have != want {
t.Errorf("have %v, want %v", have, want)
}
}
8 changes: 8 additions & 0 deletions tools/cfg-maidjwt.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/sh

URL="${BASE_URL}/v1/maidjwt/${DEP_NAME}"

curl \
$CURL_OPTS \
-u "depserver:$APIKEY" \
"$URL"