Entra Verifiable credentials Admin API with PowerShell

Entra Verifiable credentials Admin API with PowerShell

Hi All,

I’ve alredy blogged this February about Microsoft Entra Verified ID when it was still in Preview. I’ve also blogged about Entra Verified ID Advanced Setup.

In the following MS Learn Site, you can find more Information about Verified employee / Verified ID

Microsoft Entra Verified ID now generally available since August 2024 it seems, but there is not much Information and Blogs out there, especially for the Verifiable credentials Admin API.

Overview

The term “decentralized identity” is used interchangeably with Self-Sovereign Identity (SSI), which is an approach to digital identity that gives individuals control of their digital identities.

Learn more here:

Abreviations:

  • Decentralized Identity (DID)
  • Verifiable Credentials (VCs)
  • Issuer (iss)
  • Subject (sub)
  • Claims

Roles:

  • Issuer
  • Holder
    • Subject
  • Verifier
  • Registry

Verifiable credentials admin API

I’ve spent some time with the Verifiable credentials admin API

I’ve created an Entra ID App Registration and addet > API Permissions > Add a permission

You need to use “API’s my organization uses” and search vor “Verifiable Credentials Service Admin”

  • Verifiable Credentials Service bb2a64ee-5d29-4b07-a491-25806dc854d3
  • Verifiable Credentials Service Admin 6a8b4b39-c021-437c-b060-5a14a3fd65f3
  • Verifiable Credentials Service Request 3db474bg-6aOc-4840-96ac-1fceb342124f

Delegated permissions

  • full_access

For Delegated Permissions you need one of the following Entra Roles

  • Global Administrator
  • Authentication policy administrator
  • Global Reader

Application Permissions

  • VerifiableCredential.Authority.Read
  • VerifiableCredential.Authority.ReadWrite
  • VerifiableCredential.Contract.Read
  • VerifiableCredential.Contract.ReadWrite
  • VerifiableCredential.Credential.Revoke
  • VerifiableCredential.Credential.Search
  • VerifiableCredential.Network.Read

Get AccessToken

There are many ways to get an Entra Access Token. I’ve documented here a few using the PSMSALNet PowerShell Module and using JWTDetails to decode the AccessToken.

Delegate Permission WAM

###############################################################################
# PSMSALNet (Delegate Permission WAM)
###############################################################################
#Install-Module PSMSALNet
#Import-Module PSMSALNet
$TenantId = "46bbad84-29f0-4e03-8d34-f6841a5071ad"
$AppID = "da2e568b-3058-48f5-9684-a0116a86656e" # IcewolfVerifiedCredential
$Certificate = Get-Item "Cert:\CurrentUser\My\A3A07A3C2C109303CCCB011B10141A020C8AFDA3" #O365Powershell4.cer
$CustomResource = "6a8b4b39-c021-437c-b060-5a14a3fd65f3"
$RedirectURI = "ms-appx-web://microsoft.aad.brokerplugin/$AppID"
$Token = Get-EntraToken -WAMFlow -ClientId $AppID -TenantId $TenantId -RedirectUri $RedirectURI -Resource Custom -CustomResource $CustomResource -Permissions "full_access"
$AccessToken = $token.AccessToken

#View AccessToken
Get-JWTDetails -token $AccessToken

Delegated Permission

###############################################################################
# PSMSALNet (Delegated Permission)
###############################################################################
#Install-Module PSMSALNet
#Import-Module PSMSALNet
$TenantId = "46bbad84-29f0-4e03-8d34-f6841a5071ad"
$AppID = "da2e568b-3058-48f5-9684-a0116a86656e" # IcewolfVerifiedCredential
$CustomResource = "6a8b4b39-c021-437c-b060-5a14a3fd65f3"
$Permissions = @('full_access')

$HashArguments = @{
  TenantId = $TenantId
  ClientId = $AppID
  RedirectUri = 'http://localhost'
  Resource = 'Custom'
  CustomResource = $CustomResource
  Permissions = $Permissions
}

#Get AccessToken
$Token = Get-EntraToken -PublicAuthorizationCodeFlow @HashArguments
$AccessToken = $token.AccessToken

#View AccessToken
Get-JWTDetails -token $AccessToken

Application Permission with Certificate

