Skip to content

Commit 8170699

Browse files
authored
Merge pull request #112884 from franciscopgomes/patch-6
Update jwt-claims-customization.md
2 parents 88761b8 + 9cd3112 commit 8170699

File tree

1 file changed

+282
-0
lines changed

1 file changed

+282
-0
lines changed

articles/active-directory/develop/jwt-claims-customization.md

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,288 @@ As another example, consider when Britta Simon tries to sign in using the follow
195195

196196
As a final example, consider what happens if Britta has no `user.othermail` configured or it's empty. The claim falls back to `user.extensionattribute1` ignoring the condition entry in both cases.
197197

198+
## Security considerations
199+
Applications that receive tokens rely on claim values that are authoritatively issued by Azure AD and can't be tampered with. When you modify the token contents through claims customization, these assumptions may no longer be correct. Applications must explicitly acknowledge that tokens have been modified by the creator of the customization to protect themselves from customizations created by malicious actors. This can be done in one the following ways:
200+
201+
- [Configure a custom signing key](#configure-a-custom-signing-key)
202+
- [update the application manifest to accept mapped claims](#update-the-application-manifest).
203+
204+
Without this, Azure AD returns an [AADSTS50146 error code](reference-aadsts-error-codes.md#aadsts-error-codes).
205+
206+
## Configure a custom signing key
207+
For multi-tenant apps, a custom signing key should be used. Don't set `acceptMappedClaims` in the app manifest. when setting up an app in the Azure portal, you get an app registration object and a service principal in your tenant. That app is using the Azure global sign-in key, which can't be used for customizing claims in tokens. To get custom claims in tokens, create a custom sign-in key from a certificate and add it to service principal. For testing purposes, you can use a self-signed certificate. After configuring the custom signing key, your application code needs to validate the token signing key.
208+
209+
Add the following information to the service principal:
210+
211+
- Private key (as a [key credential](/graph/api/resources/keycredential?view=graph-rest-1.0&preserve-view=true))
212+
- Password (as a [password credential](/graph/api/resources/passwordcredential?view=graph-rest-1.0&preserve-view=true))
213+
- Public key (as a [key credential](/graph/api/resources/keycredential?view=graph-rest-1.0&preserve-view=true))
214+
215+
Extract the private and public key base-64 encoded from the PFX file export of your certificate. Make sure that the `keyId` for the `keyCredential` used for "Sign" matches the `keyId` of the `passwordCredential`. You can generate the `customkeyIdentifier` by getting the hash of the cert's thumbprint.
216+
217+
## Request
218+
The following example shows the format of the HTTP PATCH request to add a custom signing key to a service principal. The "key" value in the `keyCredentials` property is shortened for readability. The value is base-64 encoded. For the private key, the property usage is "Sign". For the public key, the property usage is "Verify".
219+
220+
```
221+
PATCH https://graph.microsoft.com/v1.0/servicePrincipals/f47a6776-bca7-4f2e-bc6c-eec59d058e3e
222+
223+
Content-type: servicePrincipals/json
224+
Authorization: Bearer {token}
225+
226+
{
227+
"keyCredentials":[
228+
{
229+
"customKeyIdentifier": "lY85bR8r6yWTW6jnciNEONwlVhDyiQjdVLgPDnkI5mA=",
230+
"endDateTime": "2021-04-22T22:10:13Z",
231+
"keyId": "4c266507-3e74-4b91-aeba-18a25b450f6e",
232+
"startDateTime": "2020-04-22T21:50:13Z",
233+
"type": "X509CertAndPassword",
234+
"usage": "Sign",
235+
"key":"MIIKIAIBAz.....HBgUrDgMCERE20nuTptI9MEFCh2Ih2jaaLZBZGeZBRFVNXeZmAAgIH0A==",
236+
"displayName": "CN=contoso"
237+
},
238+
{
239+
"customKeyIdentifier": "lY85bR8r6yWTW6jnciNEONwlVhDyiQjdVLgPDnkI5mA=",
240+
"endDateTime": "2021-04-22T22:10:13Z",
241+
"keyId": "e35a7d11-fef0-49ad-9f3e-aacbe0a42c42",
242+
"startDateTime": "2020-04-22T21:50:13Z",
243+
"type": "AsymmetricX509Cert",
244+
"usage": "Verify",
245+
"key": "MIIDJzCCAg+gAw......CTxQvJ/zN3bafeesMSueR83hlCSyg==",
246+
"displayName": "CN=contoso"
247+
}
248+
249+
],
250+
"passwordCredentials": [
251+
{
252+
"customKeyIdentifier": "lY85bR8r6yWTW6jnciNEONwlVhDyiQjdVLgPDnkI5mA=",
253+
"keyId": "4c266507-3e74-4b91-aeba-18a25b450f6e",
254+
"endDateTime": "2022-01-27T19:40:33Z",
255+
"startDateTime": "2020-04-20T19:40:33Z",
256+
"secretText": "mypassword"
257+
}
258+
]
259+
}
260+
```
261+
262+
## Configure a custom signing key using PowerShell
263+
Use PowerShell to [instantiate an MSAL Public Client Application](msal-net-initializing-client-applications.md#initializing-a-public-client-application-from-code) and use the [Authorization Code Grant](v2-oauth2-auth-code-flow.md) flow to obtain a delegated permission access token for Microsoft Graph. Use the access token to call Microsoft Graph and configure a custom signing key for the service principal. After configuring the custom signing key, your application code needs to [validate the token signing key](#validate-token-signing-key).
264+
265+
To run this script you need:
266+
267+
- The object ID of your application's service principal, found in the Overview blade of your application's entry in Enterprise Applications in the Azure portal.
268+
- An app registration to sign in a user and get an access token to call Microsoft Graph. Get the application (client) ID of this app in the Overview blade of the application's entry in App registrations in the Azure portal. The app registration should have the following configuration:
269+
- A redirect URI of "http://localhost" listed in the **Mobile and desktop applications** platform configuration.
270+
- In **API permissions**, Microsoft Graph delegated permissions **Application.ReadWrite.All** and **User.Read** (make sure you grant Admin consent to these permissions).
271+
- A user who logs in to get the Microsoft Graph access token. The user should be one of the following Azure AD administrative roles (required to update the service principal):
272+
- Cloud Application Administrator
273+
- Application Administrator
274+
- Global Administrator
275+
- A certificate to configure as a custom signing key for our application. You can either create a self-signed certificate or obtain one from your trusted certificate authority. The following certificate components are used in the script:
276+
- public key (typically a .cer file)
277+
- private key in PKCS#12 format (in .pfx file)
278+
- password for the private key (pfx file)
279+
280+
> [!IMPORTANT]
281+
> The private key must be in PKCS#12 format since Azure AD doesn't support other format types. Using the wrong format can result in the error "Invalid certificate: Key value is invalid certificate" when using Microsoft Graph to PATCH the service principal with a `keyCredentials` containing the certificate information.
282+
283+
```
284+
$fqdn="fourthcoffeetest.onmicrosoft.com" # this is used for the 'issued to' and 'issued by' field of the certificate
285+
$pwd="mypassword" # password for exporting the certificate private key
286+
$location="C:\\temp" # path to folder where both the pfx and cer file will be written to
287+
288+
# Create a self-signed cert
289+
$cert = New-SelfSignedCertificate -certstorelocation cert:\currentuser\my -DnsName $fqdn
290+
$pwdSecure = ConvertTo-SecureString -String $pwd -Force -AsPlainText
291+
$path = 'cert:\currentuser\my\' + $cert.Thumbprint
292+
$cerFile = $location + "\\" + $fqdn + ".cer"
293+
$pfxFile = $location + "\\" + $fqdn + ".pfx"
294+
295+
# Export the public and private keys
296+
Export-PfxCertificate -cert $path -FilePath $pfxFile -Password $pwdSecure
297+
Export-Certificate -cert $path -FilePath $cerFile
298+
299+
$ClientID = "<app-id>"
300+
$loginURL = "https://login.microsoftonline.com"
301+
$tenantdomain = "fourthcoffeetest.onmicrosoft.com"
302+
$redirectURL = "http://localhost" # this reply URL is needed for PowerShell Core
303+
[string[]] $Scopes = "https://graph.microsoft.com/.default"
304+
$pfxpath = $pfxFile # path to pfx file
305+
$cerpath = $cerFile # path to cer file
306+
$SPOID = "<service-principal-id>"
307+
$graphuri = "https://graph.microsoft.com/v1.0/serviceprincipals/$SPOID"
308+
$password = $pwd # password for the pfx file
309+
310+
311+
# choose the correct folder name for MSAL based on PowerShell version 5.1 (.Net) or PowerShell Core (.Net Core)
312+
313+
if ($PSVersionTable.PSVersion.Major -gt 5)
314+
{
315+
$core = $true
316+
$foldername = "netcoreapp2.1"
317+
}
318+
else
319+
{
320+
$core = $false
321+
$foldername = "net45"
322+
}
323+
324+
# Load the MSAL/microsoft.identity/client assembly -- needed once per PowerShell session
325+
[System.Reflection.Assembly]::LoadFrom((Get-ChildItem C:/Users/<username>/.nuget/packages/microsoft.identity.client/4.32.1/lib/$foldername/Microsoft.Identity.Client.dll).fullname) | out-null
326+
327+
$global:app = $null
328+
329+
$ClientApplicationBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientID)
330+
[void]$ClientApplicationBuilder.WithAuthority($("$loginURL/$tenantdomain"))
331+
[void]$ClientApplicationBuilder.WithRedirectUri($redirectURL)
332+
333+
$global:app = $ClientApplicationBuilder.Build()
334+
335+
Function Get-GraphAccessTokenFromMSAL {
336+
[Microsoft.Identity.Client.AuthenticationResult] $authResult = $null
337+
$AquireTokenParameters = $global:app.AcquireTokenInteractive($Scopes)
338+
[IntPtr] $ParentWindow = [System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle
339+
if ($ParentWindow)
340+
{
341+
[void]$AquireTokenParameters.WithParentActivityOrWindow($ParentWindow)
342+
}
343+
try {
344+
$authResult = $AquireTokenParameters.ExecuteAsync().GetAwaiter().GetResult()
345+
}
346+
catch {
347+
$ErrorMessage = $_.Exception.Message
348+
Write-Host $ErrorMessage
349+
}
350+
351+
return $authResult
352+
}
353+
354+
$myvar = Get-GraphAccessTokenFromMSAL
355+
if ($myvar)
356+
{
357+
$GraphAccessToken = $myvar.AccessToken
358+
Write-Host "Access Token: " $myvar.AccessToken
359+
#$GraphAccessToken = "eyJ0eXAiOiJKV1QiL ... iPxstltKQ"
360+
361+
362+
# this is for PowerShell Core
363+
$Secure_String_Pwd = ConvertTo-SecureString $password -AsPlainText -Force
364+
365+
# reading certificate files and creating Certificate Object
366+
if ($core)
367+
{
368+
$pfx_cert = get-content $pfxpath -AsByteStream -Raw
369+
$cer_cert = get-content $cerpath -AsByteStream -Raw
370+
$cert = Get-PfxCertificate -FilePath $pfxpath -Password $Secure_String_Pwd
371+
}
372+
else
373+
{
374+
$pfx_cert = get-content $pfxpath -Encoding Byte
375+
$cer_cert = get-content $cerpath -Encoding Byte
376+
# Write-Host "Enter password for the pfx file..."
377+
# calling Get-PfxCertificate in PowerShell 5.1 prompts for password
378+
# $cert = Get-PfxCertificate -FilePath $pfxpath
379+
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pfxpath, $password)
380+
}
381+
382+
# base 64 encode the private key and public key
383+
$base64pfx = [System.Convert]::ToBase64String($pfx_cert)
384+
$base64cer = [System.Convert]::ToBase64String($cer_cert)
385+
386+
# getting id for the keyCredential object
387+
$guid1 = New-Guid
388+
$guid2 = New-Guid
389+
390+
# get the custom key identifier from the certificate thumbprint:
391+
$hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256')
392+
$hash = $hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($cert.Thumbprint))
393+
$customKeyIdentifier = [System.Convert]::ToBase64String($hash)
394+
395+
# get end date and start date for our keycredentials
396+
$endDateTime = ($cert.NotAfter).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" )
397+
$startDateTime = ($cert.NotBefore).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" )
398+
399+
# building our json payload
400+
$object = [ordered]@{
401+
keyCredentials = @(
402+
[ordered]@{
403+
customKeyIdentifier = $customKeyIdentifier
404+
endDateTime = $endDateTime
405+
keyId = $guid1
406+
startDateTime = $startDateTime
407+
type = "X509CertAndPassword"
408+
usage = "Sign"
409+
key = $base64pfx
410+
displayName = "CN=fourthcoffeetest"
411+
},
412+
[ordered]@{
413+
customKeyIdentifier = $customKeyIdentifier
414+
endDateTime = $endDateTime
415+
keyId = $guid2
416+
startDateTime = $startDateTime
417+
type = "AsymmetricX509Cert"
418+
usage = "Verify"
419+
key = $base64cer
420+
displayName = "CN=fourthcoffeetest"
421+
}
422+
)
423+
passwordCredentials = @(
424+
[ordered]@{
425+
customKeyIdentifier = $customKeyIdentifier
426+
keyId = $guid1
427+
endDateTime = $endDateTime
428+
startDateTime = $startDateTime
429+
secretText = $password
430+
}
431+
)
432+
}
433+
434+
$json = $object | ConvertTo-Json -Depth 99
435+
Write-Host "JSON Payload:"
436+
Write-Output $json
437+
438+
# Request Header
439+
$Header = @{}
440+
$Header.Add("Authorization","Bearer $($GraphAccessToken)")
441+
$Header.Add("Content-Type","application/json")
442+
443+
try
444+
{
445+
Invoke-RestMethod -Uri $graphuri -Method "PATCH" -Headers $Header -Body $json
446+
}
447+
catch
448+
{
449+
# Dig into the exception to get the Response details.
450+
# Note that value__ is not a typo.
451+
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
452+
Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
453+
}
454+
455+
Write-Host "Complete Request"
456+
}
457+
else
458+
{
459+
Write-Host "Fail to get Access Token"
460+
}
461+
```
462+
463+
## Validate token signing key
464+
Apps that have claims mapping enabled must validate their token signing keys by appending `appid={client_id}` to their [OpenID Connect metadata requests](v2-protocols-oidc.md#fetch-the-openid-configuration-document). The following example shows the format of the OpenID Connect metadata document you should use:
465+
466+
```
467+
https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration?appid={client-id}
468+
```
469+
470+
## Update the application manifest
471+
For single tenant apps, you can set the `acceptMappedClaims` property to `true` in the [application manifest](reference-app-manifest.md). As documented on the [apiApplication resource type](/graph/api/resources/apiapplication?view=graph-rest-1.0&preserve-view=true#properties), this allows an application to use claims mapping without specifying a custom signing key.
472+
473+
>[!WARNING]
474+
>Do not set the acceptMappedClaims property to true for multi-tenant apps, which can allow malicious actors to create claims-mapping policies for your app.
475+
476+
The requested token audience is required to use a verified domain name of your Azure AD tenant, which means you should set the `Application ID URI` (represented by the `identifierUris` in the application manifest) for example to `https://contoso.com/my-api` or (simply using the default tenant name) `https://contoso.onmicrosoft.com/my-api`.
477+
478+
If you're not using a verified domain, Azure AD returns an `AADSTS501461` error code with message "_AcceptMappedClaims is only supported for a token audience matching the application GUID or an audience within the tenant's verified domains. Either change the resource identifier or use an application-specific signing key."
479+
198480
## Advanced claims options
199481

200482
Configure advanced claims options for OIDC applications to expose the same claim as SAML tokens. Also for applications that intend to use the same claim for both SAML2.0 and OIDC response tokens.

0 commit comments

Comments
 (0)