Use the Microsoft Graph API and the Samsung KME API to automatic replace profile tokens for Android Enterprise dedicated devices
Thought to share how I have automated the renewal of tokens for Android Enterprise dedicated devices profile in MEM and Samsung KME. I found some people who have blogged about some steps but none that have automated it all the way. The difficult step for me was to use the Samsung KME API as I didn’t want to use their own assemblies to generate and sign JWT access token with the keys.json file.
Why I don’t want to use Samsung’s own assemblies is because you have to pass the path to the keys.json file instead of pass it as a string. This means that I can’t store it securely by using Azure Key Vault. Since I like to use Logic Apps I had to create an Azure Function that I can trigger to generate an access token by passing the clientId and keys.json as strings.
Components
- Azure Functions App
- Azure Key Vault
- Logic Apps
Azure Functions App
Note: See the blog post from Nicola Suter that I’ve linked below for instructions on how use Advanced Tools to upload the dll’s to the Modules folder in the Azure Function App
Dll’s
- NuGet Gallery | Portable.BouncyCastle 1.9.0
- NuGet Gallery | JWT 8.6.0
- NuGet Gallery | Newtonsoft.Json 10.0.3
Note: The code is based on the blog post from Tobias Almen but instead of using Samsung’s assemblies I build my own code to handle the certificate and signing. See his blog post that I have linked below for more information how to get started with the Samsung KME API
GetKnoxJWTAccessToken
<#
.SYNOPSIS
Azure Function to generate and sign JWT access token.
.DESCRIPTION
This function uses third party assemblies to generate and sign JWT access token by passing the client identifier and the content of keys.json as strings. To call this function you first need to set up API access in the Samsung Knox portal and generated a client identifier and download the keys.json file.
All needed assemblies can be download from the nuget homepage. https://www.nuget.org/packages
Portable.BouncyCastle 1.9.0
JWT 8.6.0
Newtonsoft.Json 10.0.3
.NOTES
Written by: Tobias Renström
Blog: https://cloudnstuff.com/
Version: 1.0
#>
using namespace System.Net
# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)
[Reflection.Assembly]::LoadFile("C:\home\site\wwwroot\Modules\BouncyCastle.Crypto.dll") | Out-Null
[Reflection.Assembly]::LoadFile("C:\home\site\wwwroot\Modules\JWT.dll") | Out-Null
[Reflection.Assembly]::LoadFile("C:\home\site\wwwroot\Modules\Newtonsoft.Json.dll") | Out-Null
#region functions
function Get-RsaParameters {
param(
[Parameter(Mandatory = $true)]
[System.String]$privateKey
)
$byteArray = [System.Text.Encoding]::ASCII.GetBytes($privateKey)
$ms = [System.IO.MemoryStream]::new($byteArray)
$sr = [System.IO.StreamReader]::new($ms)
$pemReader = [Org.BouncyCastle.OpenSsl.PemReader]::new($sr)
$pemObj = $pemReader.ReadObject()
$pubSpec = [Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters]::new($false, $pemObj.Modulus, $pemObj.PublicExponent)
$keyPair = [Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair]::new($pubSpec, $pemObj)
return [Org.BouncyCastle.Security.DotNetUtilities]::ToRSAParameters($keyPair.Private)
}
function Get-RS256JWTEncoder {
param(
[Parameter(Mandatory = $true)]
[System.Security.Cryptography.RSAParameters]$rsaParams
)
$csp = [System.Security.Cryptography.RSACryptoServiceProvider]::new()
$csp.ImportParameters($rsaParams)
$algorithm = [JWT.Algorithms.RS256Algorithm]::new($csp, $csp)
$serializer = [JWT.Serializers.JsonNetSerializer]::new()
$urlEncoder = [JWT.JwtBase64UrlEncoder]::new()
return [JWT.JwtEncoder]::new($algorithm, $serializer, $urlEncoder)
}
function Get-SignedClientIdentifier {
param(
[Parameter(Mandatory = $true)]
$certificate,
[System.String]$clientId,
[Parameter(Mandatory = $false)]
[System.Int32]$validForSeconds = 1800
)
$privateKey = "-----BEGIN PRIVATE KEY-----`n" + $certificate.Private + "`n-----END PRIVATE KEY-----"
$publicKey = $certificate.Public
$createTime = [System.Int32][System.Double]::parse((Get-Date -Date $((Get-Date).ToUniversalTime()) -UFormat %s))
$expiryTime = [System.Int32][System.Double]::parse((Get-Date -Date $((Get-Date).addseconds($validforSeconds).ToUniversalTime()) -UFormat %s))
$payload = [System.Collections.Generic.Dictionary[System.String, System.Object]]::new()
$payload.Add('clientIdentifier', "$clientId")
$payload.Add('publicKey', "$publicKey")
$payload.Add('jti', "$([guid]::NewGuid())")
$payload.Add('aud', "KnoxWSM")
$payload.Add('exp', "$expiryTime")
$payload.Add('iss', "Knox")
$payload.Add('iat', "$createTime")
$payload.Add('nbf', "$createTime")
$header = [System.Collections.Generic.Dictionary[System.String, System.Object]]::new()
# Generate signed Client Identifier
try {
$rsaParams = Get-RsaParameters -privateKey $privateKey
$encoder = Get-RS256JWTEncoder -rsaParams $rsaParams
$script:signedClientIdentifier = $encoder.Encode($header, $payload, [System.Byte[]]::new(0))
}
catch {
Write-Error $_.Exception | format-list -force
Break
}
}
function Get-SignedAccessToken {
param(
[Parameter(Mandatory = $true)]
$certificate,
[Parameter(Mandatory = $false)]
[System.Int32]$validForSeconds = 1800
)
$privateKey = "-----BEGIN PRIVATE KEY-----`n" + $certificate.Private + "`n-----END PRIVATE KEY-----"
$publicKey = $certificate.Public
$createTime = [System.Int32][System.Double]::parse((Get-Date -Date $((Get-Date).ToUniversalTime()) -UFormat %s))
$expiryTime = [System.Int32][System.Double]::parse((Get-Date -Date $((Get-Date).addseconds($validforSeconds).ToUniversalTime()) -UFormat %s))
# Build body for access token request
$body = [ordered]@{
clientIdentifierJwt = "$signedClientIdentifier"
base64EncodedStringPublicKey = "$publicKey"
#validityForAccessTokenInMinutes = 30
}
# Convert body to json format
$requestObject = $body | ConvertTo-Json
$requestbody = @"
$requestObject
"@
# Set access token request headers
$headers = @{'Content-type' = 'application/json' }
# If JWT has been generated, request access token
if ($body.clientIdentifierJwt) {
$accessToken = (Invoke-RestMethod -Method Post -Uri "https://eu-kcs-api.samsungknox.com/ams/v1/users/accesstoken" -Body $requestBody -Headers $headers).accessToken
}
else {
Write-Error "Not able to get JWT"
}
# If access token request failed, break
if (-not ($accesstoken)) {
Write-Error "Not able to get access token"
Break
}
else {
$payload = [System.Collections.Generic.Dictionary[System.String, System.Object]]::new()
$payload.Add('accessToken', "$accessToken")
$payload.Add('publicKey', "$publicKey")
$payload.Add('jti', "$([guid]::NewGuid())")
$payload.Add('aud', "KnoxWSM")
$payload.Add('exp', "$expiryTime")
$payload.Add('iss', "Knox")
$payload.Add('iat', "$createTime")
$payload.Add('nbf', "$createTime")
$header = [System.Collections.Generic.Dictionary[System.String, System.Object]]::new()
try {
$rsaParams = Get-RsaParameters $privateKey
$encoder = Get-RS256JWTEncoder $rsaParams
$script:signedAccessToken = $encoder.Encode($header, $payload, [System.Byte[]]::new(0))
}
catch {
Write-Error $_.Exception | format-list -force
Break
}
}
}
#endregion functions
$ErrorActionPreference = "Stop"
# Write to the Azure Functions log stream.
Write-Host "PowerShell HTTP trigger function processed a request."
# Interact with query parameters or the body of the request.
$keysJson = $Request.Query.keysJson.Replace
if (-not $keysJson) {
$keysJson = $Request.Body.keysJson
}
$clientId = $Request.Query.clientId
if (-not $clientId) {
$clientId = $Request.Body.clientId
}
$certificate = $keysJson | ConvertFrom-Json
try
{
# Generate access token
Get-SignedClientIdentifier -certificate $certificate -clientId $clientId
Get-SignedAccessToken -certificate $certificate
$Body = $signedAccessToken
$StatusCode = [HttpStatusCode]::OK
}
catch
{
$Body = $_.Exception.Message
$StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
Body = $Body
})
Logic Apps
updateknoxtoken-logicapps
Microsoft Graph API
Note: See the blog post from Daniel Chronlund that I have linked below how to get started with the Microsoft Graph API to replace the profile token in MEM
HTTP – Get Intune Graph token
Update 2022-01-04: Now it works with application permissions. Below I changed the step to get a Intune Graph token
The HTTP method | POST |
The request URL | https://login.microsoftonline.com/tenantId (Initialize variable)/oauth2/v2.0/token |
The Content-Type header | application/x-www-form-urlencoded |
The request body | grant_type=client_credentials&client_id=intuneClientId (Initialize variable)&client_secret=urlEncodedIntuneClientSecret (Compose)&scope=https://graph.microsoft.com/.default |
HTTP – Get Intune Android dedicated profile
The HTTP method | GET |
The request URL | https://graph.microsoft.com/beta/deviceManagement/androidDeviceOwnerEnrollmentProfiles?$filter=displayName eq ‘intuneProfileDisplayName (Initialize variable)‘&select=id,displayName |
The Authorization header | Bearer Parse JSON – Parse Intune Graph token response body (body)?[‘access_token’] |
Compose – intuneProfileId
first(body('Parse_JSON_-_Parse_Intune_Android_dedicated_profile_response_body')?['value'])?['id']
HTTP – Replace Intune Android dedicated profile token
The HTTP method | POST |
The request URL | https://graph.microsoft.com/beta/deviceManagement/androidDeviceOwnerEnrollmentProfiles/intuneProfileId (Compose)/createToken |
The Content-Type header | application/x-www-form-urlencoded |
The request body | { “tokenValidityInSeconds”: 7776000 } |
HTTP – Get Intune Android dedicated profile token
The HTTP method | GET |
The request URL | https://graph.microsoft.com/beta/deviceManagement/androidDeviceOwnerEnrollmentProfiles/intuneProfileId (Initialize variable)/?select=tokenValue |
The Authorization header | Bearer Parse JSON – Parse Intune Graph token response body (body)?[‘access_token’] |
Samsung KME API
GetKnoxJWTAccessToken
The request body | { “clientId”: “knoxClientId (Get secret)“, “keysJson”: “knoxKeysJson (Get secret)“ } |
HTTP – Get Knox profile
The HTTP method | GET |
The request URL | https://eu-kcs-api.samsungknox.com/kcs/v1/kme/profiles/list/?search=knoxProfileName (Initialize variable) |
The cache-control header | no-cache |
The charset header | UTF-8 |
The content-type header | application/json |
The x-knox-apitoken header | GetKnoxJWTAccessToken (body) |
Compose – Update token value in mdmProfileCustomData
replace(first(body('Parse_JSON_-_Get_Knox_profile')?['profileList'])?['mdmProfileCustomData'], json(first(body('Parse_JSON_-_Get_Knox_profile')?['profileList'])?['mdmProfileCustomData'])?['com.google.android.apps.work.clouddpc.EXTRA_ENROLLMENT_TOKEN'], body('Parse_JSON_-_Parse_Intune_Android_dedicated_profile_token_response_body_')?['tokenValue'])
Compose – Update Intune token for Knox profile JSON body
{
"customerId": "@{variables('knoxCustomerId')}",
"mdmApkInfo": @{first(body('Parse_JSON_-_Get_Knox_profile')?['profileList'])?['mdmApkInfo']},
"mdmProfileCustomData": @{outputs('Compose_-_Update_token_value_in_mdmProfileCustomData')},
"profileId": "@{first(body('Parse_JSON_-_Get_Knox_profile')?['profileList'])?['id']}",
"profileName": "@{first(body('Parse_JSON_-_Get_Knox_profile')?['profileList'])?['name']}",
"supportCompanyName": "@{first(body('Parse_JSON_-_Get_Knox_profile')?['profileList'])?['supportInfo']?['companyName']}",
"supportEmailAddress": "@{first(body('Parse_JSON_-_Get_Knox_profile')?['profileList'])?['supportInfo']?['supportEmailAddress']}",
"supportPhoneNumber": "@{first(body('Parse_JSON_-_Get_Knox_profile')?['profileList'])?['supportInfo']?['supportPhoneNumber']}",
"supportedMDM": "@{first(body('Parse_JSON_-_Get_Knox_profile')?['profileList'])?['supportedMDM']}"
}
HTTP – Update Intune token for Knox profile
The HTTP method | PUT |
The request URL | https://eu-kcs-api.samsungknox.com/kcs/v1/kme/profiles/first(Parse JSON – Get Knox profile (body)?[‘profileList’])?[‘id’] |
The cache-control header | no-cache |
The charset header | UTF-8 |
The content-type header | application/json |
The x-knox-apitoken header | GetKnoxJWTAccessToken (body) |
The request body | Update Intune token for Knox profile JSON body (Compose) |
Summary
As I said before, some of the code is based from other people’s examples so that I could quickly get a workable solution. Will change and improve the code when I get more time but I hope this can help others to get started. You could also build this entirely in PowerShell and instead use Azure Automation to run the automation. I also hope that Microsoft solves the problem of not being able to use the Graph application permissions to replace the profile token in MEM as Daniel Chronlund mentions in his blog post that I have linked below.
Thanks and inspiration
Tobias Almen – Get devices from Samsung Knox Mobile Enrollment using Powershell (almenscorner.io)
Nicola Suter – Add PowerShell modules to Azure functions – nicolonsky tech
Daniel Chronlund – How to Automate Renewal of Android Dedicated Devices Enrollment Tokens and QR Codes in MEM (Solve the 90 Day Limit Issue) – Daniel Chronlund Cloud Tech Blog