###############################################################################
# PSMSALNet (Application Permission with Certificate)
###############################################################################
#Install-Module PSMSALNet
#Import-Module PSMSALNet
$TenantId = "46bbad84-29f0-4e03-8d34-f6841a5071ad"
$AppID = "da2e568b-3058-48f5-9684-a0116a86656e" # IcewolfVerifiedCredential
$Certificate = Get-Item "Cert:\CurrentUser\My\A3A07A3C2C109303CCCB011B10141A020C8AFDA3" #O365Powershell4.cer
$CustomResource = "6a8b4b39-c021-437c-b060-5a14a3fd65f3"

$HashArguments = @{
  TenantId = $TenantId
  ClientId = $AppID
  ClientCertificate = $Certificate
  Resource = 'Custom'
  CustomResource = $CustomResource
}

#Get AccessToken
$Token = Get-EntraToken -ClientCredentialFlowWithCertificate @HashArguments
$AccessToken = $token.AccessToken

#View AccessToken
Get-JWTDetails -token $AccessToken

Native Login with ClientSecret (Application Permission)

###############################################################################
# Native Login with ClientSecret (Application Permission)
###############################################################################
$ClientSecret = "YourClientSecret"
$TenantId = "46bbad84-29f0-4e03-8d34-f6841a5071ad"
$AppID = "da2e568b-3058-48f5-9684-a0116a86656e" # IcewolfVerifiedCredential
$ContentType = "application/x-www-form-urlencoded"
$URI = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"

#Create Body
$Body = @"
client_id=$AppID
&scope=6a8b4b39-c021-437c-b060-5a14a3fd65f3/.default
&client_secret=$ClientSecret
&grant_type=client_credentials
"@

#Get AccessToken
$Token = Invoke-RestMethod -Uri $URI -Method "Post" -ContentType $ContentType -Body $Body
$AccessToken = $token.access_token

#View AccessToken
Get-JWTDetails -token $AccessToken

Native Login with Certificate (Application Permission)

###############################################################################
# Native Login with Certificate (Application Permission)
# https://learn.microsoft.com/en-us/answers/questions/346048/how-to-get-access-token-from-client-certificate-ca
###############################################################################
$TenantName = "icewolfch.onmicrosoft.com"
$AppId = "da2e568b-3058-48f5-9684-a0116a86656e" # IcewolfVerifiedCredential
#$CertificateThumbprint = "A3A07A3C2C109303CCCB011B10141A020C8AFDA3" #O365Powershell4.cer
$Certificate = Get-Item "Cert:\CurrentUser\My\A3A07A3C2C109303CCCB011B10141A020C8AFDA3" #O365Powershell4.cer
$Scope = "6a8b4b39-c021-437c-b060-5a14a3fd65f3/.default" # Example: "https://graph.microsoft.com/.default"

# Create base64 hash of certificate
$CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())

# Create JWT timestamp for expiration
$StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
$JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds
$JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0)

# Create JWT validity start timestamp  
$NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds  
$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)

# Create JWT header
$JWTHeader = @{
    alg = "RS256"
    typ = "JWT"
    # Use the CertificateBase64Hash and replace/strip to match web encoding of base64  
    x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='  
}

# Create JWT payload
$JWTPayLoad = @{
    # What endpoint is allowed to use this JWT  
    aud = "https://login.microsoftonline.com/$TenantName/oauth2/token"  

    # Expiration timestamp
    exp = $JWTExpiration

    # Issuer = your application
    iss = $AppId

    # JWT ID: random guid
    jti = [guid]::NewGuid()

    # Not to be used before
    nbf = $NotBefore

    # JWT Subject
    sub = $AppId
}

# Convert header and payload to base64
$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)

$JWTPayLoadToByte =  [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)

# Join header and Payload with "." to create a valid (unsigned) JWT
$JWT = $EncodedHeader + "." + $EncodedPayload

# Get the private key object of your certificate
$PrivateKey = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate))

# Define RSA signature and hashing algorithm
$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256

# Create a signature of the JWT
$Signature = [Convert]::ToBase64String(
    $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)
) -replace '\+','-' -replace '/','_' -replace '='

# Join the signature to the JWT with "."
$JWT = $JWT + "." + $Signature

