himmelblau-idm / himmelblau

Azure Entra ID Authentication, with PAM and NSS modules.
GNU General Public License v3.0
29 stars 6 forks source link

Implement device enrollment using ConfidentialClientApplication #35

Closed dmulder closed 8 months ago

dmulder commented 1 year ago

https://learn.microsoft.com/en-us/python/api/msal/msal.application.confidentialclientapplication?view=msal-py-latest#parameters

The constructor for ConfidentialClientApplication allows us to pass a client_credential, which I believe is the private key associated with the public key sent when enrolling the device. This allows us to obtain an auth_token associated with the enrolled device. This could enable nss lookups for users and groups prior to authentication, for example. It could also resolve some the of policy application issues faced, as well as allowing Device policy application.

dmulder commented 1 year ago

Additionally, this may allow a 'blessed' user authentication (such as how MS's current Intune broker does now).

dmulder commented 1 year ago

Here is a blog explaining some of it: https://jairocadena.com/2016/02/01/azure-ad-join-what-happens-behind-the-scenes/

dmulder commented 1 year ago

https://learn.microsoft.com/en-us/azure/active-directory/devices/device-registration-how-it-works

dmulder commented 1 year ago

The Intune portal prints the following debug which may be useful:

2023-09-11 13:55:28  INFO get_client{capability="LinuxEnrollmentService" resource=ResourceId("XXXXX") endpoint="https://fef.msua08.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService/" endpoint="https://fef.msua08.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService/"}: Requesting a token silently resource=ResourceId("XXXXX")
dmulder commented 1 year ago

I thought device registration was happening via this graph request, but the Azure AD audit logs indicate there is a difference between a device add and a device enrollment (which externally MS refers to as a join). Both processes create a device object, but perhaps device add isn't fully functional?

dmulder commented 1 year ago

Here is the definition of the alternativeSecurityIds used in the device add: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dvrj/f900e812-8f1c-4345-9ab0-b91111068651

It is also base64 encoded and using widechars (the spec says it is unicode, so apparently UTF-16 encoded), for example: base64<widechar<"X509:[thumbprint]+[publickeyhash]">> The type is always 2 (not sure if there are alternate types), and the identityProvider is null (simply don't set it).

dmulder commented 1 year ago

I was able to successfully join using a alternativeSecurityId ripped from an intune-portal for Linux join.

dmulder commented 1 year ago

So the join process is as follows: Submit a post to v1.0/devices (with sufficient creds via the bearer) with json content, with the following payload (essentially):

{ "accountEnabled": true, "alternativeSecurityIds": [ { "key": "X509:[thumbprint]+[publickeyhash]", "type": 2 } ], "deviceId": "RANDOM_UUID_GEN", "displayName": "localhost.localdomain", "operatingSystem": "Linux", "operatingSystemVersion": "openSUSE Tumbleweed 20230816", "isCompliant": true, "isManaged": true, "mdmAppId": "0000000a-0000-0000-c000-000000000000" }

Where the alternativeSecurityIds is formatted as previously described, and the deviceId is a randomly generated UUID. The mdmAppId says we are joining as the Microsoft Intune portal (faking it). Perhaps at some point we can create our own mdmAppId. It's important that the operatingSystem is set to Linux (at least for a Linux join, anyway).

Next register an owner of the device: Submit a post to v1.0/devices/{id}/registeredOwners/$ref where {id} is the id of the device (do NOT use the deviceId) with json content, with the following payload:

{ "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/{UID}" }

Where {UID} is the id of the user object which will own the device (perhaps this can be a group?).

This creates a seemingly viable joined Device object, with an attached alternativeSecurityId that we can auth with (this will enable nss lookups, etc).

dmulder commented 1 year ago

This protocol appears to define the whole process: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dvrj/fbac29b8-92dd-4757-8fd9-c004b9efb6e3

More importantly, here is the specific request and response protocol details which we need.

dmulder commented 1 year ago

Closely inspecting several valid Alt-Security-Identities revealed that the publickeyhash is not SHA1, but clearly SHA256. This lead me to this documentation: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dvre/1f2ebba7-7783-42c4-982a-ce9ca76af949

dmulder commented 11 months ago

I think this is how the device is enrolled in Intune after device creation and CSR. https://learn.microsoft.com/en-us/graph/api/intune-devices-manageddevice-create?view=graph-rest-1.0&tabs=http

dmulder commented 11 months ago

MS's intune-portal sends a json request (somewhere) such as this to get a certificate after device creation:

{
    "CertificateSigningRequest": "SOME_CSR",
    "AppVersion": "Application version",
    "DeviceName": "A named device",
}

The request includes the parameters api-version and client-version in the url (I'm not sure of their value). I think maybe this is requesting a replacement certificate every time a user auths to the device? I suspect the purpose is to continually renew the device auth and keep kicking the can down the road.

dmulder commented 11 months ago

Device creation via the intune-portal sends the following json POST:

{
    "DeviceId": "XXXXXXXX",
    "DeviceName": "XXXXXXXX",
    "Manufacturer": "XXXXXXXX",
    "OSDistribution": "XXXXXXXX",
    "OSVersion": "XXXXXXXX",
}

Also with the api-version and client-version url params.

dmulder commented 11 months ago

You can fetch the enrollment endpoints with a get request to: https://graph.windows.net/myorganization/applicationRefs/0000000a-0000-0000-c000-000000000000/appData?$select=appData&api-version=1.6-internal This appears to be some kind of internal graph. The access_token is something different than expected. I tried sending this request using a token from authenticating via msal, but it fails. If I sign into the intune portal (via the web), then navigate to https://portal.azure.com/#view/Microsoft_AAD_IAM/MdmConfiguration.ReactView/appName/Microsoft%20Intune/appId/0000000a-0000-0000-c000-000000000000 I can scrape a token from the web dev tools. This token successfully authenticates to the internal graph. Apparently I need to authenticate differently here.

dmulder commented 11 months ago

The following get query response contains the enrollment url found in the debug previously (https://fef.msua08.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService/):

GET https://graph.windows.net/myorganization/applicationRefs/0000000a-0000-0000-c000-000000000000?api-version=1.6-internal&$select=identifierUris
{
    "odata.metadata": "https://graph.windows.net/myorganization/$metadata#applicationRefs/@Element",
    "identifierUris": [
        "https://fef.amsuin01.manage.microsoft.com",
        "https://fef.msuc06.manage.microsoft.com",
        "https://fef.msua09.manage.microsoft.com",
        "https://manage-mig.microsoft.com",
        "https://fef.migsu01.manage-mig.microsoft.com",
        "https://fef.migsu02.manage-mig.microsoft.com",
        "https://fef.msub07.manage.microsoft.com",
        "https://fef.msub06.manage.microsoft.com",
        "https://fef.msua08.manage.microsoft.com",
        "https://fef.amsub0502.manage.microsoft.com",
        "https://fef.amsua0702.manage.microsoft.com",
        "https://fef.amsua0102.manage.microsoft.com",
        "https://fef.msud01.manage.microsoft.com",
        "https://fef.shmsua02.manage-selfhost.microsoft.com",
        "https://warehouse.manage.microsoft.com",
        "https://warehouse.manage-beta.microsoft.com",
        "https://warehouse.manage-selfhost.microsoft.com",
        "https://fef.amsub0302.manage.microsoft.com",
        "https://fef.amsub0202.manage.microsoft.com",
        "https://fef.amsua0202.manage.microsoft.com",
        "https://fef.amsua0402.manage.microsoft.com",
        "https://manage-beta.microsoft.com",
        "https://manage.microsoft.com",
        "https://manage-beta.microsoft.com/",
        "https://manage.microsoft.com/",
        "https://manage-selfhost.microsoft.com",
        "https://fef.shmsua01.manage-selfhost.microsoft.com",
        "https://fef.bmsua01.manage-beta.microsoft.com",
        "https://fef.bmsub01.manage-beta.microsoft.com",
        "https://fef.msua07.manage.microsoft.com",
        "https://fef.msub05.manage.microsoft.com",
        "https://fef.msub03.manage.microsoft.com",
        "https://fef.msub02.manage.microsoft.com",
        "https://FEF.amsub0102.manage.microsoft.com",
        "https://fef.msub01.manage.microsoft.com",
        "https://fef.msuc05.manage.microsoft.com",
        "https://fef.msuc03.manage.microsoft.com",
        "https://fef.msuc02.manage.microsoft.com",
        "https://fef.msuc01.manage.microsoft.com",
        "https://fef.amsua0602.manage.microsoft.com",
        "https://fef.msua06.manage.microsoft.com",
        "https://fef.amsua0502.manage.microsoft.com",
        "https://fef.msua05.manage.microsoft.com",
        "https://fef.msua04.manage.microsoft.com",
        "https://fef.msua02.manage.microsoft.com",
        "https://fef.msua01.manage.microsoft.com"
    ]
}

The string LinuxMDM at least is hard coded in the intune-portal binary, so perhaps the rest of the url is generated.

dmulder commented 11 months ago

FYI, the queries against graph.windows.net fail against the public graph server graph.microsoft.com. Interestingly, with an error saying 'Invalid version', suggesting the api version 1.6-internal is important.

dmulder commented 11 months ago

Interesting, this internal graph allows me to do a full user listing with much more information than the public graph returns: https://graph.windows.net/myorganization/users?api-version=1.6&$select=userPrincipalName

dmulder commented 11 months ago

Ah, graph.windows.net is the deprecated Azure AD graph... which MS is still using to perform the enrollment, hrm.

dmulder commented 11 months ago

Ok, this is the correct request using the new MS Graph: https://graph.microsoft.com/v1.0/servicePrincipals?$filter=appId eq '0000000a-0000-0000-c000-000000000000' Which returns an interesting tidbit of information:

            "oauth2PermissionScopes": [
                {
                    "adminConsentDescription": "Allows Intune Admins to enroll a Microsoft Tunnel Gateway Agent",
                    "adminConsentDisplayName": "MicrosoftTunnelGatewayEnrollment",
                    "id": "e323f13a-1fcc-49cc-883f-c6da13ae0542",
                    "isEnabled": true,
                    "type": "User",
                    "userConsentDescription": "Allows Intune Admins to enroll a Microsoft Tunnel Gateway Agent",
                    "userConsentDisplayName": "MicrosoftTunnelGatewayEnrollment",
                    "value": "MicrosoftTunnelGatewayEnrollment"
                }
            ],
dmulder commented 11 months ago

The following GET request returns a list of service URLs for enrollment:

GET https://enterpriseregistration.windows.net/$tenant_id/Discover?api-version=1.9&managed=True

Where $tenant_id is the tenant id of your domain. The request must be made with an Authorization header with the access_token obtained from msal. It also requires the headers ocp-adrs-client-name and ocp-adrs-client-version, which are respectively the machine name and the version of your client software (these are arbitrary).

dmulder commented 11 months ago

The following spec appears to describe the service URLs for enrollment: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dvrd/296ebf70-bd4b-489e-a531-460d8ef7519b

dmulder commented 11 months ago

This is gold: https://learn.microsoft.com/en-us/windows/client-management/azure-active-directory-integration-with-mdm Explains how to create our own MDM, so we can get access to the join secrets.

dmulder commented 11 months ago

And here is a useful walk through.

dmulder commented 11 months ago

I noticed the services listed from the discovery service response refer to the services as 'ms-drs', which is the device registration service: https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/technical-reference/device-registration-technical-reference This can apparently be installed on a Windows Server.

dmulder commented 11 months ago

https://learn.microsoft.com/en-us/azure/active-directory/devices/concept-primary-refresh-token#how-is-a-prt-issued

dmulder commented 11 months ago

The Intune portal prints the following debug which may be useful:

2023-09-11 13:55:28  INFO get_client{capability="LinuxEnrollmentService" resource=ResourceId("XXXXX") endpoint="https://fef.msua08.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService/" endpoint="https://fef.msua08.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService/"}: Requesting a token silently resource=ResourceId("XXXXX")

I finally found where this comes from. An authenticated get request to https://graph.microsoft.com/beta/servicePrincipals/appId=0000000a-0000-0000-c000-000000000000/endpoints returns a long list of enrollment endpoints, including this one for Linux. You can see it in the network traffic if you navigate to the Intune admin center, then open the Data Warehouse pane by selecting Reports > Data warehouse.

dmulder commented 11 months ago

These are the interesting ones:

        {
            "id": "352ac532-313b-4e22-b513-1f81e153ab68",
            "deletedDateTime": null,
            "capability": "LinuxDeviceCheckinService",
            "providerId": "0000000a-0000-0000-c000-000000000000",
            "providerName": "LinuxDeviceCheckinService",
            "uri": "https://fef.amsua0602.manage.microsoft.com/TrafficGateway/TrafficRoutingService/LinuxMdm/LinuxDeviceCheckinService",
            "providerResourceId": "88cf0172-3b18-4cd4-abe5-552181a59741"
        },
        {
            "id": "7bf281a6-4d6a-42b1-b01e-12bd6fc02f06",
            "deletedDateTime": null,
            "capability": "LinuxEnrollmentService",
            "providerId": "0000000a-0000-0000-c000-000000000000",
            "providerName": "LinuxEnrollmentService",
            "uri": "https://fef.amsua0602.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService",
            "providerResourceId": "6a559596-421e-4d9f-b81d-3a27680c5e66"
        }
dmulder commented 11 months ago

The following scopes are required to read that list: "CloudPC.Read.All CloudPC.ReadWrite.All DeviceManagementApps.ReadWrite.All DeviceManagementConfiguration.ReadWrite.All DeviceManagementManagedDevices.PrivilegedOperations.All DeviceManagementManagedDevices.ReadWrite.All DeviceManagementRBAC.ReadWrite.All Directory.AccessAsUser.All email openid profile Sites.Read.All"

dmulder commented 11 months ago

Seems like we get preauthorization from a broker app?

https://graph.windows.net/myorganization/applicationRefs/29d9ed98-a469-4536-ade2-f981bc1d605e?api-version=1.6-internal
{
    "odata.metadata": "https://graph.windows.net/myorganization/$metadata#applicationRefs/@Element",
    "appCategory": null,
    "appContextId": "f8cdef31-a31e-4b4a-93e4-5f571e91255a",
    "appData": null,
    "appId": "29d9ed98-a469-4536-ade2-f981bc1d605e",
    "appRoles": [],
    "availableToOtherTenants": true,
    "certification": null,
    "displayName": "Microsoft Authentication Broker",
    "errorUrl": null,
    "homepage": null,
    "identifierUris": [],
    "knownClientApplications": [],
    "logoutUrl": null,
    "logoUrl": null,
    "mainLogo@odata.mediaEditLink": "applicationRefs/29d9ed98-a469-4536-ade2-f981bc1d605e/Microsoft.DirectoryServices.ApplicationRef/mainLogo",
    "oauth2Permissions": [],
    "publisherDomain": "sharepoint.com",
    "publisherName": "Microsoft Services",
    "publicClient": true,
    "replyUrls": [
        "https://login.microsoftonline.com/WebApp/CloudDomainJoin/8",
        "https://login.microsoftonline.com/WebApp/CloudDomainJoin/3",
        "https://login.microsoftonline.com/WebApp/CloudDomainJoin/6",
        "https://login.microsoftonline.com/WebApp/CloudDomainJoin/4",
        "urn:ietf:wg:oauth:2.0:oob",
        "ms-appx-web://Microsoft.AAD.BrokerPlugin/DRSFF",
        "ms-aadj-redir://auth/mdm",
        "ms-aadj-redir://auth/drs",
        "https://clouddomainjoin.azurewebsites.net/webapp/clouddomainjoin/2",
        "msauth://Microsoft.AAD.BrokerPlugin",
        "ms-appx-web://Microsoft.AAD.BrokerPlugin/DRS",
        "ms-appx-web://Microsoft.AAD.BrokerPlugin/MDM",
        "ms-appx-web://Microsoft.AAD.BrokerPlugin",
        "https://login.microsoftonline.com/applebroker/msauth",
        "https://login.microsoftonline.com/applebroker/msauthv2",
        "https://login.microsoftonline.com/androidbroker/msauth"
    ],
    "requiredResourceAccess": [],
    "samlMetadataUrl": null,
    "supportsConvergence": true,
    "verifiedPublisher": {
        "displayName": null,
        "verifiedPublisherId": null,
        "addedDateTime": null
    }
}
dmulder commented 11 months ago

Someone else has already figured this out: https://aadinternals.com/post/prt/#creating-your-own-prt

dmulder commented 9 months ago

aad-join-spec.pdf

dmulder commented 8 months ago

Resolved by MR#46