Sample app demonstrating how to use Azure AD B2C with MitID to validate a citizens civil registration number (colloquially: CPR number) and write back a custom attribute in the corporate Azure AD.
This is done to be NSIS compliant.
- Use MitID to ensure a given user has validated her civil registration number (this sample).
- Use MitID to enable a disabled account.
- Use MitID to perform a password reset.
To enable MitID with Azure AD B2C, you need to use a MitID broker.
This sample currently supports Criipto but more brokers may be added in the future. If you manage to make it work with another broker, do sends us a PR.
-
Create an Azure AD B2C tenant using the Azure Portal.
-
Deploy AAD B2C Custom Policies (check Remove Facebook references).
-
Find the newly created App Registration (IEF Test App):
- Go to Authentication and click the link to migrate URIs.
- Under Implicit grant and hybrid flows uncheck ID tokens (used for implicit and hybrid flows)
- Go to API permissions and click [
Grant admin consent for <your-azure-ad-b2c-tenant-name>
].
-
Create an account on Criipto and create an Application:
- Callback url;
https://<your-azure-ad-b2c-tenant-name>.b2clogin.com/<your-azure-ad-b2c-tenant-name>.onmicrosoft.com/oauth2/authresp
. - e-IDs:
DK MitID
only.
Capture client id and client secret. Store the client secret under policy keys as
CriiptoClientSecret
. - Callback url;
-
For production: All users should have their civil registration number (ten digits, no dash) stored in Azure AD (value to be hashed using SHA256 and subsequently base64 encoded).
Source from https://sequencediagram.org/:
title Flow
User->App:Initiate sign in
App->Azure AD B2C:Redirect to Azure AD B2C
Azure AD B2C->MitID Broker:Redirect to MitID Broker to sign in with MitID
MitID Broker->Azure AD B2C:Return claims with uuid, name, and cprNumberIdentifier
Azure AD B2C->Azure AD B2C: Translate claims
Azure AD B2C->Azure Function:Call Azure Function with issuerUserId, displayName, and civilRegistrationNumber
Azure Function->Azure AD:Lookup user on hashed civilRegistrationNumber
Azure Function->Azure AD:If found, update user with civilRegistrationNumberValidated (UTC Now)
Azure Function->Azure AD B2C:Return user with id, displayName, accountEnabled, and civilRegistrationNumberValidated
Azure AD B2C->App:Return claims
App->User:Signed in
- Clone code
- Search and replace all instances of
ondfiskb2c
with<your-azure-ad-b2c-tenant-name>
- Search and replace all instances of
ondfisk
with<your-azure-ad-tenant-name>
#!/bin/bash
SUBSCRIPTION_ID="2b554ca5-5009-4849-9b8b-730a9820de6a"
RESOURCE_GROUP_NAME="ondfiskb2c"
LOCATION="swedencentral"
FUNCTION_APP_NAME="ondfiskb2c"
# Set subscription
az account set --subscription $SUBSCRIPTION_ID
# Create resource group
az group create --name $RESOURCE_GROUP_NAME --location $LOCATION
# Deploy resources
az deployment group create \
--resource-group $RESOURCE_GROUP_NAME \
--template-file "infrastructure/azuredeploy.bicep" \
--parameters "infrastructure/azuredeploy.bicepparam"
Capture functionAppManagedIdentity
:
functionAppManagedIdentity
:4c8f6136-3449-4196-a0e9-393175027044
#!/bin/bash
APP_ROLE="User.ReadWrite.All"
MANAGED_IDENTITY=$(az functionapp identity show --resource-group $RESOURCE_GROUP_NAME --name $FUNCTION_APP_NAME --query principalId --output tsv)
MICROSOFT_GRAPH=$(az rest --method GET --uri "https://graph.microsoft.com/v1.0/servicePrincipals?\$filter=displayName+eq+'Microsoft+Graph'" --query "value[].id" --output tsv)
APP_ROLE_ID=$(az rest --method GET --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$MICROSOFT_GRAPH" --query "appRoles[?value=='$APP_ROLE'].id" --output tsv)
CURRENT=$(az rest --method GET --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$MANAGED_IDENTITY/appRoleAssignments" --query "value[?appRoleId=='$APP_ROLE_ID'].id" --output tsv)
if [ -z $CURRENT ]; then
az rest --method POST --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$MANAGED_IDENTITY/appRoleAssignments" --body "{\"principalId\":\"$MANAGED_IDENTITY\",\"resourceId\":\"$MICROSOFT_GRAPH\",\"appRoleId\":\"$APP_ROLE_ID\"}"
fi
POST https://graph.microsoft.com/v1.0/users
Content-type: application/json
{
"accountEnabled": true,
"displayName": "Åge Petersen",
"givenName": "Åge",
"surname": "Petersen",
"mailNickname": "aagep",
"passwordProfile": {
"forceChangePasswordNextSignIn": true,
"forceChangePasswordNextSignInWithMfa": false,
"password": "..."
},
"userPrincipalName": "aagep@ondfisk.dk",
"jobTitle": "Senior Tester"
}
Capture id
:
id
:e110ccf5-f660-4777-ace0-ed9c56f04981
POST https://graph.microsoft.com/v1.0/applications/
Content-type: application/json
{
"displayName": "ondfisk extensions app - DO NOT DELETE",
"description": "ondfisk extensions app. Reserved for directory extensions. DO NOT DELETE. Used by ondfisk for storing user data.",
"signInAudience": "AzureADMyOrg",
"tags": [
"Directory extensions",
"Extensions",
"Extension attributes"
]
}
Capture id
and appId
id
:c45cf23e-e189-4b24-95d7-8814c4d09736
appId
:1be97e58-6e49-44ee-a17a-42fd1fc944cf
POST https://graph.microsoft.com/v1.0/servicePrincipals
Content-type: application/json
{
"appId": "1be97e58-6e49-44ee-a17a-42fd1fc944cf"
}
POST https://graph.microsoft.com/v1.0/applications/c45cf23e-e189-4b24-95d7-8814c4d09736/extensionProperties
Content-type: application/json
{
"name": "civilRegistrationNumber",
"dataType": "String",
"targetObjects": [
"User"
]
}
POST https://graph.microsoft.com/v1.0/applications/c45cf23e-e189-4b24-95d7-8814c4d09736/extensionProperties
Content-type: application/json
{
"name": "civilRegistrationNumberValidated",
"dataType": "DateTime",
"targetObjects": [
"User"
]
}
$civilRegistrationNumber = "0905540335"
$bytes = [System.Text.Encoding]::UTF8.GetBytes($civilRegistrationNumber)
$hash = [System.Security.Cryptography.SHA256]::HashData($bytes)
$base64 = [System.Convert]::ToBase64String($hash)
$base64
or
#!/bin/bash
civilRegistrationNumber="0905540335"
hash=$(echo -n $civilRegistrationNumber | sha256sum | awk '{print $1}')
base64=$(echo -n $hash | base64)
echo $base64
PATCH https://graph.microsoft.com/v1.0/users/e110ccf5-f660-4777-ace0-ed9c56f04981
Content-type: application/json
{
"extension_1be97e586e4944eea17a42fd1fc944cf_civilRegistrationNumber": "yqaVIggNuNCpgvScLH9GdX0gB3LMo+LUuGc8Jv+bxSc="
}
Deploy resources:
./infrastructure/deploy.sh
Under /src/Ondfisk.B2C.Functions
, create a local.settings.json
:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsFeatureFlags": "EnableHttpProxying"
}
}
Deploy /src/Ondfisk.B2C.Functions
.
Capture the default function key from the ValidateUser
function.
Note: You may want consider using a client credential flow instead of function keys.
-
Add signing and encryption keys for Identity Experience Framework applications
-
Configure App Registration:
- Name:
jwt.ms
- Supported account types:
Accounts in any identity provider or organizational directory (for authenticating users with user flows)
- Redirect URI (recommended):
Single-page application (SPA)
https://jwt.ms
- Permissions: [
X
] Grant admin consent to openid and offline_access permissions - Authentication/Implicit grant and hybrid flows:
[X]
Access tokens (used for implicit flows)
- Name:
Using your newly created Application Insights resource follow this guide:
Collect Azure Active Directory B2C logs with Application Insights
Note: Remember to change DeploymentMode="Development"
and DeveloperMode="true"
before moving to production.
Using the data from your Criipto App Registration:
- Update
policies/CriiptoExtensions.xml
with client id - Create policy key
FunctionsKey
with function key
Upload /policies
.
You should now be able to test your app registration using:
- Username: Åge29164
- Password: ZXzx11^x
- CPR number: 0905540335