# Create a hash with body parameters
$Body = @{
    client_id = $AppId
    client_assertion = $JWT
    client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
    scope = $Scope
    grant_type = "client_credentials"
}

$Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"

# Use the self-generated JWT as Authorization
$Header = @{
    Authorization = "Bearer $JWT"
}

# Splat the parameters for Invoke-Restmethod for cleaner code
$PostSplat = @{
    ContentType = 'application/x-www-form-urlencoded'
    Method = 'POST'
    Body = $Body
    Uri = $Url
    Headers = $Header
}

$Token = Invoke-RestMethod @PostSplat
$AccessToken = $Token.access_token

#View AccessToken
Get-JWTDetails -token $AccessToken

Onboarding

Onboarding just returns you the ObjectId’s of the Service Principals.

46bbad84-29f0-4e03-8d34-f6841a5071ad is just my TenantId.

###############################################################################
#Onboarding - Works only with delegated Authentication
###############################################################################
#POST /v1.0/verifiableCredentials/onboard
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/onboard"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
Invoke-RestMethod -URI $URI -Headers $Headers -Method "POST"

Entra > Enterprise Applications > Application type == Microsoft Applications > Verifiable credentials

Authorities

List Authorities - nothing setup yet, therefore nothing is returned

###############################################################################
#List Authorities
###############################################################################
#GET /v1.0/verifiableCredentials/authorities
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$Json = Invoke-RestMethod -URI $URI -Headers $Headers
$Json.value

Let’s create an Authority. You need to use Delegated Credentials (with Global Admin) and Access to your Azure Key Vault.

###############################################################################
#Create authority - Only works with Delegated Credentials
###############################################################################
#POST /v1.0/verifiableCredentials/authorities
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$ContentType = "application/json"
$SubscriptionID = "1e467fc0-3227-4628-a048-fc5ef79bff93"
$ResourceGroup = "RG_VerifiableCredentials"
$ResourceName = "DemoAuthorityKeyVault"

#with keVaultMetaData
$Body = @"
{
    "name": "Icewolf Authority",
    "linkedDomainUrl": "https://icewolf.ch",
    "didMethod": "web",
    "template": {
        "type": "VerifiedEmployee"
    },
    "keyVaultMetadata": {
        "subscriptionId": "$SubscriptionID",
        "resourceGroup": "$ResourceGroup",
        "resourceName": "$ResourceName",
        "resourceUrl": "https://$ResourceName.vault.azure.net/"
    }
}
"@

#Create Authority
Invoke-RestMethod -URI $URI -Body $Body -Headers $Headers -Method "POST" -ContentType $ContentType

#You still need to Register Decentralized ID (DID)
#https://icewolf.ch/.well-known/did.json

#You still need to upload DID Configuration JSON
#https://icewolf.ch/.well-known/did-configuration.json

Now let’s get the did.json

###############################################################################
#Generate DID document
###############################################################################
#POST /v1.0/verifiableCredentials/authorities/<authorityId>/generateDidDocument
#https://icewolf.ch/.well-known/did.json
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/generateDidDocument"
$URI = $BaseURL + $APIURL
$ContentType = "application/json"
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$Response = Invoke-WebRequest -URI $URI -Headers $Headers -Method "POST" -ContentType $ContentType
$Response.Content

Now let’s get the did-configuration.json

###############################################################################
#Well-known DID configuration
###############################################################################
#POST /v1.0/verifiableCredentials/authorities/<authorityId>/generateWellknownDidConfiguration
#https://icewolf.ch/.well-known/did-configuration.json
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/generateWellknownDidConfiguration"
$URI = $BaseURL + $APIURL
$ContentType = "application/json"
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$Response = Invoke-WebRequest -URI $URI -Headers $Headers -Method "POST" -ContentType $ContentType
$Response.Content

I am using an Azure App Service to host the Webiste. Hat do extend the web.config with the MIME Type.

<system.webServer>
 <staticContent>
  <remove fileExtension=".json"/>
  <mimeMap fileExtension=".json" mimeType="application/json"/>
</staticContent>

Now you need to upload the two files. After that we can verify the Configuration.

