Skip to content

Commit a7cb5bd

Browse files
Update mTLS docs: JNA implementation, correct resource, fix links
- msal4j-sdk/docs/mtls-pop.md: - Path 2: replace subprocess/.NET description with JNA/CNG description - Remove 'Why Java Cannot Use CNG' section (no longer true) - Remove .NET 8 runtime from requirements - Update ManagedIdentityParameters API table description - msal4j-sdk/docs/mtls-pop-manual-testing.md: - Path 2: remove .NET helper exe smoke-test, .NET runtime prereq - Replace with fat JAR e2e runner (path2 --attest) - Show expected output including JWT claims and downstream 401 - Change resource from management.azure.com to graph.microsoft.com - Add resource enrollment note (AADSTS392196) - Update troubleshooting table - msal4j-mtls-extensions/README.md: - Fix broken quick links (docs are in msal4j-sdk/docs/, not here) - Change resource examples from management.azure.com to graph.microsoft.com - Add resource enrollment note - Add Path 1 (Confidential Client) quick start section - Add e2e test driver section with path1/path2 commands Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7a835e5 commit a7cb5bd

3 files changed

Lines changed: 167 additions & 118 deletions

File tree

msal4j-mtls-extensions/README.md

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ The latest code resides in the `dev` branch.
66

77
Quick links:
88

