libhimmelblau 0.8.37

Samba Library for Azure Entra ID Authentication
Documentation
MSAL
====

The purpose of this project is to implement MSAL for Rust, based on the specifications found in the Microsoft API Reference for [ClientApplication Class](https://learn.microsoft.com/en-us/python/api/msal/msal.application.clientapplication?view=msal-py-latest) and [PublicClientApplication Class](https://learn.microsoft.com/en-us/python/api/msal/msal.application.publicclientapplication?view=msal-py-latest). These are Python references which will be mimicked in Rust here.

The project also implements the [MS-DRS] protocol, which is undocumented by
microsoft. A [protocol specification](https://github.com/himmelblau-idm/aad-join-spec/releases/latest)
is in progress as part of the himmelblau project.

In addition to the ClientApplication Class and [MS-DRS] implementations, this project implements [MS-OAPXBC] sections [3.1.5.1.2 Request for Primary Refresh Token](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oapxbc/d32d5cd0-05d4-4ec2-8bcc-ac29ce711c23) and [3.1.5.1.3 Exchange Primary Refresh Token for Access Token](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oapxbc/06e2bf0d-8cea-4b11-ad78-d212330ebda9). These are not implemented in Microsoft's MSAL libraries, but are possible when authenticating from an enrolled device.

How do I use this library?
--------------------------

Import the module into your project, then include the PublicClientApplication:

```Rust
use msal::PublicClientApplication;
```

Create an instance of the PublicClientApplication, then authenticate:

```Rust
let authority = format!("https://login.microsoftonline.com/{}", tenant_id);
let app = PublicClientApplication::new(client_id, Some(&authority)).expect("Failed creating app");
let scope = vec![];
let token = app.acquire_token_by_username_password(username, password, scope).await?;
```

You can obtain your `client_id` and `tenant_id` from the Azure portal.

You can perform a silent auth using a previously obtained refresh token:

```Rust
let token = app.acquire_token_silent(scope, &token.refresh_token).await?;
```

Or finally, you can perform a Device Authorization Grant:

```Rust
let flow = app.initiate_device_flow(scope).await?;

// Prompt the user with the message found in flow.message

let token = app.acquire_token_by_device_flow(flow).await?;
```

If msal is built with the `broker` feature, you can enroll the device, then request an authentication token:

```Rust
use kanidm_hsm_crypto::soft::SoftTpm;
use kanidm_hsm_crypto::{BoxedDynTpm, Tpm, AuthValue};

// First create the TPM object and a machine_key
let mut tpm = BoxedDynTpm::new(SoftTpm::new());
let auth_str = AuthValue::generate().expect("Failed to create hex pin");
let auth_value = AuthValue::from_str(&auth_str).expect("Unable to create auth value");
let loadable_machine_key = tpm
    .machine_key_create(&auth_value)
    .expect("Unable to create new machine key");
let machine_key = tpm
    .machine_key_load(&auth_value, &loadable_machine_key)
    .expect("Unable to load machine key");

let app = BrokerClientApplication::new(Some(&authority), None, None).expect("Failed creating app");

// Obtain a token for authentication. If authenticating here without MFA, the PRT and
// user token will not have the mfa claim. Use initiate_device_flow_for_device_enrollment()
// and acquire_token_by_device_flow() to authenticate with the
// mfa claim.
let token = app.acquire_token_by_username_password_for_device_enrollment(username, password).await?;
// Specify the attributes which will be used for enrollment
let attrs = match EnrollAttrs::new(
    domain.to_string(),
    Some("test_machine".to_string()), // Device name
    Some("Linux".to_string()), // Device type
    Some(0), // Join type
    Some("openSUSE Leap 15.5".to_string()), // OS version
) {
    Ok(attrs) => attrs,
    Err(e) => {
        println!("{:?}", e);
        return ();
    }
};
// Use the tpm for enrollment.
let (transport_key, cert_key, device_id) = app.enroll_device(&token.refresh_token, attrs, &mut tpm, &machine_key).await?;

// Request an authentication token
let token = app.acquire_token_by_username_password(username, password, scope, &mut tpm, &machine_key).await?;
```

In order to initialize a BrokerClientApplication that was previously enrolled, ensure you've cached your `auth_value`, `loadable_machine_key`, `transport_key`, and `cert_key`. The `auth_value` MUST be stored in a secure manor only accessible to your application. Preferably your application should execute as a unique user, and only that user will have read access to the `auth_value`. Re-initialize as follows:

```Rust
let mut tpm = BoxedDynTpm::new(SoftTpm::new());
let loadable_machine_key = tpm
    .root_storage_key_create(&auth_value)
    .expect("Unable to create new machine key");
let machine_key = tpm
    .root_storage_key_load(&auth_value, &loadable_machine_key)
    .expect("Unable to load machine key");

let app = BrokerClientApplication::new(Some(&authority), Some(&transport_key), Some(&cert_key)).expect("Failed creating app");
```

Entra OpenSSH certificates
--------------------------

`BrokerClientApplication` can request the same Microsoft-issued OpenSSH user
certificate used by Azure SSH login. The caller generates and retains an
ephemeral RSA private key, then passes its `ssh-rsa` public-key line to
`exchange_prt_for_ssh_certificate`. The returned `access_token` is treated as a
base64-encoded OpenSSH certificate, not as a JWT. Use
`EntraSshCertificate::openssh_certificate()` to obtain the complete
`ssh-rsa-cert-v01@openssh.com` line suitable for an OpenSSH certificate file.

`exchange_prt_for_ssh_certificate` deliberately uses the sealed PRT flow; it
does not expose a refresh-token or interactive fallback. The result also
provides the complete signing CA OpenSSH public key and its SHA-256 fingerprint
for target-side trust decisions.

C callers own the returned certificate until `ssh_certificate_free`, and must
release strings returned by its accessors with `string_free`. Principal arrays
must be released with `ssh_certificate_free_principals` and their exact count.
Python values are managed by Python.

The library does not write key files, invoke `ssh`, or validate target-side CA
trust. Callers remain responsible for secure private-key storage and cleanup.

Using the On-Behalf-Of (OBO) flow
---------------------------------

The OBO flow is available when built with the `on_behalf_of` feature.

Rust (confidential middle-tier service):

```Rust
use msal::{ClientCredential, ConfidentialClientApplication, MsalError};

let authority = format!("https://login.microsoftonline.com/{}", tenant_id);
let credential = ClientCredential::from_secret(client_secret.to_string());
let app = ConfidentialClientApplication::new(client_id, Some(&authority), credential)?;

match app
    .acquire_token_on_behalf_of(
        user_access_token, // incoming bearer token
        vec!["https://graph.microsoft.com/User.Read"],
        None,
    )
    .await
{
    Ok(token) => {
        println!("access_token={}", token.access_token);
    }
    Err(MsalError::OboInteractionRequired { claims, .. }) => {
        // Return this claims challenge to the original client so it can
        // re-authenticate and satisfy Conditional Access.
        println!("claims_challenge={:?}", claims);
    }
    Err(e) => return Err(e),
}
```

C API:

* Initialize with `confidential_client_init_with_secret`. Note: only client secret
  credentials are currently supported in the C API; certificate-based credentials
  are available from Rust only.
* Exchange the incoming user token with `confidential_acquire_token_on_behalf_of`.
* Compile OBO C callers with `-DON_BEHALF_OF` so OBO declarations are visible in
  the generated header.
* Read token fields with:
  * `obo_token_access_token`
  * `obo_token_token_type`
  * `obo_token_expires_in`
  * `obo_token_ext_expires_in`
  * `obo_token_scope` (returns `NULL` when not present)
  * `obo_token_refresh_token` (returns `NULL` when not present)
* On Conditional Access claims challenge, check `MSAL_ERROR.code == OBO_INTERACTION_REQUIRED`
  and read `MSAL_ERROR.claims`.

Python API:

* Create `ConfidentialClientApplication(client_id, authority, client_secret)`. Note:
  only client secret credentials are currently supported in the Python API;
  certificate-based credentials are available from Rust only.
* Call `acquire_token_on_behalf_of(user_assertion, scopes)`.
* Catch `OboInteractionRequiredError` and inspect:
  * `claims`
  * `error`
  * `error_description`
  * `error_codes`
  * `suberror`

Reference examples:

* `example/msal_obo_example.c`
* `example/msal_obo_example.py`
* `example/msal_obo_end_to_end_test.py` (full functional validation: upstream token -> OBO -> Graph `/me`)

Using the Python API
--------------------

A script that uses `PublicClientApplication` from Python can be found in the [examples](example/msal_public_example.py).

This script uses a public client created via an [Azure App Registration](https://himmelblau-idm.org/docs/advanced/Creating-an-Entra-ID-Application-for-Himmelblau-GroupMember.Read.All-Permissions/). The URL `https://login.microsoftonline.com/common/oauth2/nativeclient` should be used as a "Mobile and desktop applications" Redirect URI under the Authentication settings for the App Registration. The script was tested with the following permissions for the application:

* email
* Group.Read.All
* offline_access
* openid
* profile
* User.Read

The script needs the tenant ID and client ID for the App Registration, and allows logging in with a username and password.
It also demonstrates explicitly choosing an MFA method for the login, rather than using the default MFA method.

To build `libhimmelblau` and test it with this script using (uv)[https://docs.astral.sh/uv/]:

```sh
uv tool install maturin
uv venv && uv pip install patchelf cffi
# build the library with python bindings and install it into the virtual environment
maturin build --features "pyapi,on_behalf_of" && uv pip install --force-reinstall target/wheels/libhimmelblau-*.whl

python example/msal_public_example.py
```