###############################################################################
#Check DID JSON
###############################################################################
(Invoke-WebRequest -Uri "https://icewolf.ch/.well-known/did.json" -Method "GET").Content
(Invoke-WebRequest -Uri "https://icewolf.ch/.well-known/did-configuration.json" -Method "GET").Content

Verify the DID Configuration

###############################################################################
#Validate well-known DID configuration
###############################################################################
#POST /v1.0/verifiableCredentials/authorities/<authorityId>/validateWellKnownDidConfiguration
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/validateWellKnownDidConfiguration"
$URI = $BaseURL + $APIURL
$ContentType = "application/json"
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$Body = ""
Invoke-RestMethod -URI $URI -Body $Body -Headers $Headers -Method "POST"

It should look like this

Get authority

###############################################################################
#Get authority
###############################################################################
#GET /v1.0/verifiableCredentials/authorities/<authorityId>
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$Json = Invoke-RestMethod -URI $URI -Headers $Headers
$Json

Let’s update the Name of the authority

###############################################################################
#Update authority - Works only with delegated Authentication
###############################################################################
#PATCH /v1.0/verifiableCredentials/authorities/<authorityId>
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$ContentType = "application/json"

$body = @"
{
    "name":"Icewolf Authority DEMO"
}
"@
#Update Authority
Invoke-RestMethod -URI $URI -Body $Body -Headers $Headers -Method "PATCH" -ContentType $ContentType

As you can see the Organization Name has been updated

Delete Authority. Note that this uses the BETA Endpoint.

Caution this will also delete all of your contracts and credentials!

###############################################################################
#Delete authority > Works only with delegated Permission
###############################################################################
#DELETE /beta/verifiableCredentials/authorities/<authorityId>
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/beta/verifiableCredentials/authorities/$AuthorityId"
$ContentType = "application/json"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
#Delete authority
Invoke-RestMethod -URI $URI -Headers $Headers -Method "DELETE" -ContentType $ContentType

I was able to rotate the signin key. And download updated did.json and did-configuration.json. After uploading them to the Website i was never able to validate the DID Configuration afterwards

###############################################################################
#Rotate signing key > Caution! Was not able to validate did.json afterwards
###############################################################################
#POST /v1.0/verifiableCredentials/authorities/<authorityId>/didInfo/signingKeys/rotate
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/didInfo/signingKeys/rotate"
$URI = $BaseURL + $APIURL
$ContentType = "application/json"
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
Invoke-RestMethod -URI $URI -Headers $Headers -Method "POST"

#You need to update DID JSON and DID Configuration
#https://icewolf.ch/.well-known/did.json
#https://icewolf.ch/.well-known/did-configuration.json

#Could not validate did.json afterwards

Contracts

List the Contracts - nothing configured yet, therefore it returns nothing

###############################################################################
#List contracts
###############################################################################
#GET /v1.0/verifiableCredentials/authorities/<authorityId>/contracts
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/contracts"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$Json = Invoke-RestMethod -URI $URI -Headers $Headers
$Json.Value

Here is how i configured my Display Settings (Card style)

Logo URL: https://icewolf.ch/images/icewolf_ch.png
Text color: #000000
Background color: #FFFFFF

If you wondering about the “validityInterval” set to 15552000 when you creating a contract in the section below

New-TimeSpan -Seconds 15552000

Let’s create a “Verified Employee” Contract

###############################################################################
#Create contract (Verified Employee)
###############################################################################
#POST /v1.0/verifiableCredentials/authorities/<authorityId>/contracts
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/contracts"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$ContentType = "application/json"