9-
| [Docs](docs/mtls-pop.md) | [Manual Testing](docs/mtls-pop-manual-testing.md) | [Architecture](docs/mtls-pop-architecture.md) | [Support](README.md#community-help-and-support) |
10-
| --- | --- | --- | --- |
9+
| [Docs](../../msal4j-sdk/docs/mtls-pop.md) | [Manual Testing](../../msal4j-sdk/docs/mtls-pop-manual-testing.md) | [Support](README.md#community-help-and-support) |
10+
| --- | --- | --- |
1111

1212
## Installation
1313

@@ -50,7 +50,7 @@ Unlike msal-dotnet, which receives this DLL automatically via NuGet, Java applic
5050

5151
Before using this extension, ensure Managed Identity is enabled on your Azure VM.
5252

53-
### Acquiring an mTLS PoP Token
53+
### Path 2 — Managed Identity: Acquiring an mTLS PoP Token
5454

5555
Acquiring a token follows this general pattern:
5656

@@ -63,7 +63,7 @@ Acquiring a token follows this general pattern:
6363

6464
MtlsMsiClient client = new MtlsMsiClient();
6565
MtlsMsiHelperResult result = client.acquireToken(
66-
"https://management.azure.com", // resource
66+
"https://graph.microsoft.com", // resource (confirmed enrolled for mTLS PoP)
6767
"SystemAssigned", // identity type
6868
null, // identity id (null for system-assigned)
6969
false, // withAttestation — set true on Trusted Launch VMs
@@ -76,7 +76,7 @@ Acquiring a token follows this general pattern:
7676

7777
```java
7878
MtlsMsiHelperResult result = client.acquireToken(
79-
"https://management.azure.com",
79+
"https://graph.microsoft.com",
8080
"UserAssigned",
8181
"your-client-id",
8282
false,
@@ -85,9 +85,11 @@ Acquiring a token follows this general pattern:
8585
String accessToken = result.getAccessToken();
8686
```
8787

88+
> **Resource note:** Use `https://graph.microsoft.com` or `https://storage.azure.com`. `https://management.azure.com` may return `AADSTS392196` if that resource is not enrolled for mTLS PoP in your tenant.
89+
8890
2. The binding certificate is cached in-process for the lifetime of the IMDS-issued certificate (minus a 5-minute safety margin). Subsequent calls return the cached token until it nears expiry.
8991

90-
### Making Downstream mTLS Calls
92+
### Path 2Making Downstream mTLS Calls
9193

9294
Once you have a token, use `httpRequest()` to make downstream calls over the same KeyGuard-backed mTLS channel:
9395

@@ -99,7 +101,7 @@ MtlsMsiHttpResponse response = client.httpRequest(
99101
null, // body
100102
null, // contentType
101103
null, // extra headers
102-
"https://management.azure.com", // resource (for cert refresh)
104+
"https://graph.microsoft.com", // resource (for cert refresh)
103105
"SystemAssigned", null, // identity type, identity id
104106
false, // withAttestation
105107
null, // correlationId
@@ -109,7 +111,65 @@ System.out.println(response.getStatus()); // e.g. 200
109111
System.out.println(response.getBody());
110112
```
111113

112-
The downstream server must be configured to *require* mutual TLS — it must send a TLS `CertificateRequest` during the handshake. Public Azure APIs (Graph, Key Vault, etc.) do not require a client certificate.
114+
The downstream server must be configured to *require* mutual TLS — it must send a TLS `CertificateRequest` during the handshake.
115+
116+
### Path 1 — Confidential Client (SNI Certificate)
117+
118+
For applications with an SNI certificate (e.g., from OneCert/DSMS), use `ConfidentialClientApplication` from the core `msal4j` library:
119+
120+
```java
121+
import com.microsoft.aad.msal4j.*;
122+
import java.io.FileInputStream;
123+
124+
// 1. Load your certificate (PKCS12)
125+
IClientCertificate cert = ClientCredentialFactory.createFromCertificate(
126+
new FileInputStream("cert.p12"), "password");
127+
128+
// 2. Build the app — tenanted authority and region required
129+
ConfidentialClientApplication app = ConfidentialClientApplication
130+
.builder("your-client-id", cert)
131+
.authority("https://login.microsoftonline.com/your-tenant-id")
132+
.azureRegion("centraluseuap")
133+
.build();
134+
135+
// 3. Acquire an mTLS PoP token
136+
IAuthenticationResult result = app.acquireToken(
137+
ClientCredentialParameters
138+
.builder(Collections.singleton("https://graph.microsoft.com/.default"))
139+
.withMtlsProofOfPossession()
140+
.build()
141+
).get();
142+
143+
System.out.println("Token type: " + result.tokenType()); // "mtls_pop"
144+
System.out.println("Binding cert: " + result.bindingCertificate().getSubjectX500Principal());
145+
System.out.println("Access token: " + result.accessToken());
146+
```
147+
148+
**Requirements:** Certificate credential, tenanted authority (not `/common` or `/organizations`), Azure region.
149+
150+
---
151+
152+
## End-to-End Test Driver
153+
154+
The `msal4j-mtls-extensions` module ships an e2e fat JAR for manual testing:
155+
156+
```powershell
157+
# Build
158+
mvn package -DskipTests
159+
160+
# Path 1 — error-case validation (no Azure credentials required)
161+
java -jar target\msal4j-mtls-extensions-1.0.0-e2e.jar path1 --errors-only
162+
163+
# Path 1 — full happy path
164+
java -jar target\msal4j-mtls-extensions-1.0.0-e2e.jar path1 `
165+
--tenant <tenantID> --client <clientID> --region centraluseuap
166+
167+
# Path 2 — Managed Identity (with attestation)
168+
java -Djava.library.path=C:\msiv2 `
169+
-jar target\msal4j-mtls-extensions-1.0.0-e2e.jar path2 --attest
170+
```
171+
172+
See [Manual Testing Guide](../../msal4j-sdk/docs/mtls-pop-manual-testing.md) for full instructions.
113173

114174
## Community Help and Support
115175

msal4j-sdk/docs/mtls-pop-manual-testing.md

Lines changed: 88 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ This guide walks through manual verification of both mTLS PoP paths in MSAL4J.
1010
- For SNI path: a valid test certificate (PKCS12)
1111
- For Managed Identity path:
1212
- An Azure VM with managed identity enabled
13+
- Windows x64 OS with VBS (Virtualization-Based Security) KeyGuard
1314
- `msal4j-mtls-extensions` on classpath (add dependency)
14-
- .NET 8 runtime installed on the VM
15+
- On Trusted Launch VMs: `AttestationClientLib.dll` on `PATH`
1516
- An AAD tenant with a registered app (client credentials configured)
1617

1718
---
@@ -145,119 +146,120 @@ Decode the access token (base64url decode the middle JWT segment) and verify:
145146
### Prerequisites
146147

147148
- Azure VM with managed identity enabled (System-assigned or User-assigned)
148-
- `msal4j-mtls-extensions` JAR on classpath
149-
- .NET 8 runtime: `dotnet --version` should print `8.x.x`
150-
- IMDS accessible: `curl http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=...`
149+
- Windows x64 OS with VBS (Virtualization-Based Security) KeyGuard
150+
- `msal4j-mtls-extensions` JAR on classpath (or use the pre-built fat JAR)
151+
- On Trusted Launch VMs: `AttestationClientLib.dll` on `PATH` or application directory
152+
- No .NET runtime required — the extension calls CNG directly via JNA
151153

152-
### 1. Smoke-test the .NET helper binary
153-
154-
Locate the helper (bundled in the `msal4j-mtls-extensions` JAR or at `MSAL_MTLS_HELPER_PATH`):
154+
### 1. Build the e2e fat JAR
155155

156156
```bash
157-
# If using env override:
158-
export MSAL_MTLS_HELPER_PATH=/path/to/MsalMtlsMsiHelper.exe
159-
160-
# Smoke test - acquire token for ARM resource
161-
./MsalMtlsMsiHelper.exe \
162-
--mode acquire-token \
163-
--resource https://management.azure.com/ \
164-
--identity-type SystemAssigned
157+
cd msal4j-mtls-extensions
158+
mvn package -DskipTests
159+
# Produces: target/msal4j-mtls-extensions-1.0.0-e2e.jar
165160
```
166161

167-
Expected stdout (JSON):
168-
```json
169-
{
170-
"access_token": "eyJ0...",
171-
"token_type": "mtls_pop",
172-
"expires_on": 1234567890,
173-
"thumbprint": "abc123..."
174-
}
175-
```
176-
177-
### 2. Java test program
178-
179-
```java
180-
import com.microsoft.aad.msal4j.*;
181-
import java.util.*;
182-
183-
public class TestMtlsMsi {
184-
public static void main(String[] args) throws Exception {
185-
ManagedIdentityApplication app = ManagedIdentityApplication
186-
.builder(ManagedIdentityId.systemAssigned())
187-
.build();
162+
### 2. Run Path 2 (Managed Identity)
188163

189-
ManagedIdentityParameters params = ManagedIdentityParameters
190-
.builder("https://management.azure.com/")
191-
.withMtlsProofOfPossession(true)
192-
.build();
193-
194-
IAuthenticationResult result = app.acquireTokenForManagedIdentity(params).get();
164+
```powershell
165+
# Basic (no attestation — works on standard VMs)
166+
java -jar target\msal4j-mtls-extensions-1.0.0-e2e.jar path2
195167
196-
System.out.println("=== SUCCESS ===");
197-
System.out.println("Token type: " + result.tokenType());
198-
System.out.println("Expires: " + result.expiresOnDate());
199-
System.out.println("Token: " + result.accessToken().substring(0, 40) + "...");
200-
}
201-
}
168+
# With attestation (Trusted Launch VMs with AttestationClientLib.dll)
169+
java -Djava.library.path=C:\msiv2 -jar target\msal4j-mtls-extensions-1.0.0-e2e.jar path2 --attest
202170
```
203171

204-
### 3. With attestation
172+
### 3. Expected output
205173

206-
```java
207-
ManagedIdentityParameters params = ManagedIdentityParameters
208-
.builder("https://management.azure.com/")
209-
.withMtlsProofOfPossession(true)
210-
.withAttestation(true)
211-
.build();
174+
```
175+
=== Path 2: Managed Identity mTLS PoP ===
176+
177+
Acquiring mTLS PoP token via IMDSv2 (full flow)...
178+
179+
[First call (from IMDS)]
180+
✅ BindingCertificate present
181+
Subject: CN=<client-id>,DC=<tenant-id>
182+
Issuer: CN=managedidentitysnissuer.login.microsoft.com
183+
NotBefore: ...
184+
NotAfter: ... (14 days)
185+
TokenType: mtls_pop
186+
ExpiresIn: 86399s
187+
AccessToken cnf: {"x5t#S256":"<thumbprint>"}
188+
✅ AccessToken present
189+
190+
Acquiring again (expect cert cache hit)...
191+
[Second call (should be cert-cached, ~fast)]
192+
✅ Binding cert cache working: same cert on second call
193+
⏱ Elapsed: ~60ms
194+
195+
Making downstream mTLS call to graph.microsoft.com...
196+
Downstream HTTP status: 401
197+
✅ TLS handshake + token delivery succeeded (HTTP < 500)
198+
ℹ️ 401 — TLS OK, authorization depends on permissions
199+
200+
=== Path 2 Complete ===
212201
```
213202

214-
Attestation requires:
215-
- Azure VM with vTPM or Trusted Launch enabled
216-
- MAA (Microsoft Azure Attestation) service accessible from the VM
203+
> **Expected HTTP 401 from graph.microsoft.com:** This is correct behavior. The TLS handshake and token were accepted — the managed identity simply has no Graph role assigned. HTTP 401 confirms the mTLS PoP flow succeeded end-to-end.
217204
218-
### 4. User-assigned managed identity
205+
### 4. Java API
219206

220207
```java
221-
// By client ID
222-
ManagedIdentityApplication app = ManagedIdentityApplication
223-
.builder(ManagedIdentityId.userAssignedClientId("your-client-id"))
224-
.build();
225-
226-
// By object ID
227-
ManagedIdentityApplication app2 = ManagedIdentityApplication
228-
.builder(ManagedIdentityId.userAssignedObjectId("your-object-id"))
229-
.build();
208+
import com.microsoft.aad.msal4j.mtls.*;
230209

231-
// By resource ID
232-
ManagedIdentityApplication app3 = ManagedIdentityApplication
233-
.builder(ManagedIdentityId.userAssignedResourceId("/subscriptions/.../resourceGroups/.../providers/..."))
234-
.build();
210+
MtlsMsiClient client = new MtlsMsiClient();
211+
MtlsMsiHelperResult result = client.acquireToken(
212+
"https://graph.microsoft.com", // resource (graph.microsoft.com confirmed enrolled)
213+
"SystemAssigned", // identity type
214+
null, // identity id (null for system-assigned)
215+
false, // withAttestation — set true on Trusted Launch VMs
216+
null // correlationId (optional)
217+
);
218+
219+
String accessToken = result.getAccessToken();
220+
String certPem = result.getBindingCertificate();
235221
```
236222

237-
### 5. End-to-end test: making an mTLS-required HTTP request
223+
> **Resource note:** Use `https://graph.microsoft.com` or `https://storage.azure.com` for testing.
224+
> `https://management.azure.com` may return `AADSTS392196` if the resource is not enrolled for mTLS PoP in your tenant.
238225
239-
Use the result from step 2 to make a request to a resource server that enforces mTLS:
226+
### 5. Verify token claims
240227

241-
```java
242-
// After acquiring the token, use MtlsMsiClient directly for mTLS-authenticated HTTP calls
243-
// (this requires msal4j-mtls-extensions on classpath)
244-
import com.microsoft.aad.msal4j.mtls.MtlsMsiClient;
228+
Decode the JWT payload and confirm:
245229

246-
MtlsMsiClient client = new MtlsMsiClient();
247-
// Use the token to call a downstream API via mTLS
248-
// The helper binary handles the mTLS transport
230+
```powershell
231+
$token = "<access-token>"
232+
$parts = $token -split "\."
233+
[System.Text.Encoding]::UTF8.GetString(
234+
[System.Convert]::FromBase64String(
235+
$parts[1].PadRight($parts[1].Length + (4 - $parts[1].Length % 4) % 4, '='))) |
236+
ConvertFrom-Json
249237
```
250238

239+
Expected claims:
240+
```json
241+
{
242+
"cnf": { "x5t#S256": "<thumbprint matching binding cert>" },
243+
"xms_tbflags": 2,
244+
"appidacr": "2",
245+
"aud": "https://graph.microsoft.com",
246+
"idtyp": "app",
247+
"app_displayname": "<your VM's managed identity name>"
248+
}
249+
```
250+
251+
The `cnf.x5t#S256` thumbprint must match the binding certificate returned by `result.getBindingCertificate()`.
252+
251253
### Troubleshooting
252254

253255
| Symptom | Likely Cause | Fix |
254256
|---------|-------------|-----|
255-
| `msal4j-mtls-extensions not on classpath` | Missing dependency | Add `msal4j-mtls-extensions` to pom.xml |
256-
| Helper not found | No exe or env var not set | Set `MSAL_MTLS_HELPER_PATH` or include extensions JAR |
257-
| `.NET runtime not found` | .NET 8 not installed | `sudo apt install dotnet-runtime-8.0` or Windows installer |
257+
| `VBS KeyGuard not available` | Credential Guard not enabled | Enable VBS/Credential Guard and reboot |
258+
| `AttestationClientLib.dll not found` | DLL not on PATH | Copy DLL from NuGet package to application directory |
259+
| `HTTP 400 from IMDS issuecredential` | Attestation token empty | Check DLL is present; VM must be Trusted Launch |
260+
| `AADSTS392196` | Resource not enrolled for mTLS PoP | Use `https://graph.microsoft.com` instead |
258261
| `IMDS not accessible` | Not running on Azure VM | This path only works in Azure managed identity environments |
259-
| Helper exits with non-zero | See stderr JSON `error_description` | Check IMDS logs, managed identity config, network rules |
260-
| Attestation failure | VM doesn't support vTPM | Use `withAttestation(false)` or enable Trusted Launch |
262+
| `NCryptFinalizeKey NTE_BAD_FLAGS` | VBS not running | Check `msinfo32.exe` → Virtualization-based security must show "Running" |
261263

262264
---
263265

0 commit comments

Comments
 (0)