<#
$Body = @"
{
    "name": "ExampleContractName1",
    "rules": "<rules JSON>",
    "displays": [{<display JSON}],
}
"@
#>
###############################################################################
#VerifiedEmployee
###############################################################################
$Body = @"
{
    "name": "Verified employee",
    "status": "Enabled",
    "issueNotificationEnabled": false,
    "issueNotificationAllowedToGroupOids": [],
    "availableInVcDirectory": true,
    "rules": {
        "attestations": {
            "accessTokens": [
                {
                    "mapping": [
                        {
                            "outputClaim": "displayName",
                            "required": true,
                            "inputClaim": "displayName",
                            "indexed": false
                        },
                        {
                            "outputClaim": "givenName",
                            "required": false,
                            "inputClaim": "givenName",
                            "indexed": false
                        },
                        {
                            "outputClaim": "jobTitle",
                            "required": false,
                            "inputClaim": "jobTitle",
                            "indexed": false
                        },
                        {
                            "outputClaim": "preferredLanguage",
                            "required": false,
                            "inputClaim": "preferredLanguage",
                            "indexed": false
                        },
                        {
                            "outputClaim": "surname",
                            "required": false,
                            "inputClaim": "surname",
                            "indexed": false
                        },
                        {
                            "outputClaim": "mail",
                            "required": false,
                            "inputClaim": "mail",
                            "indexed": false
                        },
                        {
                            "outputClaim": "revocationId",
                            "required": true,
                            "inputClaim": "userPrincipalName",
                            "indexed": true
                        },
                        {
                            "outputClaim": "photo",
                            "required": false,
                            "inputClaim": "photo",
                            "indexed": false
                        }
                    ],
                    "required": true
                }
            ]
        },
        "validityInterval": 15552000,
        "vc": {
            "type": [
                "VerifiedEmployee"
            ]
        }
    },
    "displays": [
        {
            "locale": "en-US",
            "card": {
                "backgroundColor": "#FFFFFF",
                "description": "This verifiable credential is issued to all members of the Icewolf Authority org.",
                "issuedBy": "Icewolf Authority",
                "textColor": "#000000",
                "title": "Verified Employee",
                "logo": {
                    "description": "Default verified employee logo",
                    "uri": "https://icewolf.ch/images/icewolf_ch.png"
                }
            },
            "consent": {
                "instructions": "Verify your identity and workplace the easy way. Add this ID for online and in-person use.",
                "title": "Do you want to accept the verified employee credential from Icewolf Authority."
            },
            "claims": [
                {
                    "claim": "vc.credentialSubject.givenName",
                    "label": "Name",
                    "type": "String"
                },
                {
                    "claim": "vc.credentialSubject.surname",
                    "label": "Surname",
                    "type": "String"
                },
                {
                    "claim": "vc.credentialSubject.mail",
                    "label": "Email",
                    "type": "String"
                },
                {
                    "claim": "vc.credentialSubject.jobTitle",
                    "label": "Job title",
                    "type": "String"
                },
                {
                    "claim": "vc.credentialSubject.photo",
                    "label": "User picture",
                    "type": "image/jpg;base64url"
                },
                {
                    "claim": "vc.credentialSubject.displayName",
                    "label": "Display name",
                    "type": "String"
                },
                {
                    "claim": "vc.credentialSubject.preferredLanguage",
                    "label": "Preferred language",
                    "type": "String"
                },
                {
                    "claim": "vc.credentialSubject.revocationId",
                    "label": "Revocation id",
                    "type": "String"
                }
            ]
        }
    ],
    "allowOverrideValidityIntervalOnIssuance": false
}
"@

#Create Contract
Invoke-RestMethod -URI $URI -Headers $Headers -Method "POST" -Body $Body -ContentType $ContentType

Contract has been created

Let’s create a Custom Contract

###############################################################################
#Create contract (Custom Contract)
###############################################################################
#POST /v1.0/verifiableCredentials/authorities/<authorityId>/contracts
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/contracts"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$ContentType = "application/json"

###############################################################################
#VerifiedCredentialExpert
###############################################################################
$Body = @"
{
    "name": "Example Contract",
    "rules": {
        "attestations": {
            "idTokenHints": [
                {
                    "mapping": [
                        {
                            "outputClaim": "firstName",
                            "required": false,
                            "inputClaim": "given_name",
                            "indexed": false
                        },
                        {
                            "outputClaim": "lastName",
                            "required": false,
                            "inputClaim": "family_name",
                            "indexed": false
                        }
                    ],
                    "required": false
                }
            ]
        },
        "validityInterval": 2592000,
        "vc": {
            "type": [
                "VerifiedCredentialExpert"
            ]
        }
    },
    "displays": [
        {
            "locale": "en-US",
            "card": {
                "title": "Verified Credential Expert",
                "issuedBy": "Icewolf Authority DEMO",
                "backgroundColor": "#000000",
                "textColor": "#ffffff",
                "logo": {
                    "uri": "https://icewolf.ch/images/icewolf_ch.png",
                    "description": "Icewolf Logo"
                },
                "description": "My Description"
            },
            "consent": {
                "title": "Do you want to accept the Verified Credential Expert VC from Icewolf Authority",
                "instructions": "Verify your Verified Credential Expert. Add this ID for online and in-person use"
            },
            "claims": [
                {
                    "claim": "vc.credentialSubject.firstName",
                    "label": "Name",
                    "type": "String"
                },
                {
                    "claim": "vc.credentialSubject.lastName",
                    "label": "Surname",
                    "type": "String"
                }
            ]
        }
    ]
}
"@

#Create Contract
Invoke-RestMethod -URI $URI -Headers $Headers -Method "POST" -Body $Body -ContentType $ContentType

Get the Contract

###############################################################################
#Get contract
###############################################################################
#GET /v1.0/verifiableCredentials/authorities/<authorityId>/contracts/<contractid>
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$ContractId = "29f089ae-b40c-3d39-04b8-f1e73b23b46e"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/contracts/$ContractId"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$Json = Invoke-RestMethod -URI $URI -Headers $Headers -Method "GET"
$Json
#$Request = Invoke-WebRequest -URI $URI -Headers $Headers -Method "GET"
#$Request.Content

Enable through MyAccount

###############################################################################
#Enable through MyAccount - Only with Delegated Credential
###############################################################################
#POST https://verifiedid.did.msidentity.com/v1.0/verifiableCredentials/organizationSettings/myAccount
$ContractId = "29f089ae-b40c-3d39-04b8-f1e73b23b46e"
$URI = "https://verifiedid.did.msidentity.com/v1.0/verifiableCredentials/organizationSettings/myAccount"
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$ContentType = "application/json"

#Enable through MyAccount
$Body = @"
{"contractIdsEnabled":["$ContractId"]}
"@
Invoke-RestMethod -URI $URI -Headers $Headers -Method "POST" -Body $Body -ContentType $ContentType

#Disable through MyAccount
$Body = @"
{"contractIdsEnabled":[]}
"@
Invoke-RestMethod -URI $URI -Headers $Headers -Method "POST" -Body $Body -ContentType $ContentType

It looks like this in the Entra Portal > Verified ID > Credentials

Update Contract did not work

###############################################################################
#Update contract - did not work
###############################################################################
#PATCH /v1.0/verifiableCredentials/authorities/<authorityId>/contracts/<contractid>
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$ContractId = "ef2fb8d8-95eb-6c3a-55e0-8400157d7d32" #ExampleContract
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/contracts/$ContractId"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$ContentType = "application/json"

$Body = @"
{
    "issueNotificationAllowedToGroupOids": [
        "503eafb2-021f-4ee2-9942-3d3f91e48e03"
    ]
}
"@

#Update contract
$Json = Invoke-RestMethod -URI $URI -Headers $Headers -Method "PATCH" -Body $Body -ContentType $ContentType

Delete Contract - as you can see we’re using the BETA endpoint here.

###############################################################################
#Delete Contract (BETA)
###############################################################################
#PATCH /v1.0/verifiableCredentials/authorities/<authorityId>/contracts/<contractid>
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$ContractId = "29f089ae-b40c-3d39-04b8-f1e73b23b46e"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/beta/verifiableCredentials/authorities/$AuthorityId/contracts/$ContractId"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
Invoke-RestMethod -URI $URI -Headers $Headers -Method "DELETE"

Search credential:

  • The Claim Value is case-sensitive.
  • The ContractId and the Claim Value are combined: 29f089ae-b40c-3d39-04b8-f1e73b23b46ea.bohren@icewolf.ch
  • The combined string is converted to a Bytearray
  • The ByteArray is hashed with SHA256
  • The SHA256 Hash is Base64 Encoded
  • The Base64 Encoded Value will be URLEncoded
  • Now you can search for the Credential
###############################################################################
#Search Credential
###############################################################################
#GET /v1.0/verifiableCredentials/authorities/<authorityId>/contracts/<contractId>/credentials?filter=indexclaimhash eq {hashedsearchclaimvalue}
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$ContractId = "29f089ae-b40c-3d39-04b8-f1e73b23b46e"
$claimvalue = ("A.Bohren@icewolf.ch").ToLower()

# Create Input Data
$sha256 = [System.Security.Cryptography.SHA256]::Create()
$strInput = "$contractid$claimvalue"
Add-Type -AssemblyName System.Web
$enc = [system.Text.Encoding]::UTF8
$InputBytes = $enc.GetBytes($strInput)
$Base64String = [Convert]::ToBase64String($sha256.ComputeHash($InputBytes))
$hashedsearchclaimvalue = [System.Web.HttpUtility]::UrlEncode($Base64String)

#Create Query
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/contracts/$ContractId/credentials?filter=indexclaimhash eq $hashedsearchclaimvalue"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$JSON = Invoke-RestMethod -URI $URI -Headers $Headers -Method "GET"
$JSON.Value

We get the CredentialId that looks somethin like this: urn:pic:<NumbersAndCharacters> and see the Status and issueing Timestamp

We can now get the Credential and will also see the Status and issueing Timestamp

###############################################################################
#Get Credential
###############################################################################
#GET /v1.0/verifiableCredentials/authorities/<authorityId>/contracts/<contractId>/credentials/<credentialId>
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$ContractId = "29f089ae-b40c-3d39-04b8-f1e73b23b46e"
$CredentialId = "urn:pic:00e59840d54044b59ef7902c40201626"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/contracts/$ContractId/credentials/$CredentialId"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$JSON = Invoke-RestMethod -URI $URI -Headers $Headers
$JSON

You can Revoke the Credential with the Credential ID

###############################################################################
#Revoke Credential
###############################################################################
#POST /v1.0/verifiableCredentials/authorities/<authorityId>/contracts/<contractId>/credentials/<credentialid>/revoke
$AuthorityId = "824776a0-935b-8caf-d74e-a41b60786dec"
$ContractId = "29f089ae-b40c-3d39-04b8-f1e73b23b46e"
$CredentialId = "urn:pic:00e59840d54044b59ef7902c40201626"
$ContentType = "application/json"
$BaseURL = "https://verifiedid.did.msidentity.com"
$APIURL = "/v1.0/verifiableCredentials/authorities/$AuthorityId/contracts/$ContractId/credentials/$CredentialId/revoke"
$URI = $BaseURL + $APIURL
$Headers = @{"Authorization" = "Bearer "+ $AccessToken}
$Body = ""
Invoke-RestMethod -URI $URI -Headers $Headers -Body $Body -Method "POST" -ContentType $ContentType

It is now revoked

Manifest

While playing around i found that under the Manifest URL you can find a Token

###############################################################################
#Manifest URL is JWT Encoded Information
###############################################################################
# GET https://verifiedid.did.msidentity.com/v1.0/tenants/<TenantId>/verifiableCredentials/contracts/<ContractId>/manifest
#$TenantId = "46bbad84-29f0-4e03-8d34-f6841a5071ad"
#Use OIDC to get TenantId
$Domain = "icewolf.ch"
$Response = Invoke-WebRequest -UseBasicParsing https://login.windows.net/$($Domain)/.well-known/openid-configuration -TimeoutSec 1
$TenantId = ($Response | ConvertFrom-Json).token_endpoint.Split('/')[3]

$ContractId = "29f089ae-b40c-3d39-04b8-f1e73b23b46e"
$ManifestUrl = "https://verifiedid.did.msidentity.com/v1.0/tenants/$TenantId/verifiableCredentials/contracts/$ContractId/manifest"
$Json = Invoke-RestMethod -URI $ManifestURL -Method "GET"
$Accesstoken = $Json.token

#View AccessToken
Get-JWTDetails -token $AccessToken

Learnings

As i’ve spent many Hours exploring the Verifiable credentials admin API this is what i’ve learned

  • Watch out for JSON Errors - check with jsonlint
  • JSON differs between lower- and uppercase
  • Examples are very badly documented - did a lot of Reverse Engineering and use Browser Development Tools
  • If you get “WID” Errors, try with Delegated Authentication
  • Was not able to get these things running
    • Rotate Key > could not Verify did.json
    • Update Contract
  • There is no List of all Credentials - How can you tell what Credentials you issued?

Regards
Andres Bohren

EntraID Logo

PowerShell Logo

Security Logo