├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── codeql.yml ├── .gitignore ├── 1-python-api-idtokenhint ├── .vscode │ ├── launch.json │ └── settings.json ├── ARMTemplate │ └── template.json ├── AppCreationScripts │ ├── Cleanup.ps1 │ ├── Cleanup.sh │ ├── Configure.ps1 │ ├── Configure.sh │ └── README.md ├── CredentialFiles │ ├── VerifiedCredentialExpertDisplay.json │ └── VerifiedCredentialExpertRules.json ├── Dockerfile ├── README.md ├── ReadmeFiles │ ├── AdminConcent.PNG │ ├── DeployToAzure.png │ ├── SampleArchitectureOverview.svg │ └── ngrok-url-screen.png ├── Templates │ └── README.md ├── app.py ├── callback.py ├── config.json ├── docker-build.cmd ├── docker-build.sh ├── docker-run.cmd ├── docker-run.sh ├── issuer.py ├── presentation_request_trueidentity.json ├── public │ ├── VerifiedCredentialExpert-icon.png │ ├── authenticator-icon.png │ ├── favicon.ico │ ├── favicon.png │ ├── index.html │ ├── issuer.html │ ├── presentation-verified.html │ ├── qrcode.min.js │ ├── styles.css │ ├── verifiedid.requestservice.client.js │ ├── verifiedid.uihandler.js │ └── verifier.html ├── requirements.txt ├── setenv.cmd ├── setenv.sh └── verifier.py ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md └── ReadmeFiles └── SampleArchitectureOverview.svg /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '23 15 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 27 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: [ 'python' ] 37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] 38 | # Use only 'java' to analyze code written in Java, Kotlin or both 39 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@v3 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v2 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 56 | # queries: security-extended,security-and-quality 57 | 58 | 59 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 60 | # If this step fails, then you should remove it and run the build manually (see below) 61 | - name: Autobuild 62 | uses: github/codeql-action/autobuild@v2 63 | 64 | # ℹ️ Command-line programs to run using the OS shell. 65 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 66 | 67 | # If the Autobuild fails above, remove it and uncomment the following three lines. 68 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 69 | 70 | # - run: | 71 | # echo "Run, Build Application using script" 72 | # ./location_of_script_within_repo/buildscript.sh 73 | 74 | - name: Perform CodeQL Analysis 75 | uses: github/codeql-action/analyze@v2 76 | with: 77 | category: "/language:${{matrix.language}}" 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # ignore (private) dev settings 132 | config.[Dd]evelopment.json 133 | run.[Dd]evelopment.* 134 | docker-run.[Dd]evelopment.* 135 | .dev-config/ -------------------------------------------------------------------------------- /1-python-api-idtokenhint/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Attach using Process Id", 9 | "type": "python", 10 | "request": "attach", 11 | "processId": "${command:pickProcess}" 12 | }, 13 | { 14 | "name": "Python: Current File", 15 | "type": "python", 16 | "request": "launch", 17 | "program": "${file}", 18 | "console": "integratedTerminal" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /1-python-api-idtokenhint/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "appService.showBuildDuringDeployPrompt": false 3 | } -------------------------------------------------------------------------------- /1-python-api-idtokenhint/ARMTemplate/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "webAppName": { 6 | "type": "string", 7 | "defaultValue": "", 8 | "minLength": 2, 9 | "metadata": { 10 | "description": "app name." 11 | } 12 | }, 13 | "repoURL": { 14 | "type": "string", 15 | "metadata": { 16 | "description": "Github repo URL" 17 | }, 18 | "defaultValue": "https://github.com/Azure-Samples/active-directory-verifiable-credentials-python.git" 19 | }, 20 | "branch": { 21 | "type": "string", 22 | "metadata": { 23 | "description": "Github repo branch" 24 | }, 25 | "defaultValue": "main" 26 | }, 27 | "Project": { 28 | "type": "string", 29 | "metadata": { 30 | "description": "Github repo subfolder" 31 | }, 32 | "defaultValue": "1-python-api-idtokenhint" 33 | }, 34 | "azTenantId": { 35 | "type": "string", 36 | "metadata": { 37 | "description": "Entra ID Tenant id" 38 | }, 39 | "defaultValue": "" 40 | }, 41 | "azClientId": { 42 | "type": "string", 43 | "metadata": { 44 | "description": "azClientId" 45 | }, 46 | "defaultValue": "" 47 | }, 48 | "azClientSecret": { 49 | "type": "string", 50 | "metadata": { 51 | "description": "azClientSecret" 52 | }, 53 | "defaultValue": "" 54 | }, 55 | "DidAuthority": { 56 | "type": "string", 57 | "metadata": { 58 | "description": "DidAuthority" 59 | }, 60 | "defaultValue": "" 61 | }, 62 | "CredentialManifest": { 63 | "type": "string", 64 | "metadata": { 65 | "description": "CredentialManifest" 66 | }, 67 | "defaultValue": "" 68 | }, 69 | "CredentialType": { 70 | "type": "string", 71 | "metadata": { 72 | "description": "CredentialType" 73 | }, 74 | "defaultValue": "VerifiedCredentialExpert" 75 | }, 76 | "PhotoClaimName": { 77 | "type": "string", 78 | "metadata": { 79 | "description": "claim name for photo - if you are using FaceCheck during presentation. Otherwise leave this field blank" 80 | }, 81 | "defaultValue": "" 82 | } 83 | }, 84 | "variables": { 85 | "appServicePlanPortalName": "[concat(parameters('webAppName'), '-plan')]", 86 | "linuxFxVersion": "PYTHON|3.12" 87 | }, 88 | "resources": [ 89 | { 90 | "type": "Microsoft.Web/serverfarms", 91 | "apiVersion": "2020-06-01", 92 | "name": "[variables('appServicePlanPortalName')]", 93 | "location": "[resourceGroup().location]", 94 | "sku": { 95 | "name": "B1", 96 | "tier": "Basic", 97 | "size": "B1", 98 | "family": "B", 99 | "capacity": 1 100 | }, 101 | "properties": { 102 | "perSiteScaling": false, 103 | "elasticScaleEnabled": false, 104 | "maximumElasticWorkerCount": 1, 105 | "isSpot": false, 106 | "reserved": true, 107 | "isXenon": false, 108 | "hyperV": false, 109 | "targetWorkerCount": 0, 110 | "targetWorkerSizeId": 0, 111 | "zoneRedundant": false 112 | }, 113 | "kind": "linux" 114 | }, 115 | 116 | { 117 | "type": "Microsoft.Web/sites", 118 | "apiVersion": "2022-09-01", 119 | "name": "[parameters('webAppName')]", 120 | "location": "[resourceGroup().location]", 121 | "kind": "app,linux", 122 | "dependsOn": [ 123 | "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanPortalName'))]" 124 | ], 125 | "properties": { 126 | "enabled": true, 127 | "hostNameSslStates": [ 128 | { 129 | "name": "[concat(parameters('webAppName'), '.azurewebsites.net')]", 130 | "sslState": "Disabled", 131 | "hostType": "Standard" 132 | }, 133 | { 134 | "name": "[concat(parameters('webAppName'), '.scm.azurewebsites.net')]", 135 | "sslState": "Disabled", 136 | "hostType": "Repository" 137 | } 138 | ], 139 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanPortalName'))]", 140 | "reserved": true, 141 | "isXenon": false, 142 | "hyperV": false, 143 | "vnetRouteAllEnabled": false, 144 | "vnetImagePullEnabled": false, 145 | "vnetContentShareEnabled": false, 146 | "siteConfig": { 147 | "numberOfWorkers": 1, 148 | "linuxFxVersion": "[variables('linuxFxVersion')]", 149 | "acrUseManagedIdentityCreds": false, 150 | "alwaysOn": false, 151 | "http20Enabled": false, 152 | "functionAppScaleLimit": 0, 153 | "minimumElasticInstanceCount": 0, 154 | "appSettings": [ 155 | { 156 | "name": "ENABLE_ORYX_BUILD", 157 | "value": "false" 158 | }, 159 | { 160 | "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", 161 | "value": "false" 162 | }, 163 | { 164 | "name": "DEPLOYMENT_SOURCE", 165 | "value": "[Concat('/home/site/repository/', parameters('Project'))]" 166 | }, 167 | { 168 | "name": "azTenantId", 169 | "value": "[parameters('azTenantId')]" 170 | }, 171 | { 172 | "name": "azClientId", 173 | "value": "[parameters('azClientId')]" 174 | }, 175 | { 176 | "name": "azClientSecret", 177 | "value": "[parameters('azClientSecret')]" 178 | }, 179 | { 180 | "name": "DidAuthority", 181 | "value": "[parameters('DidAuthority')]" 182 | }, 183 | { 184 | "name": "acceptedIssuers", 185 | "value": "[parameters('DidAuthority')]" 186 | }, 187 | { 188 | "name": "CredentialType", 189 | "value": "[parameters('CredentialType')]" 190 | }, 191 | { 192 | "name": "CredentialManifest", 193 | "value": "[parameters('CredentialManifest')]" 194 | }, 195 | { 196 | "name": "issuancePinCodeLength", 197 | "value": "4" 198 | }, 199 | { 200 | "name": "photoClaimName", 201 | "value": "[parameters('PhotoClaimName')]" 202 | }, 203 | { 204 | "name": "matchConfidenceThreshold", 205 | "value": "70" 206 | } 207 | ] 208 | }, 209 | "scmSiteAlsoStopped": false, 210 | "clientAffinityEnabled": true, 211 | "clientCertEnabled": false, 212 | "clientCertMode": "Required", 213 | "hostNamesDisabled": false, 214 | "containerSize": 0, 215 | "dailyMemoryTimeQuota": 0, 216 | "httpsOnly": false, 217 | "redundancyMode": "None", 218 | "storageAccountRequired": false, 219 | "keyVaultReferenceIdentity": "SystemAssigned" 220 | }, 221 | 222 | "resources": [ 223 | { 224 | "type": "sourcecontrols", 225 | "apiVersion": "2018-02-01", 226 | "name": "web", 227 | "location": "[resourceGroup().location]", 228 | "dependsOn": [ 229 | "[resourceId('Microsoft.Web/sites', parameters('webAppName'))]" 230 | ], 231 | "properties": { 232 | "repoUrl": "[parameters('repoURL')]", 233 | "branch": "[parameters('branch')]", 234 | "isManualIntegration": true 235 | } 236 | } 237 | ] 238 | 239 | }, 240 | 241 | { 242 | "type": "Microsoft.Web/sites/hostNameBindings", 243 | "apiVersion": "2022-09-01", 244 | "name": "[concat(parameters('webAppName'), '/', parameters('webAppName'), '.azurewebsites.net')]", 245 | "location": "[resourceGroup().location]", 246 | "dependsOn": [ 247 | "[resourceId('Microsoft.Web/sites', parameters('webAppName'))]" 248 | ], 249 | "properties": { 250 | "siteName": "[parameters('webAppName')]", 251 | "hostNameType": "Verified" 252 | } 253 | }, 254 | 255 | { 256 | "type": "Microsoft.Web/sites/config", 257 | "apiVersion": "2022-09-01", 258 | "name": "[concat(parameters('webAppName'), '/web')]", 259 | "location": "[resourceGroup().location]", 260 | "dependsOn": [ 261 | "[resourceId('Microsoft.Web/sites', parameters('webAppName'))]" 262 | ], 263 | "properties": { 264 | "numberOfWorkers": 1, 265 | "defaultDocuments": [ 266 | "Default.htm", 267 | "Default.html", 268 | "Default.asp", 269 | "index.htm", 270 | "index.html", 271 | "iisstart.htm", 272 | "default.aspx", 273 | "index.php", 274 | "hostingstart.html" 275 | ], 276 | "netFrameworkVersion": "v4.0", 277 | "linuxFxVersion": "[variables('linuxFxVersion')]", 278 | "requestTracingEnabled": false, 279 | "remoteDebuggingEnabled": false, 280 | "httpLoggingEnabled": true, 281 | "acrUseManagedIdentityCreds": false, 282 | "logsDirectorySizeLimit": 100, 283 | "detailedErrorLoggingEnabled": false, 284 | "publishingUsername": "[concat('$', parameters('webAppName'))]", 285 | "scmType": "None", 286 | "use32BitWorkerProcess": true, 287 | "webSocketsEnabled": false, 288 | "alwaysOn": false, 289 | "managedPipelineMode": "Integrated", 290 | "appCommandLine": "[concat('cd /home/site/wwwroot && cp -r /home/site/repository/', parameters('Project'), '/* . && pip install -r requirements.txt && python app.py')]", 291 | "virtualApplications": [ 292 | { 293 | "virtualPath": "/", 294 | "physicalPath": "site\\wwwroot", 295 | "preloadEnabled": false 296 | } 297 | ], 298 | "loadBalancing": "LeastRequests", 299 | "experiments": { 300 | "rampUpRules": [] 301 | }, 302 | "autoHealEnabled": false, 303 | "vnetRouteAllEnabled": false, 304 | "vnetPrivatePortsCount": 0, 305 | "localMySqlEnabled": false, 306 | "ipSecurityRestrictions": [ 307 | { 308 | "ipAddress": "Any", 309 | "action": "Allow", 310 | "priority": 2147483647, 311 | "name": "Allow all", 312 | "description": "Allow all access" 313 | } 314 | ], 315 | "scmIpSecurityRestrictions": [ 316 | { 317 | "ipAddress": "Any", 318 | "action": "Allow", 319 | "priority": 2147483647, 320 | "name": "Allow all", 321 | "description": "Allow all access" 322 | } 323 | ], 324 | "scmIpSecurityRestrictionsUseMain": false, 325 | "http20Enabled": false, 326 | "minTlsVersion": "1.2", 327 | "scmMinTlsVersion": "1.2", 328 | "ftpsState": "FtpsOnly", 329 | "preWarmedInstanceCount": 0, 330 | "elasticWebAppScaleLimit": 0, 331 | "functionsRuntimeScaleMonitoringEnabled": false, 332 | "minimumElasticInstanceCount": 0, 333 | "azureStorageAccounts": {} 334 | } 335 | } 336 | 337 | ] 338 | } 339 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/AppCreationScripts/Cleanup.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')][string] $tenantId 4 | ) 5 | 6 | # Pre-requisites 7 | if ($null -eq (Get-Module -ListAvailable -Name "Az.Accounts")) { 8 | Install-Module -Name "Az.Accounts" -Scope CurrentUser 9 | } 10 | if ($null -eq (Get-Module -ListAvailable -Name "Az.Resources")) { 11 | Install-Module "Az.Resources" -Scope CurrentUser 12 | } 13 | Import-Module -Name "Az.Accounts" 14 | Import-Module -Name "Az.Resources" 15 | 16 | $isMacLinux = ($env:PATH -imatch "/usr/bin" ) 17 | 18 | $ctx = Get-AzContext 19 | if ( !$ctx ) { 20 | if ( $tenantId ) { 21 | $creds = Connect-AzAccount -TenantId $tenantId 22 | } else { 23 | $creds = Connect-AzAccount 24 | $tenantId = $creds.Context.Account.Tenants[0] 25 | } 26 | } else { 27 | if ( $TenantId -and $TenantId -ne $ctx.Tenant.TenantId ) { 28 | write-error "You are targeting tenant $tenantId but you are signed in to tennant $($ctx.Tenant.TenantId)" 29 | } 30 | $tenantId = $ctx.Tenant.TenantId 31 | } 32 | 33 | $tenant = Get-AzTenant 34 | $tenantDomainName = ($tenant | Where { $_.Id -eq $tenantId }).Domains[0] 35 | 36 | # Removes the applications 37 | Write-Host "Cleaning-up application from tenant '$tenantDomainName'" 38 | $appName = "Verifiable Credentials Python sample" 39 | $appShortName = "vcpythonsample" 40 | Write-Host "Removing 'client' ($appName) if needed" 41 | $app = Get-AzADApplication -DisplayName $appName 42 | if ($null -ne $app) { 43 | $app | Remove-AzADApplication 44 | Write-Host "Removed app $($app.AppId)" 45 | } 46 | 47 | $certSubject = "CN=$appShortName" 48 | write-host "Removing self-signed certificate $certSubject files aadappcert*" 49 | if ( $False -eq $isMacLinux ) { 50 | Remove-Item -Path .\aadappcert.* 51 | } else { 52 | & rm ./aadappcert* 53 | } 54 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/AppCreationScripts/Cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This bash script is intended to run on Mac/Linux and requires Azure-CLI 4 | # https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-macos 5 | 6 | while getopts t: flag; do 7 | case "${flag}" in 8 | t) tenantId=${OPTARG};; 9 | esac 10 | done 11 | 12 | acct=$(az account show) 13 | if [[ -z $acct ]]; then 14 | if [[ -z $tenantId ]]; then az login; else az login -t $tenantId; fi 15 | fi 16 | 17 | appName="Verifiable Credentials Python sample" 18 | 19 | # get thing we need 20 | echo "Getting things..." 21 | tenantId=$(az account show --query "tenantId" -o tsv) 22 | 23 | # create the app and the sp 24 | echo "Deleting app $appName" 25 | appId=$(az ad sp list --display-name "$appName" --query "[0].appId" -o tsv) 26 | if [ -z $appId ]; then 27 | echo "App does not exist" 28 | else 29 | az ad app delete --id $appId 30 | fi 31 | 32 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/AppCreationScripts/Configure.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')][string] $tenantId, 4 | [Parameter(Mandatory=$False, HelpMessage='Switch if you want to generate a client_secret for the app')][switch]$ClientSecret = $False, 5 | [Parameter(Mandatory=$False, HelpMessage='Switch if you want to generate a client certificate for the app')][switch]$ClientCertificate = $False 6 | ) 7 | 8 | # Pre-requisites 9 | if ($null -eq (Get-Module -ListAvailable -Name "Az.Accounts")) { 10 | Install-Module -Name "Az.Accounts" -Scope CurrentUser 11 | } 12 | if ($null -eq (Get-Module -ListAvailable -Name "Az.Resources")) { 13 | Install-Module "Az.Resources" -Scope CurrentUser 14 | } 15 | Import-Module -Name "Az.Accounts" 16 | Import-Module -Name "Az.Resources" 17 | 18 | $isMacLinux = ($env:PATH -imatch "/usr/bin" ) 19 | # default to client_secret 20 | if ( !$ClientSecret -and !$ClientCertificate ) { $ClientSecret = $True } 21 | 22 | Function UpdateLine([string] $line, [string] $value) 23 | { 24 | $index = $line.IndexOf(':') 25 | $delimiter = ',' 26 | if ($index -eq -1) { 27 | $index = $line.IndexOf('=') 28 | $delimiter = ';' 29 | } 30 | if ($index -ige 0) { 31 | $line = $line.Substring(0, $index+1) + " "+'"'+$value+'"'+$delimiter 32 | } 33 | return $line 34 | } 35 | 36 | Function UpdateTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary) 37 | { 38 | $lines = Get-Content $configFilePath 39 | for( $index = 0; $index -lt $lines.Length; $index++ ) { 40 | foreach($key in $dictionary.Keys) { 41 | if ($lines[$index].Contains($key)) { 42 | $lines[$index] = UpdateLine $lines[$index] $dictionary[$key] 43 | } 44 | } 45 | } 46 | Set-Content -Path $configFilePath -Value $lines -Force 47 | } 48 | 49 | $ctx = Get-AzContext 50 | if ( !$ctx ) { 51 | if ( $tenantId ) { 52 | $creds = Connect-AzAccount -TenantId $tenantId 53 | } else { 54 | $creds = Connect-AzAccount 55 | $tenantId = $creds.Context.Account.Tenants[0] 56 | } 57 | } else { 58 | if ( $TenantId -and $TenantId -ne $ctx.Tenant.TenantId ) { 59 | write-error "You are targeting tenant $tenantId but you are signed in to tennant $($ctx.Tenant.TenantId)" 60 | } 61 | $tenantId = $ctx.Tenant.TenantId 62 | } 63 | 64 | $tenant = Get-AzTenant 65 | $tenantDomainName = ($tenant | Where { $_.Id -eq $tenantId }).Domains[0] 66 | $tenantName = ($tenant | Where { $_.Id -eq $tenantId }).Name 67 | 68 | # Create the client AAD application 69 | $appName = "Verifiable Credentials Python sample" 70 | $appShortName = "vcpythonsample" 71 | $clientAadApplication = Get-AzADApplication -DisplayName $appName 72 | if ($null -ne $clientAadApplication) { 73 | Write-Host "App $appName ($($clientAadApplication.AppId)) already exists" 74 | exit 75 | } 76 | Write-Host "Creating the AAD application ($appName)" 77 | $clientAadApplication = New-AzADApplication -DisplayName $appName ` 78 | -IdentifierUris "https://$tenantDomainName/$appShortName" 79 | $clientServicePrincipal = ($clientAadApplication | New-AzADServicePrincipal) 80 | Write-Host "AppId $($clientAadApplication.AppId)" 81 | # Generate a certificate or client_secret 82 | $client_secret = "" 83 | $certSubject = "" 84 | $certLocation = "" 85 | $certPrivateKey = "" 86 | if ( $ClientCertificate ) { 87 | $certSubject = "CN=$appShortName" 88 | Write-Host "Generating self-signed certificate $certSubject" 89 | # generating a self signed certificate is done differently on Windows vs Mac/Linux 90 | if ( $False -eq $isMacLinux ) { 91 | if (!(Test-Path ".\aadappcert.crt")) { 92 | write-warning "Certificate file 'aadappcert.crt' missing. You need to manually generate it and upload it - see README.md for details" 93 | } 94 | $certLocation = (Resolve-Path ".\aadappcert.crt").Path 95 | $certData = Get-Content $certLocation | Out-String 96 | $certData =[Convert]::ToBase64String( [System.Text.Encoding]::Ascii.GetBytes($certData) ) 97 | } else { # Mac/Linux - generate the self-signed certificate via openssl 98 | & openssl genrsa -out ./aadappcert.pem 2048 99 | & openssl req -new -key ./aadappcert.pem -out ./aadappcert.csr -subj "/$certSubject" 100 | & openssl x509 -req -days 365 -in ./aadappcert.csr -signkey ./aadappcert.pem -out ./aadappcert.crt 101 | $certData = Get-Content ./aadappcert.crt | Out-String 102 | $certData =[Convert]::ToBase64String( [System.Text.Encoding]::Ascii.GetBytes($certData) ) 103 | $certLocation = (Resolve-Path ".$([IO.Path]::DirectorySeparatorChar)aadappcert.crt").Path 104 | } 105 | $certPrivateKey = (Resolve-Path ".$([IO.Path]::DirectorySeparatorChar)aadappcert.pem").Path 106 | $clientAadApplication | New-AzADAppCredential -CertValue $certData 107 | } 108 | if ( $ClientSecret ) { 109 | # Get a 1 year client secret for the client Application 110 | Write-Host "Generating client_secret" 111 | $fromDate = [DateTime]::Now 112 | $appCreds = ($clientAadApplication | New-AzADAppCredential -StartDate $fromDate -EndDate $fromDate.AddYears(1) ) 113 | $client_secret = $appCreds.SecretText 114 | } 115 | 116 | # Add Required Resources Access (from 'client' to 'Verifiable Credential Request Service') 117 | $permissionName = "VerifiableCredential.Create.All" 118 | Write-Host "Adding API Permission $permissionName" 119 | $spVCSR = Get-AzADServicePrincipal -DisplayName "Verifiable Credentials Service Request" 120 | $permissionId = ($spVCSR.AppRole | where {$_.DisplayName -eq $permissionName}).Id 121 | Add-AzADAppPermission -ObjectId $clientAadApplication.Id -ApiId $spVCSR.AppId -PermissionId $permissionId -Type "Role" 122 | 123 | Write-Host "Done creating the client application ($appName)" 124 | 125 | # URL of the AAD application in the Azure portal 126 | # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" 127 | $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" 128 | 129 | # create the HTML file with deployment details 130 | Set-Content -Value "" -Path createdApps.html 131 | Add-Content -Value "" -Path createdApps.html 132 | Add-Content -Value "" -Path createdApps.html 133 | Add-Content -Value "
ApplicationAppIdUrl in the Azure portal
$appName$($clientAadApplication.AppId)$appName
" -Path createdApps.html 134 | 135 | # Update config file for the app 136 | $configFile = $pwd.Path + "$([IO.Path]::DirectorySeparatorChar)..$([IO.Path]::DirectorySeparatorChar)config.json" 137 | Write-Host "Updating the sample code ($configFile)" 138 | $dictionary = @{ "azTenantId" = $tenantId; "azClientId" = $clientAadApplication.AppId; "azClientSecret" = $client_secret; 139 | "azCertificateName" = $certSubject; "azCertificateLocation" = $certLocation.Replace("\", "\\"); "azCertificatePrivateKeyLocation" = $certPrivateKey.Replace("\", "\\") 140 | }; 141 | UpdateTextFile -configFilePath $configFile -dictionary $dictionary 142 | Write-Host "" 143 | Write-Host "IMPORTANT: Please follow the instructions below to complete a few manual step(s) in the Azure portal": 144 | Write-Host "- For '$appName'" 145 | Write-Host " - Navigate to $clientPortalUrl" 146 | Write-Host " - Click on 'Grant admin consent for $tenantName' in the API Permissions page" 147 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/AppCreationScripts/Configure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This bash script is intended to run on Mac/Linux and requires Azure-CLI 4 | # https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-macos 5 | 6 | while getopts t: flag; do 7 | case "${flag}" in 8 | t) tenantId=${OPTARG};; 9 | esac 10 | done 11 | 12 | acct=$(az account show) 13 | if [[ -z $acct ]]; then 14 | if [[ -z $tenantId ]]; then az login; else az login -t $tenantId; fi 15 | fi 16 | 17 | appName="Verifiable Credentials Python sample" 18 | appShortName="vcpythonsample" 19 | 20 | # get things we need 21 | echo "Getting things..." 22 | tenantId=$(az account show --query "tenantId" -o tsv) 23 | tenantDomainName=$(az ad signed-in-user show --query 'userPrincipalName' -o tsv | cut -d '@' -f 2) 24 | 25 | # create the app and the sp 26 | echo "Creating the app and the sp" 27 | appId=$(az ad app create --display-name "$appName" --identifier-uris "https://$tenantDomainName/$appShortName" --query "appId" -o tsv) 28 | spId=$(az ad sp create --id $appId) 29 | 30 | # set the current user as app owner 31 | echo "Assigning owner" 32 | userId=$(az ad signed-in-user show --query objectId -o tsv) 33 | az ad app owner add --id $appId --owner-object-id $userId 34 | 35 | # create a client_secret 36 | echo "Generating client_secret" 37 | clientSecret=$(az ad app credential reset --id $appId --credential-description "Default" --query "password" -o tsv) 38 | 39 | # add permissions 40 | echo "Assigning permissions" 41 | vcsrAppId=$(az ad sp list --display-name "Verifiable Credentials Service Request" --query "[0].appId" -o tsv) 42 | vcsrPermissionId=$(az ad sp list --display-name "Verifiable Credentials Service Request" --query "[0].appRoles" | grep id | cut -d "\"" -f 4) 43 | perm=$(az ad app permission add --id $appId --api $vcsrAppId --api-permissions $vcsrPermissionId=Role) 44 | 45 | # updating the sample config file with details of the app we created 46 | echo "Updating ..\config.json" 47 | sed -i -e "s//$tenantId/g" ../config.json 48 | sed -i -e "s//$appId/g" ../config.json 49 | sed -i -e "s//$clientSecret/g" ../config.json 50 | 51 | # creating report for the user 52 | clientPortalUrl="https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/$appId" 53 | 54 | echo "" 55 | echo "IMPORTANT: Please follow the instructions below to complete a few manual step(s) in the Azure portal" 56 | echo "- For '$appName'" 57 | echo " - Navigate to $clientPortalUrl" 58 | echo " - Click on 'Grant admin consent for $tenantDomainName' in the API Permissions page" 59 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/AppCreationScripts/README.md: -------------------------------------------------------------------------------- 1 | # Registering the Azure Active Directory applications and updating the configuration files for this sample using PowerShell scripts 2 | 3 | ## Overview 4 | 5 | ### Quick summary 6 | 7 | 1. On Windows run PowerShell, navigate to the root of the cloned directory and then run the command: 8 | ```PowerShell 9 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force 10 | ``` 11 | 1. On Mac/Linux, open a terminal, navigate to the root of the cloned directory, and then run command `pwsh` to start Powershell Core. You do not need to .run the `Set-ExecutionPolicy` 12 | 1. If you plan to use a client secret to authenticate the app (which is the default), you can skip this step. On Windows, if you plan to use app authentication via a self-signed certificate, you need to generate the certificate using `openssl` either using Windows Subsystem for Linux or on a Mac/Linux and then copy the files to your Windows computer. Please see details below and complete this step before continuing. 13 | 1. Run the script to create your Azure AD application and configure the code of the sample application accordinly. (Other ways of running the scripts are described below) 14 | ```PowerShell 15 | cd AppCreationScripts 16 | ./Configure.ps1 17 | ``` 18 | 1. Update the `config.json` with the CredentialManifest, IssuerAuthority and VerifierAuthority details 19 | 1. Run the solution via executing `run.cmd` on Windows or `run.sh` on Mac/Linux 20 | 21 | ### Generating Self-Signed Certificate on Windows 22 | 23 | If you plan to use a self-signed certificate for app authentication ***and*** you are using a Windows computer, you need to manually generate the certificate using `openssl` ***before*** you run the Configure.ps1 script. If you are on a Mac/Linux, you don't need to do this manually as it is being done as part of the Configure.ps1 script. There are two ways of generating the self-signed certificate: 24 | 25 | If you have Windows Subsystem for Linux (WSL) enabled on your Windows computer, you can use it. Start WSL and navigate to the AppCreationScripts folder. If you don't have WSL, you need to use a Mac/Linux computer to run the below commands. After the commands have completed, you need to copy the files `aadappcert.pem` and `aadappcert.crt` from your Mac/Linux to your Windows computer. 26 | 27 | ```bash 28 | openssl genrsa -out ./aadappcert.pem 2048 29 | openssl req -new -key ./aadappcert.pem -out ./aadappcert.csr -subj "/CN=vcpythonsample" 30 | openssl x509 -req -days 365 -in ./aadappcert.csr -signkey ./aadappcert.pem -out ./aadappcert.crt 31 | ``` 32 | 33 | If you need to install openssl, you can do it via the below command 34 | 35 | In WSL/Ubuntu 36 | 37 | ```bash 38 | sudo apt-get install openssl 39 | ``` 40 | 41 | On a Mac, you install openssl via brew 42 | 43 | ```bash 44 | brew install openssl 45 | ``` 46 | 47 | ### More details 48 | 49 | The following paragraphs: 50 | 51 | - [Present the scripts](#presentation-of-the-scripts) and explain their [usage patterns](#usage-pattern-for-tests-and-devops-scenarios) for test and DevOps scenarios. 52 | - Explain the [pre-requisites](#pre-requisites) 53 | - Explain [different ways of running the scripts](#different-ways-of-running-the-scripts): 54 | 55 | ## Goal of the scripts 56 | 57 | ### Presentation of the scripts 58 | 59 | This sample comes with two PowerShell scripts, which automate the creation of the Azure Active Directory applications, and the configuration of the code for this sample. Once you run them, you will only need to build the solution and you are good to test. 60 | 61 | These scripts are: 62 | 63 | - `Configure.ps1` which: 64 | - creates Azure AD applications and their related objects (permissions, dependencies, secrets/certificate), 65 | - changes the configuration file `config.json` in the sample code. 66 | - creates a summary file named `createdApps.html` in the folder from which you ran the script, and containing, for each Azure AD application it created: 67 | - the identifier of the application 68 | - the AppId of the application 69 | - the url of its registration in the [Azure portal](https://portal.azure.com). 70 | 71 | - `Cleanup.ps1` which cleans-up the Azure AD objects created by `Configure.ps1`, including deleting the certificate files in the directory. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from VSCode, or from the command line using, for instance, git reset). 72 | 73 | ## How to use the app creation scripts ? 74 | 75 | ### Pre-requisites 76 | 77 | You must have Powershell installed on your machine. To install it, follow the documentation: 78 | - [Windows](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.2) 79 | - [Mac](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-macos?view=powershell-7.2) 80 | - [Linux](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-linux?view=powershell-7.2) 81 | 82 | ### Install Az PowerShell modules 83 | The scripts required PowerShell module Az. To install it, follow the documentation [here](https://docs.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-7.1.0). If you have installed it previously, make sure that you have the latest version by running Powershell command. The Configure.ps1 script may give an error if you have an old version. 84 | 85 | ```powershell 86 | Update-Module -Name Az 87 | ``` 88 | If you do need to run `Update-Module` on Mac/Linux, you need to exit the Powershell shell `pwsh` and enter it again. 89 | 90 | ### openssl on Mac/Linux 91 | 92 | If you are running the `Configure.ps1` script on a Mac/Linux, and you plan to use the option of authenticating as the app using a client certificate, you need to install `openssl` on your computer. How you do this varies with what Linux distro you are using, but on Ubunto, you install openssl by running this in the terminal window 93 | 94 | ```bash 95 | sudo apt-get install openssl 96 | ``` 97 | 98 | On a Mac, you install openssl via brew 99 | 100 | ```bash 101 | brew install openssl 102 | ``` 103 | 104 | ### Different ways of running the scripts 105 | 106 | Using the parameters, the script can be run in the following different ways. Depending on if you haven't signed in from the powershell prompt yet, you will be required to do an interactive signin. If you already have signed in, the script will execute in the current context. You can check if you have a current context via the `Get-AzContext` powershell command. This will show which Azure AD tenant you are currently signed in to. If this is the wrong Azure AD tenant, you can clear the current context with the `Clear-AzContext`. 107 | 108 | If you don't want to sign in every time you execute a script, you can to this via running the following 109 | 110 | ```powershell 111 | $tenantId = "yourTenantIdGuid" 112 | Connect-AzConnect -tenantId $tenantId 113 | ``` 114 | 115 | #### Option 1 116 | 117 | Running the script without any parameters will use the current context, if it exist, or ask the user to sign in interactively. 118 | 119 | ```powershell 120 | .\Configure.ps1 121 | ``` 122 | 123 | #### Option 2 - specify tenant 124 | 125 | Running the script and specifying the tenantId will check that the current is for the specified Azure AD tenant and exit if it is not. If an interactive sign in is needed, it will be for the Azure AD tenant specified in the parameter. Specifying the tenant explicitly avoids accidentally running the script in the wrong tenant. 126 | 127 | ```powershell 128 | .\Configure.ps1 -tenantId $tenantId 129 | ``` 130 | 131 | #### Option 3 - specify type of app credentials 132 | 133 | The default behaviour of the `Configure.ps1` script is to register the app and create a `client secret` as it's credentials. This allows the sample app to authenticate using a client_id and a client_secret. If you instead prefer that the app authenticates via a `client certificate`, you can let the script generate a self-signed certificate and upload it to the app registration. There is also the possibility of creating both a client secret 134 | 135 | ```powershell 136 | .\Configure.ps1 -ClientCertificate 137 | ``` 138 | 139 | ```powershell 140 | .\Configure.ps1 -ClientCertificate -ClientSecret 141 | ``` 142 | 143 | If you use the `-ClientCertificate` option, on Windows, the script will create a self-signed certificate in the user certificate store under Personal\Certificates with the subject `CN=vcaspnetcoresample`. On Mac/Linux, the self-signed certificate will be three files named appaadcert.pem, appaadcert.csr and addaadcert.csr. The `Cleanup.ps1` script will remove the certificate from the certificate store on Windows and delete the files on Mac/Linux. 144 | 145 | #### Option 4 - Cleanup 146 | 147 | This option is if you need cleanup after you are done testing or if you need to re-run the script. The `-ClientCertificate` and `-ClientSecret` parameters are just there for reference to show how it could look. 148 | 149 | ```PowerShell 150 | $tenantId = "yourTenantIdGuid" 151 | . .\Cleanup.ps1 -TenantId $tenantId 152 | . .\Configure.ps1 -TenantId $tenantId -ClientCertificate -ClientSecret 153 | ``` 154 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/CredentialFiles/VerifiedCredentialExpertDisplay.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "en-US", 3 | "card": { 4 | "backgroundColor": "#000000", 5 | "description": "Use your verified credential to prove to anyone that you know all about verifiable credentials.", 6 | "issuedBy": "Microsoft", 7 | "textColor": "#ffffff", 8 | "title": "Verified Credential Expert", 9 | "logo": { 10 | "description": "Verified Credential Expert Logo", 11 | "uri": "https://didcustomerplayground.z13.web.core.windows.net/VerifiedCredentialExpert_icon.png" 12 | } 13 | }, 14 | "consent": { 15 | "instructions": "Sign in with your account to get your card.", 16 | "title": "Do you want to get your Verified Credential?" 17 | }, 18 | "claims": [ 19 | { 20 | "claim": "vc.credentialSubject.firstName", 21 | "label": "First name", 22 | "type": "String" 23 | }, 24 | { 25 | "claim": "vc.credentialSubject.lastName", 26 | "label": "Last name", 27 | "type": "String" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/CredentialFiles/VerifiedCredentialExpertRules.json: -------------------------------------------------------------------------------- 1 | { 2 | "attestations": { 3 | "idTokenHints": [ 4 | { 5 | "mapping": [ 6 | { 7 | "outputClaim": "firstName", 8 | "required": true, 9 | "inputClaim": "$.given_name", 10 | "indexed": false 11 | }, 12 | { 13 | "outputClaim": "lastName", 14 | "required": true, 15 | "inputClaim": "$.family_name", 16 | "indexed": true 17 | } 18 | ], 19 | "required": true 20 | } 21 | ] 22 | }, 23 | "validityInterval": 2592000, 24 | "vc": { 25 | "type": [ 26 | "VerifiedCredentialExpert" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt requirements.txt 6 | RUN pip3 install -r requirements.txt 7 | 8 | COPY . . 9 | 10 | CMD python3 app.py $CONFIGFILE $ISSUANCEFILE $PRESENTATIONFILE 11 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - python 5 | - powershell 6 | products: 7 | - Entra 8 | - Verified ID 9 | description: "A code sample demonstrating issuance and verification of verifiable credentials." 10 | urlFragment: "active-directory-verifiable-credentials-pytnon" 11 | --- 12 | # Verified ID idTokenHint Sample for Python 13 | 14 | This code sample demonstrates how to use Microsoft Entra Verified ID to issue and consume verifiable credentials. 15 | 16 | ## About this sample 17 | 18 | Welcome to Microsoft Entra Verified ID. In this sample, we'll teach you to issue your first verifiable credential: a Verified Credential Expert Card. You'll then use this card to prove to a verifier that you are a Verified Credential Expert, mastered in the art of digital credentialing. The sample uses the preview REST API which supports ID Token hints to pass a payload for the verifiable credential. 19 | 20 | ## Deploy to Azure 21 | 22 | Complete the [setup](#Setup) before deploying to Azure so that you have all the required parameters. 23 | 24 | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Factive-directory-verifiable-credentials-node%2Fmain%2F1-python-api-idtokenhint%2FARMTemplate%2Ftemplate.json) 25 | 26 | You will be asked to enter some parameters during deployment about your app registration and your Verified ID details. You will find these values in the admin portal. 27 | 28 | ![Deployment Parameters](ReadmeFiles/DeployToAzure.png) 29 | 30 | The `photo` claim is for presentation to name the claim in the requested credential when asking for a `FaceCheck`. 31 | For issuance, the sample will use the credential manifest to determind if the credential has a photo or not. 32 | If you have a claim with a [type](https://learn.microsoft.com/en-us/entra/verified-id/rules-and-display-definitions-model#displayclaims-type) of `image/jpg;base64ur`, then the sample will add the selfie or uploaded photo to that claim during issuance. 33 | 34 | ## Test Issuance and Verification 35 | 36 | Once you have deployed this sample to Azure AppServices with a working configuration, you can issue yourself a `VerifiedCredentialExpert` credential and then test verification. 37 | This requires completing the [Verified ID onboarding and creation](https://learn.microsoft.com/en-us/entra/verified-id/verifiable-credentials-configure-issuer) of the `VerifiedCredentialExpert`. 38 | If you want to test presenting and verifying other types and credentials, follow the next section. 39 | 40 | ## Test Verification via templates 41 | 42 | The sample creates a [presentation request](https://learn.microsoft.com/en-us/entra/verified-id/get-started-request-api?tabs=http%2Cconstraints#presentation-request-example) in code based on your configuration in `setenv.cmd`. 43 | You can also use JSON templates to create other presentation requests without changing the configuration to quickly test different scenarios. 44 | This github repo provises four templates for your convenience. Right-click and copy the below links, remove `http://localhost` from the link and append it to your deployed webapp so you have a URL that looks like `.../verifier.html?template=https://...`. 45 | You can issue yourself a `VerifiedEmployee` credential at [MyAccount](https://myaccound.microsoft.com) if your organization have onboarded to Verified ID and enabled MyAccount (doc [here](https://learn.microsoft.com/en-us/entra/verified-id/verifiable-credentials-configure-tenant-quick#myaccount-available-now-to-simplify-issuance-of-workplace-credentials)). 46 | 47 | | Template | Description | Link | 48 | |------|--------|--------| 49 | | TrueIdentity | A presentation request for a [TrueIdentity](https://trueidentityinc.azurewebsites.net/) credential | [Link](http://localhost/verifier.html?template=https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-dotnet/main/1-asp-net-core-api-idtokenhint/Templates/presentation_request_TrueIdentity.json) | 50 | | VerifiedEmployee | A presentation request for a [VerifiedEmployee](https://learn.microsoft.com/en-us/entra/verified-id/how-to-use-quickstart-verifiedemployee) credential | [Link](http://localhost/verifier.html?template=https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-dotnet/main/1-asp-net-core-api-idtokenhint/Templates/presentation_request_VerifiedEmployee.json) | 51 | | VerifiedEmployee with FaceCheck*| A presentation request for a VerifiedEmployee credential that will perform a liveness check in the Authenticator. This requires that you have a good photo of yourself in the VerifiedEmployee credential | [Link](http://localhost/verifier.html?template=https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-dotnet/main/1-asp-net-core-api-idtokenhint/Templates/presentation_request_VerifiedEmployee-FaceCheck.json) | 52 | | VerifiedEmployee with constraints | A presentation request for a VerifiedEmployee credential that uses a claims constraints that `jobTitle` contains the word `manager` | [Link](http://localhost/verifier.html?template=https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-dotnet/main/1-asp-net-core-api-idtokenhint/Templates/presentation_request_VerifiedEmployee-Constraints.json) | 53 | 54 | *Note - FaceCheck is in preview. If you plan to test it, make sure you have the latest Microsoft Authenticator. 55 | 56 | ## Contents 57 | 58 | The project is divided in 2 parts, one for issuance and one for verifying a verifiable credential. Depending on the scenario you need you can remove 1 part. To verify if your environment is completely working you can use both parts to issue a `VerifiedCredentialExpert` credential and verify that as well. 59 | 60 | | Issuance | | 61 | |------|--------| 62 | | public/issuer.html|The basic webpage containing the javascript to call the APIs for issuance. Depending if you use a photo, you will see options to take a selfie or upload a stock photo of you to be issued with the credential. | 63 | | issuer.py | This is the file which contains the API called from the webpage. It calls the REST API after getting an access token through MSAL. | 64 | 65 | | Verification | | 66 | |------|--------| 67 | | public/verifier.html | The website acting as the verifier of the verifiable credential. Depending if you use a photo, you will have a checkbox that let's you create a presentation request with FaceCheck. | 68 | | verifier.py | This is the file which contains the API called from the webpage. It calls the REST API after getting an access token through MSAL and helps verifying the presented verifiable credential. 69 | 70 | | Common | | 71 | |------|--------| 72 | | public/index.html|Start page with option to continue with issuance or verification. | 73 | | public/presentation-verified.html | The webpage that displays the result of the presented VC | 74 | | public/verifiedid.requestservice.client.js|js lib that handles all the API calls to the app | 75 | | public/verifiedid.uihandler.js |js lib that handles common UI updates | 76 | | callback.py | This file handles common functions between issuance and verification. It handles callback event from Request Service API, the polling requests from the browser and generating the selfie request. | 77 | 78 | ## Setup 79 | 80 | Before you can use Verified ID you need to onboard to it. You can either onboard using the [quick setup](https://learn.microsoft.com/en-us/entra/verified-id/verifiable-credentials-configure-tenant-quick) method or the [manual setup](https://learn.microsoft.com/en-us/entra/verified-id/verifiable-credentials-configure-tenant) method. In the manual method, you use your own Azure Key Vault to store your signing key. 81 | 82 | ### Create application registration 83 | Follow the documentation for how to do [app registeration](https://learn.microsoft.com/en-us/entra/verified-id/verifiable-credentials-configure-issuer#configure-the-verifiable-credentials-app) for an app with permission to Verified ID. 84 | 85 | ## Setting up and running the sample 86 | To run the sample, clone the repository, compile & run it. It's callback endpoint must be publically reachable, and for that reason, use a tool like `ngrok` as a reverse proxy to reach your app. 87 | 88 | ```Powershell 89 | git clone https://github.com/Azure-Samples/active-directory-verifiable-credentials-python.git 90 | cd active-directory-verifiable-credentials-python\1-python-api-idtokenhint 91 | ``` 92 | 93 | ### Create your credential 94 | To use the sample we need a configured Verifiable Credential in the azure portal. 95 | In the project directory CredentialFiles you will find the `VerifiedCredentialExpertDisplay.json` file and the `VerifiedCredentialExpertRules.json` file. Use these 2 files to create your own VerifiedCredentialExpert credential. 96 | 97 | If you navigate to your [Verifiable Credentials](https://portal.azure.com/#blade/Microsoft_AAD_DecentralizedIdentity/InitialMenuBlade/issuerSettingsBlade) blade in azure portal, follow the instructions how to create your first verifiable credential. 98 | 99 | You can find the instructions on how to create a Verifiable Credential in the azure portal [here](https://aka.ms/didfordev) 100 | 101 | ### Setting app's configuration 102 | The sample uses environment variables for app configuration. The files [setenv.cmd](setenv.cmd) and [setenv.sh](setenv.sh) contains a template for setting the required environment variables before you run the app. You need to update the files with the appropriate values. 103 | 104 | ```Dos 105 | @echo off 106 | set azTenantId= 107 | set azClientId= 108 | set azClientSecret= 109 | set DidAuthority=did:web:...your-domain....com 110 | set clientName=Python Verified ID sample 111 | set purpose=To prove you are an Verified ID expert 112 | set CredentialManifest=https://verifiedid.did.msidentity.com/v1.0/tenants/...etc... 113 | set CredentialType=VerifiedCredentialExpert 114 | set acceptedIssuers=%DidAuthority% 115 | set issuancePinCodeLength=4 116 | set sourcePhotoClaimName= 117 | set matchConfidenceThreshold=70 118 | ``` 119 | 120 | | Env var | Source | Description | 121 | |------|--------|--------| 122 | | azTenantId | Directory (tenant) id in AppReg blade | Identifies your Entra ID tenant | 123 | | azClientId | Application (client) id in AppReg blade | Identifies your app | 124 | | azClientSecret | Certificates & secrets in AppReg blade | app's client secret | 125 | | DidAuthority | Verified ID blade | Identifies your Verified ID authority | 126 | | clientName | file | Descriptive name that displays in the Authenticator | 127 | | purpose | file | Descriptive purpose that displays in the Authenticator | 128 | | CredentialManifest | verified ID blade | URL to manifest for credential. Used during issuance. | 129 | | CredentialType | Verified ID blade | type name of credential. Used during presentation to ask for type of VC | 130 | | acceptedIssuers | file | ;-separated list of DIDs of issuers you accept in your presentation request | 131 | | issuancePinCodeLength | file | Value 0-6, where 0 means no pin code | 132 | | sourcePhotoClaimName | file | Name of the photo claim if the presentation request is using FaceCheck | 133 | | matchConfidenceThreshold | file | Confidence score threshold for FaceCheck. Dault is 70. | 134 | 135 | ### API Payloads 136 | The sample app doesn't require that you specify JSON payloads in files anymore as it generates the required JSON internally. If you still prefer to use the JSON payloads, you can pass them on the command line and the config values will merge with whatever values are set in environment variables. 137 | 138 | ## Running the sample 139 | 140 | In order to build & run the sample, you need to have the [python](https://www.python.org/downloads/) installed locally. 141 | 142 | 1. After you have edited either of the files [setenv.cmd](setenv.cmd) or [setenv.sh](setenv.sh), depending on your OS, start the python app by running this in the command prompt 143 | 144 | ```Powershell 145 | pip install -r requirements.txt 146 | .\setenv.cmd 147 | python app.py 148 | ``` 149 | 150 | 1. Using a different command prompt, run ngrok to set up a URL on 8080. You can install ngrok globally from this [link](https://ngrok.com/download). 151 | ```Powershell 152 | ngrok http 8080 153 | ``` 154 | 155 | 1. Open the HTTPS URL generated by ngrok. 156 | ![API Overview](ReadmeFiles/ngrok-url-screen.png) 157 | The sample dynamically copies the hostname to be part of the callback URL, this way Verified ID's Request Service can reach your sample web application to execute the callback method. 158 | 159 | 1. Select Issue Credential 160 | 161 | 1. In Authenticator, scan the QR code. 162 | > If this is the first time you are using Verifiable Credentials the Credentials page with the Scan QR button is hidden. You can use the `add account` button. Select `other` and scan the QR code, this will enable the preview of Verifiable Credentials in Authenticator. 163 | 1. Select **Add**. 164 | 165 | ## Verify the verifiable credential by using the sample app 166 | 1. Navigate back and click on the Verify Credential button 167 | 2. Click Verify Credential button 168 | 3. Scan the QR code 169 | 4. select the VerifiedCredentialExpert credential and click share 170 | 5. You should see the result presented on the screen. 171 | 172 | 173 | ## Troubleshooting 174 | 175 | ### Did you forget to provide admin consent? This is needed for confidential apps 176 | If you get an error when calling the API `Insufficient privileges to complete the operation.`, this is because the tenant administrator has not granted permissions 177 | to the application. See step 6 of 'Register the client app' above. 178 | 179 | You will typically see, on the output window, something like the following: 180 | 181 | ```Json 182 | Failed to call the Web Api: Forbidden 183 | Content: { 184 | "error": { 185 | "code": "Authorization_RequestDenied", 186 | "message": "Insufficient privileges to complete the operation.", 187 | "innerError": { 188 | "request-id": "", 189 | "date": "" 190 | } 191 | } 192 | } 193 | ``` 194 | 195 | ### Understanding what's going on 196 | As a first source of information, the Python sample will trace output into the console window of all HTTP calls it receives. Then a good tip is to use Edge/Chrome/Firefox dev tools functionality found under F12 and watch the Network tab for traffic going from the browser to the Node app. 197 | 198 | ## Best practices 199 | When deploying applications which need client credentials and use secrets or certificates the more secure practice is to use certificates. If you are hosting your application on azure make sure you check how to deploy managed identities. This takes away the management and risks of secrets in your application. 200 | You can find more information here: 201 | - [Integrate a daemon app with Key Vault and MSI](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/3-Using-KeyVault) 202 | 203 | 204 | ## More information 205 | 206 | For more information, see MSAL.NET's conceptual documentation: 207 | 208 | - [Quickstart: Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) 209 | - [Quickstart: Configure a client application to access web APIs](https://docs.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis) 210 | - [Acquiring a token for an application with client credential flows](https://aka.ms/msal-net-client-credentials) 211 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/ReadmeFiles/AdminConcent.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-python/5e212649e9cb0aeb4d5c67531a5b549531bf186d/1-python-api-idtokenhint/ReadmeFiles/AdminConcent.PNG -------------------------------------------------------------------------------- /1-python-api-idtokenhint/ReadmeFiles/DeployToAzure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-python/5e212649e9cb0aeb4d5c67531a5b549531bf186d/1-python-api-idtokenhint/ReadmeFiles/DeployToAzure.png -------------------------------------------------------------------------------- /1-python-api-idtokenhint/ReadmeFiles/SampleArchitectureOverview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 77 | 78 | Page-1 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | Boundary.50 87 | 88 | Sheet.51 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Boundary 100 | 101 | Sheet.48 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | Azure Active Directory 110 | Azure Active Directory 111 | 112 | Sheet.2 113 | 114 | 115 | 116 | 117 | 118 | 119 | Sheet.3 120 | 121 | 122 | 123 | 124 | 125 | 126 | Sheet.4 127 | 128 | 129 | 130 | 131 | 132 | 133 | Sheet.5 134 | 135 | 136 | 137 | 138 | 139 | 140 | Sheet.6 141 | 142 | 143 | 144 | 145 | 146 | 147 | Sheet.7 148 | 149 | 150 | 151 | 152 | 153 | 154 | Sheet.8 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | Azure Active Directory 165 | 166 | 167 | Microsoft Azure 168 | Microsoft Azure 169 | 170 | Sheet.10 171 | 179 | 180 | 181 | Sheet.11 182 | 188 | 189 | 190 | 191 | 192 | Microsoft Azure 194 | 195 | 196 | Web App (opaque) (was websites) 197 | Web App 198 | 199 | Sheet.13 200 | 203 | 204 | 205 | Sheet.14 206 | 207 | Sheet.15 208 | 209 | Sheet.16 210 | 216 | 217 | 218 | 219 | Sheet.17 220 | 221 | Sheet.18 222 | 223 | Sheet.19 224 | 225 | Sheet.20 226 | 229 | 230 | 231 | 232 | Sheet.21 233 | 236 | 237 | 238 | Sheet.22 239 | 240 | Sheet.23 241 | 243 | 244 | 245 | 246 | Sheet.24 247 | 248 | Sheet.25 249 | 251 | 252 | 253 | 254 | Sheet.26 255 | 256 | Sheet.27 257 | 260 | 261 | 262 | 263 | Sheet.28 264 | 265 | Sheet.29 266 | 268 | 269 | 270 | 271 | Sheet.30 272 | 273 | Sheet.31 274 | 276 | 277 | 278 | 279 | Sheet.32 280 | 281 | Sheet.33 282 | 284 | 285 | 286 | 287 | Sheet.34 288 | 290 | 291 | 292 | Sheet.35 293 | 295 | 296 | 297 | 298 | Sheet.36 299 | 300 | Sheet.37 301 | 303 | 304 | 305 | 306 | Sheet.38 307 | 308 | Sheet.39 309 | 311 | 312 | 313 | 314 | Sheet.40 315 | 316 | Sheet.41 317 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | Web App 327 | 328 | 329 | Dynamic connector 330 | ① Acquire token client credential (app password) 331 | 332 | 333 | 334 | 335 | ① Acquire tokenclient credential(app password) 338 | 339 | API App 340 | VC Request API 341 | 342 | 343 | 351 | VC Request API 352 | 353 | Dynamic connector.45 354 | ② Create issuance or verification request 355 | 356 | 357 | 358 | 359 | ② Create issuance or verification request 360 | 361 | Server Farm 362 | local 363 | 364 | Sheet.53 365 | 366 | 367 | 368 | 371 | 372 | 373 | Sheet.54 374 | 375 | 376 | 377 | 381 | 382 | 383 | Sheet.55 384 | 385 | 386 | 387 | 388 | 389 | 390 | Sheet.56 391 | 392 | 393 | 394 | 395 | 396 | 397 | Sheet.57 398 | 399 | 400 | 401 | 404 | 405 | 406 | Sheet.58 407 | 408 | 409 | 410 | 414 | 415 | 416 | Sheet.59 417 | 418 | 419 | 420 | 423 | 424 | 425 | Sheet.60 426 | 427 | 428 | 429 | 433 | 434 | 435 | 436 | 437 | local 438 | 439 | 440 | 441 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/ReadmeFiles/ngrok-url-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-python/5e212649e9cb0aeb4d5c67531a5b549531bf186d/1-python-api-idtokenhint/ReadmeFiles/ngrok-url-screen.png -------------------------------------------------------------------------------- /1-python-api-idtokenhint/Templates/README.md: -------------------------------------------------------------------------------- 1 | # Templates 2 | 3 | The templates are located in the [dotnet sample templates folder](https://github.com/Azure-Samples/active-directory-verifiable-credentials-dotnet/tree/main/1-asp-net-core-api-idtokenhint/Templates) 4 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from flask import Flask 4 | from flask import request,Response,redirect 5 | from flask_caching import Cache 6 | from flask.json import jsonify 7 | import json 8 | import logging 9 | import sys, os, tempfile, uuid, time, datetime 10 | import configparser 11 | import argparse 12 | import requests 13 | from random import randint 14 | import msal 15 | import base64 16 | from cryptography.x509 import load_pem_x509_certificate 17 | from cryptography.hazmat.backends import default_backend 18 | from cryptography.hazmat.primitives import hashes 19 | 20 | cacheConfig = { 21 | "DEBUG": True, # some Flask specific configs 22 | "CACHE_TYPE": "SimpleCache", # Flask-Caching related configs 23 | "CACHE_DEFAULT_TIMEOUT": 300 24 | } 25 | 26 | app = Flask(__name__,static_url_path='',static_folder='public',template_folder='public') 27 | 28 | app.config.from_mapping(cacheConfig) 29 | cache = Cache(app) 30 | 31 | log = logging.getLogger() 32 | log.setLevel(logging.INFO) 33 | 34 | def base64JwtTokenToJson(base64String): 35 | token = base64String.split(".")[1] 36 | token = token + "===="[len(token)%4:4] 37 | return json.loads(base64.urlsafe_b64decode(token.encode('utf-8')).decode('utf-8')) 38 | 39 | config = { 40 | "azTenantId": os.getenv('azTenantId'), 41 | "azClientId": os.getenv('azClientId'), 42 | "azClientSecret": os.getenv('azClientSecret'), 43 | "azCertificateName": os.getenv('azCertificateName'), 44 | "azCertificateLocation": os.getenv('azCertificateLocation'), 45 | "azCertificatePrivateKeyLocation": os.getenv('azCertificatePrivateKeyLocation'), 46 | "CredentialManifest": os.getenv('CredentialManifest'), 47 | "CredentialType": os.getenv('CredentialType'), 48 | "DidAuthority": os.getenv('DidAuthority'), 49 | "acceptedIssuers": os.getenv('acceptedIssuers'), 50 | "clientName": os.getenv('clientName'), 51 | "purpose": os.getenv('purpose'), 52 | "issuancePinCodeLength": os.getenv('issuancePinCodeLength') 53 | } 54 | 55 | idx = sys.argv.index('-c') if '-c' in sys.argv else -1 56 | if idx >= 0: 57 | print("Loading config from file: " + sys.argv[idx+1]) 58 | config = json.load(open(sys.argv[idx+1])) 59 | 60 | config["apiKey"] = str(uuid.uuid4()) 61 | print(config) 62 | 63 | # Check that the manifestURL have the matching tenantId with the config file 64 | manifestTenantId = config["CredentialManifest"].split("/")[5] 65 | if config["azTenantId"] != manifestTenantId: 66 | raise ValueError("TenantId in ManifestURL " + manifestTenantId + " does not match tenantId in config file " + config["azTenantId"]) 67 | 68 | # Check that the issuer in the config file match the manifest 69 | r = requests.get(config["CredentialManifest"]) 70 | resp = r.json() 71 | manifest = base64JwtTokenToJson( resp["token"] ) 72 | config["manifest"] = manifest 73 | if config["DidAuthority"] == "": 74 | config["DidAuthority"] = manifest["iss"] 75 | if config["DidAuthority"] == "": 76 | config["DidAuthority"] = manifest["iss"] 77 | if config["DidAuthority"] != manifest["iss"]: 78 | raise ValueError("Wrong DidAuthority in config file " + config["DidAuthority"] + ". Issuer in manifest is " + manifest["iss"]) 79 | 80 | msalCca = msal.ConfidentialClientApplication( config["azClientId"], 81 | authority="https://login.microsoftonline.com/" + config["azTenantId"], 82 | client_credential=config["azClientSecret"], 83 | ) 84 | 85 | if config["azCertificateName"] is not None and config["azCertificateName"] != "": 86 | with open(config["azCertificatePrivateKeyLocation"], "rb") as file: 87 | private_key = file.read() 88 | with open(config["azCertificateLocation"]) as file: 89 | public_certificate = file.read() 90 | cert = load_pem_x509_certificate(data=bytes(public_certificate, 'UTF-8'), backend=default_backend()) 91 | thumbprint = (cert.fingerprint(hashes.SHA1()).hex()) 92 | print("Cert based auth using thumbprint: " + thumbprint) 93 | msalCca = msal.ConfidentialClientApplication( config["azClientId"], 94 | authority="https://login.microsoftonline.com/" + config["azTenantId"], 95 | client_credential={ 96 | "private_key": private_key, 97 | "thumbprint": thumbprint, 98 | "public_certificate": public_certificate 99 | } 100 | ) 101 | 102 | # Check if it is an EU tenant and set up the endpoint for it 103 | r = requests.get("https://login.microsoftonline.com/" + config["azTenantId"] + "/v2.0/.well-known/openid-configuration") 104 | resp = r.json() 105 | print("tenant_region_scope = " + resp["tenant_region_scope"]) 106 | config["tenant_region_scope"] = resp["tenant_region_scope"] 107 | config["msIdentityHostName"] = "https://verifiedid.did.msidentity.com/v1.0/" 108 | # Check that the Credential Manifest URL is in the same tenant Region and throw an error if it's not 109 | if False == config["CredentialManifest"].startswith( config["msIdentityHostName"] ): 110 | raise ValueError("Error in config file. CredentialManifest URL configured for wrong tenant region. Should start with: " + config["msIdentityHostName"]) 111 | 112 | # check that we a) can acquire an access_token and b) that it has the needed permission for this sample 113 | result = msalCca.acquire_token_for_client( scopes=["3db474b9-6a0c-4840-96ac-1fceb342124f/.default"] ) 114 | if "access_token" in result: 115 | print( result['access_token'] ) 116 | token = base64JwtTokenToJson( result["access_token"] ) 117 | print( token["roles"]) 118 | if "VerifiableCredential.Create.All" not in token["roles"]: 119 | raise ValueError("Access token do not have the required scope 'VerifiableCredential.Create.All'.") 120 | else: 121 | raise ValueError(result.get("error") + result.get("error_description")) 122 | 123 | if __name__ == "__main__": 124 | import issuer 125 | import verifier 126 | import callback 127 | 128 | @app.route('/') 129 | def root(): 130 | return app.send_static_file('index.html') 131 | 132 | @app.route("/echo", methods = ['GET']) 133 | def echoApi(): 134 | result = { 135 | 'date': datetime.datetime.utcnow().isoformat(), 136 | 'api': request.url, 137 | 'Host': request.headers.get('host'), 138 | 'x-forwarded-for': request.headers.get('x-forwarded-for'), 139 | 'x-original-host': request.headers.get('x-original-host'), 140 | 'DidAuthority': config["DidAuthority"], 141 | 'manifestURL': config["CredentialManifest"], 142 | 'clientId': config["azClientId"] 143 | } 144 | return Response( json.dumps(result), status=200, mimetype='application/json') 145 | 146 | if __name__ == "__main__": 147 | port = os.getenv('PORT') 148 | if port is None: 149 | port = 8080 150 | app.run(host="0.0.0.0", port=port) -------------------------------------------------------------------------------- /1-python-api-idtokenhint/callback.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from flask import Flask 4 | from flask import request,Response,redirect 5 | from flask_caching import Cache 6 | from flask.json import jsonify 7 | import json 8 | import logging 9 | import sys, os, tempfile, uuid, time, datetime 10 | import configparser 11 | import argparse 12 | import requests 13 | import jwt, base64 14 | import msal 15 | 16 | from __main__ import app 17 | from __main__ import cache 18 | from __main__ import log 19 | from __main__ import config 20 | from __main__ import base64JwtTokenToJson 21 | 22 | 23 | @app.route("/api/request-callback", methods = ['POST']) 24 | def presentationRequestApiCallback(): 25 | """ This method is called by the VC Request API when the user scans a QR code and presents a Verifiable Credential to the service """ 26 | callbackBody = request.json 27 | print(callbackBody) 28 | if request.headers['api-key'] != config["apiKey"]: 29 | print("api-key wrong or missing") 30 | return Response( jsonify({'error':'api-key wrong or missing'}), status=401, mimetype='application/json') 31 | if callbackBody["requestStatus"] == "request_retrieved": 32 | cacheData = { 33 | "status": callbackBody["requestStatus"], 34 | "message": "QR Code is scanned. Waiting for validation..." 35 | } 36 | elif callbackBody["requestStatus"] == "issuance_successful": 37 | cacheData = { 38 | "status": callbackBody["requestStatus"], 39 | "message": "Credential successfully issued" 40 | } 41 | elif callbackBody["requestStatus"] == "issuance_error": 42 | cacheData = { 43 | "status": callbackBody["requestStatus"], 44 | "message": callbackBody["error"]["message"] 45 | } 46 | elif callbackBody["requestStatus"] == "presentation_verified": 47 | cacheData = { 48 | "status": callbackBody["requestStatus"], 49 | "message": "Presentation received", 50 | "payload": callbackBody["verifiedCredentialsData"], 51 | "subject": callbackBody["subject"], 52 | "presentationResponse": callbackBody 53 | } 54 | if callbackBody["receipt"] is not None: 55 | vp_token = base64JwtTokenToJson(callbackBody["receipt"]["vp_token"]) 56 | vc = base64JwtTokenToJson(vp_token["vp"]["verifiableCredential"][0]) 57 | cacheData["jti"] = vc["jti"] 58 | elif callbackBody["requestStatus"] == "presentation_error": 59 | cacheData = { 60 | "status": callbackBody["requestStatus"], 61 | "message": callbackBody["error"]["message"] 62 | } 63 | else: 64 | print("400 - Unsupported requestStatus: {0}".format(callbackBody["requestStatus"]) ) 65 | return Response( jsonify({'error':'Unsupported requestStatus: {0}'.format(callbackBody["requestStatus"])}), status=400, mimetype='application/json') 66 | 67 | data = cache.get(callbackBody["state"]) 68 | if data is None: 69 | print("400 - Unknown state: {0}".format(callbackBody["state"]) ) 70 | return Response( jsonify({'error':'Unknown state: {0}'.format(callbackBody["state"])}), status=400, mimetype='application/json') 71 | print("200 - state: {0}".format(cacheData) ) 72 | cache.set( callbackBody["state"], json.dumps(cacheData) ) 73 | return "" 74 | 75 | @app.route("/api/request-status", methods = ['GET']) 76 | def presentationRequestStatus(): 77 | """ this function is called from the UI polling for a response from the Verified ID Service. 78 | when a callback is recieved at the presentationCallback service the session will be updated 79 | this method will respond with the status so the UI can reflect if the QR code was scanned and with the result of the presentation 80 | """ 81 | id = request.args.get('id') 82 | print(id) 83 | data = cache.get(id) 84 | print(data) 85 | if data is not None: 86 | cacheData = json.loads(data) 87 | response = Response( json.dumps(cacheData), status=200, mimetype='application/json') 88 | else: 89 | response = Response( "", status=400, mimetype='application/json') 90 | response.headers.add('Access-Control-Allow-Origin', '*') 91 | return response 92 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "azTenantId": "", 3 | "azClientId": "", 4 | "azClientSecret": "", 5 | "azCertificateName": "", 6 | "azCertThumbprint": "", 7 | "azCertificatePrivateKeyLocation": "", 8 | "CredentialManifest": "", 9 | "DidAuthority": "", 10 | "acceptedIssuers": "did:web:...your-did...", 11 | "CredentialType": "VerifiedCredentialExpert", 12 | "issuancePinCodeLength": 4, 13 | "sourcePhotoClaimName": "photo", 14 | "matchConfidenceThreshold": 70 15 | } 16 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/docker-build.cmd: -------------------------------------------------------------------------------- 1 | docker build -t python-aadvc-api-idtokenhint . -------------------------------------------------------------------------------- /1-python-api-idtokenhint/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build -t python-aadvc-api-idtokenhint . -------------------------------------------------------------------------------- /1-python-api-idtokenhint/docker-run.cmd: -------------------------------------------------------------------------------- 1 | docker run --rm -it -p 8080:8080 ^ 2 | -e azTenantId='' ^ 3 | -e azClientId='' ^ 4 | -e azClientSecret='' ^ 5 | -e DidAuthority='did:web:...etc...' ^ 6 | -e clientName='Python Verified ID sample' ^ 7 | -e purpose='To prove you are an Verified ID expert' ^ 8 | -e CredentialManifest='https://verifiedid.did.msidentity.com/v1.0/tenants/...etc...' ^ 9 | -e CredentialType=VerifiedCredentialExpert ^ 10 | -e acceptedIssuers='did:web:...etc...' ^ 11 | -e issuancePinCodeLength=4 ^ 12 | -e sourcePhotoClaimName= ^ 13 | -e matchConfidenceThreshold=70 ^ 14 | python-aadvc-api-idtokenhint:latest -------------------------------------------------------------------------------- /1-python-api-idtokenhint/docker-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run --rm -it -p 8080:8080 \ 4 | -e azTenantId='' \ 5 | -e azClientId='' \ 6 | -e azClientSecret='' \ 7 | -e DidAuthority='did:web:...etc...' \ 8 | -e clientName='Python Verified ID sample' \ 9 | -e purpose='To prove you are an Verified ID expert' \ 10 | -e CredentialManifest='https://verifiedid.did.msidentity.com/v1.0/tenants/...etc...' \ 11 | -e CredentialType=VerifiedCredentialExpert \ 12 | -e acceptedIssuers='did:web:...etc...' \ 13 | -e issuancePinCodeLength=4 \ 14 | -e sourcePhotoClaimName= \ 15 | -e matchConfidenceThreshold=70 \ 16 | python-aadvc-api-idtokenhint:latest 17 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/issuer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from flask import Flask 4 | from flask import request,Response,redirect 5 | from flask_caching import Cache 6 | from flask.json import jsonify 7 | import json 8 | import logging 9 | import sys, os, tempfile, uuid, time, datetime 10 | import configparser 11 | import argparse 12 | import requests 13 | from random import randint 14 | import msal 15 | 16 | from __main__ import app 17 | from __main__ import cache 18 | from __main__ import log 19 | from __main__ import config 20 | from __main__ import msalCca 21 | 22 | issuanceConfig = { 23 | "includeQRCode": False, 24 | "callback": { 25 | "url": "...set at runtime...", 26 | "state": "...set at runtime...", 27 | "headers": { 28 | "api-key": "...set at runtime..." 29 | } 30 | }, 31 | "authority": "...set at runtime...", 32 | "registration": { 33 | "clientName": config["clientName"], 34 | "purpose": config["purpose"] 35 | }, 36 | "type": "ignore-this", 37 | "manifest": config["CredentialManifest"], 38 | "pin": { 39 | "value": "1234", 40 | "length": 4 41 | }, 42 | "claims": { 43 | "given_name": "FIRSTNAME", 44 | "family_name": "LASTNAME" 45 | } 46 | } 47 | 48 | idx = sys.argv.index('-i') if '-i' in sys.argv else -1 49 | if idx >= 0: 50 | print("Loading issuanceRequest from file: " + sys.argv[idx+1]) 51 | issuanceConfig = json.load(open(sys.argv[idx+1])) 52 | 53 | issuanceConfig["authority"] = config["DidAuthority"] 54 | issuanceConfig["callback"]["headers"]["api-key"] = config["apiKey"] 55 | 56 | if "pin" in issuanceConfig is not None: 57 | if int(issuanceConfig["pin"]["length"]) == 0: 58 | del issuanceConfig["pin"] 59 | 60 | @app.route("/api/issuer/issuance-request", methods = ['GET']) 61 | def issuanceRequest(): 62 | """ This method is called from the UI to initiate the issuance of the verifiable credential """ 63 | id = str(uuid.uuid4()) 64 | cache.set( id, json.dumps({ 65 | "status" : "request_created", 66 | "message": "Waiting for QR code to be scanned" 67 | })) 68 | accessToken = "" 69 | result = msalCca.acquire_token_for_client( scopes=["3db474b9-6a0c-4840-96ac-1fceb342124f/.default"] ) 70 | if "access_token" in result: 71 | print( result['access_token'] ) 72 | accessToken = result['access_token'] 73 | else: 74 | print(result.get("error") + result.get("error_description")) 75 | 76 | payload = issuanceConfig.copy() 77 | payload["callback"]["url"] = str(request.url_root).replace("http://", "https://") + "api/request-callback" 78 | payload["callback"]["state"] = id 79 | pinCode = 0 80 | if "pin" in payload is not None: 81 | """ don't use pin if user is on mobile device """ 82 | if "Android" in request.headers['user-agent'] or "iPhone" in request.headers['user-agent']: 83 | del payload["pin"] 84 | else: 85 | pinCode = ''.join(str(randint(0,9)) for _ in range(int(payload["pin"]["length"]))) 86 | payload["pin"]["value"] = pinCode 87 | if "claims" in payload is not None: 88 | if "given_name" in payload["claims"] is not None: 89 | payload["claims"]["given_name"] = "Megan" 90 | if "family_name" in payload["claims"] is not None: 91 | payload["claims"]["family_name"] = "Bowen" 92 | print( json.dumps(payload) ) 93 | post_headers = { "content-type": "application/json", "Authorization": "Bearer " + accessToken } 94 | client_api_request_endpoint = config["msIdentityHostName"] + "verifiableCredentials/createIssuanceRequest" 95 | print( client_api_request_endpoint ) 96 | r = requests.post( client_api_request_endpoint 97 | , headers=post_headers, data=json.dumps(payload)) 98 | resp = r.json() 99 | print(resp) 100 | resp["id"] = id 101 | if "pin" in payload is not None: 102 | resp["pin"] = pinCode 103 | #print(resp) 104 | return Response( json.dumps(resp), status=200, mimetype='application/json') 105 | 106 | @app.route("/api/issuer/get-manifest", methods = ['GET']) 107 | def getManifest(): 108 | return Response( json.dumps(config["manifest"]), status=200, mimetype='application/json') 109 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/presentation_request_trueidentity.json: -------------------------------------------------------------------------------- 1 | { 2 | "includeQRCode": false, 3 | "callback": { 4 | "url": "...set at runtime...", 5 | "state": "...set at runtime...", 6 | "headers": { 7 | "api-key": "...set at runtime..." 8 | } 9 | }, 10 | "authority": "...set at runtime...", 11 | "registration": { 12 | "clientName": "TrueIdentity Verifier", 13 | "purpose": "So we can see your identity has been verified by True Identity" 14 | }, 15 | "includeReceipt": true, 16 | "requestedCredentials": [ 17 | { 18 | "type": "TrueIdentity", 19 | "acceptedIssuers": [ "did:web:did.woodgrovedemo.com" ], 20 | "configuration": { 21 | "validation": { 22 | "allowRevoked": true, 23 | "validateLinkedDomain": true 24 | } 25 | } 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/VerifiedCredentialExpert-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-python/5e212649e9cb0aeb4d5c67531a5b549531bf186d/1-python-api-idtokenhint/public/VerifiedCredentialExpert-icon.png -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/authenticator-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-python/5e212649e9cb0aeb4d5c67531a5b549531bf186d/1-python-api-idtokenhint/public/authenticator-icon.png -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-python/5e212649e9cb0aeb4d5c67531a5b549531bf186d/1-python-api-idtokenhint/public/favicon.ico -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/active-directory-verifiable-credentials-python/5e212649e9cb0aeb4d5c67531a5b549531bf186d/1-python-api-idtokenhint/public/favicon.png -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | Verifiable Credentials Issuance and Verifier Sample 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |

Verifiable Credentials Issuance and Verifier Sample

20 | 21 | 22 | 23 | 33 |
34 | 35 |
36 | 37 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/issuer.html: -------------------------------------------------------------------------------- 1 |  2 | 4 | 5 | 6 | 7 | 8 | Verifiable Credentials Request API Sample - Issuer 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |

Verifiable Credential Issuance

20 |

21 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |

45 | 46 |
47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 105 |
106 |
107 | 108 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/presentation-verified.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Verifiable Credential Expert Request API Sample - Verifier 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |

VerifiedEmployee Presentation

20 | 21 | 22 |
23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
ClaimsValue
34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
VC DetailsValue
44 | 45 |
46 | 47 | 48 | 49 | 152 |
153 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/qrcode.min.js: -------------------------------------------------------------------------------- 1 | var QRCode;!function(){function t(t){this.mode=r.MODE_8BIT_BYTE,this.data=t,this.parsedData=[];for(var e=0,o=this.data.length;e65536?(i[0]=240|(1835008&n)>>>18,i[1]=128|(258048&n)>>>12,i[2]=128|(4032&n)>>>6,i[3]=128|63&n):n>2048?(i[0]=224|(61440&n)>>>12,i[1]=128|(4032&n)>>>6,i[2]=128|63&n):n>128?(i[0]=192|(1984&n)>>>6,i[1]=128|63&n):i[0]=n,this.parsedData.push(i)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function e(t,e){this.typeNumber=t,this.errorCorrectLevel=e,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}t.prototype={getLength:function(t){return this.parsedData.length},write:function(t){for(var e=0,r=this.parsedData.length;e=7&&this.setupTypeNumber(t),null==this.dataCache&&(this.dataCache=e.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,r)},setupPositionProbePattern:function(t,e){for(var r=-1;r<=7;r++)if(!(t+r<=-1||this.moduleCount<=t+r))for(var o=-1;o<=7;o++)e+o<=-1||this.moduleCount<=e+o||(this.modules[t+r][e+o]=0<=r&&r<=6&&(0==o||6==o)||0<=o&&o<=6&&(0==r||6==r)||2<=r&&r<=4&&2<=o&&o<=4)},getBestMaskPattern:function(){for(var t=0,e=0,r=0;r<8;r++){this.makeImpl(!0,r);var o=g.getLostPoint(this);(0==r||t>o)&&(t=o,e=r)}return e},createMovieClip:function(t,e,r){var o=t.createEmptyMovieClip(e,r);this.make();for(var i=0;i>r&1);this.modules[Math.floor(r/3)][r%3+this.moduleCount-8-3]=o}for(r=0;r<18;r++){o=!t&&1==(e>>r&1);this.modules[r%3+this.moduleCount-8-3][Math.floor(r/3)]=o}},setupTypeInfo:function(t,e){for(var r=this.errorCorrectLevel<<3|e,o=g.getBCHTypeInfo(r),i=0;i<15;i++){var n=!t&&1==(o>>i&1);i<6?this.modules[i][8]=n:i<8?this.modules[i+1][8]=n:this.modules[this.moduleCount-15+i][8]=n}for(i=0;i<15;i++){n=!t&&1==(o>>i&1);i<8?this.modules[8][this.moduleCount-i-1]=n:i<9?this.modules[8][15-i-1+1]=n:this.modules[8][15-i-1]=n}this.modules[this.moduleCount-8][8]=!t},mapData:function(t,e){for(var r=-1,o=this.moduleCount-1,i=7,n=0,a=this.moduleCount-1;a>0;a-=2)for(6==a&&a--;;){for(var s=0;s<2;s++)if(null==this.modules[o][a-s]){var h=!1;n>>i&1)),g.getMask(e,o,a-s)&&(h=!h),this.modules[o][a-s]=h,-1==--i&&(n++,i=7)}if((o+=r)<0||this.moduleCount<=o){o-=r,r=-r;break}}}},e.PAD0=236,e.PAD1=17,e.createData=function(t,r,o){for(var i=m.getRSBlocks(t,r),n=new _,a=0;a8*h)throw new Error("code length overflow. ("+n.getLengthInBits()+">"+8*h+")");for(n.getLengthInBits()+4<=8*h&&n.put(0,4);n.getLengthInBits()%8!=0;)n.putBit(!1);for(;!(n.getLengthInBits()>=8*h||(n.put(e.PAD0,8),n.getLengthInBits()>=8*h));)n.put(e.PAD1,8);return e.createBytes(n,i)},e.createBytes=function(t,e){for(var r=0,o=0,i=0,n=new Array(e.length),a=new Array(e.length),s=0;s=0?d.get(c):0}}var m=0;for(u=0;u=0;)e^=g.G15<=0;)e^=g.G18<>>=1;return e},getPatternPosition:function(t){return g.PATTERN_POSITION_TABLE[t-1]},getMask:function(t,e,r){switch(t){case i:return(e+r)%2==0;case n:return e%2==0;case a:return r%3==0;case s:return(e+r)%3==0;case h:return(Math.floor(e/2)+Math.floor(r/3))%2==0;case l:return e*r%2+e*r%3==0;case u:return(e*r%2+e*r%3)%2==0;case f:return(e*r%3+(e+r)%2)%2==0;default:throw new Error("bad maskPattern:"+t)}},getErrorCorrectPolynomial:function(t){for(var e=new p([1],0),r=0;r5&&(r+=3+n-5)}for(o=0;o=256;)t-=255;return d.EXP_TABLE[t]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},c=0;c<8;c++)d.EXP_TABLE[c]=1<>>7-t%8&1)},put:function(t,e){for(var r=0;r>>e-r-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var e=Math.floor(this.length/8);this.buffer.length<=e&&this.buffer.push(0),t&&(this.buffer[e]|=128>>>this.length%8),this.length++}};var v=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];function C(){var t=!1,e=navigator.userAgent;if(/android/i.test(e)){t=!0;var r=e.toString().match(/android ([0-9]\.[0-9])/i);r&&r[1]&&(t=parseFloat(r[1]))}return t}var w=function(){var t=function(t,e){this._el=t,this._htOption=e};return t.prototype.draw=function(t){var e=this._htOption,r=this._el,o=t.getModuleCount();Math.floor(e.width/o),Math.floor(e.height/o);function i(t,e){var r=document.createElementNS("http://www.w3.org/2000/svg",t);for(var o in e)e.hasOwnProperty(o)&&r.setAttribute(o,e[o]);return r}this.clear();var n=i("svg",{viewBox:"0 0 "+String(o)+" "+String(o),width:"100%",height:"100%",fill:e.colorLight});n.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),r.appendChild(n),n.appendChild(i("rect",{fill:e.colorLight,width:"100%",height:"100%"})),n.appendChild(i("rect",{fill:e.colorDark,width:"1",height:"1",id:"template"}));for(var a=0;a'],s=0;s");for(var h=0;h');a.push("")}a.push(""),r.innerHTML=a.join("");var l=r.childNodes[0],u=(e.width-l.offsetWidth)/2,f=(e.height-l.offsetHeight)/2;u>0&&f>0&&(l.style.margin=f+"px "+u+"px")},t.prototype.clear=function(){this._el.innerHTML=""},t}():function(){function t(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}if(this._android&&this._android<=2.1){var e=1/window.devicePixelRatio,r=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(t,o,i,n,a,s,h,l,u){if("nodeName"in t&&/img/i.test(t.nodeName))for(var f=arguments.length-1;f>=1;f--)arguments[f]=arguments[f]*e;else void 0===l&&(arguments[1]*=e,arguments[2]*=e,arguments[3]*=e,arguments[4]*=e);r.apply(this,arguments)}}var o=function(t,e){this._bIsPainted=!1,this._android=C(),this._htOption=e,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=e.width,this._elCanvas.height=e.height,t.appendChild(this._elCanvas),this._el=t,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.alt="Scan me!",this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return o.prototype.draw=function(t){var e=this._elImage,r=this._oContext,o=this._htOption,i=t.getModuleCount(),n=o.width/i,a=o.height/i,s=Math.round(n),h=Math.round(a);e.style.display="none",this.clear();for(var l=0;lv.length)throw new Error("Too long data");return r}(QRCode=function(t,e){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:o.H},"string"==typeof e&&(e={text:e}),e)for(var r in e)this._htOption[r]=e[r];"string"==typeof t&&(t=document.getElementById(t)),this._htOption.useSVG&&(D=w),this._android=C(),this._el=t,this._oQRCode=null,this._oDrawing=new D(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)}).prototype.makeCode=function(t){this._oQRCode=new e(A(t,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(t),this._oQRCode.make(),this._el.title=t,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=o}(),"undefined"!=typeof module&&(module.exports=QRCode); 2 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | background-color: #E0E0E0; 4 | } 5 | 6 | #wrap { 7 | margin: 0 auto; 8 | width: 800px; 9 | } 10 | 11 | .button { 12 | background-color: #3B2E58; 13 | border: none; 14 | color: white; 15 | padding: 15px 32px; 16 | text-align: center; 17 | text-decoration: none; 18 | border-radius: 8px; 19 | display: inline-block; 20 | font-size: 16px; 21 | font-weight: bold; 22 | } 23 | 24 | .dark-purple-fill { 25 | background-color: #3B2E58; 26 | } 27 | 28 | .light-purple-fill { 29 | background-color: #8661c5; 30 | } 31 | 32 | .icon-small { 33 | width: 50px; 34 | padding-bottom: 5px; 35 | } 36 | 37 | .icon-text-large { 38 | font-size: 100px; 39 | } 40 | 41 | .small-text { 42 | font-size: 16px; 43 | } 44 | 45 | .tiny-text { 46 | font-size: 12px; 47 | } 48 | 49 | .text-gray { 50 | color: gray; 51 | } 52 | 53 | .text-center { 54 | text-align: center; 55 | } 56 | 57 | #qrcode>img { 58 | margin: 0 auto; 59 | padding-top: 25px; 60 | } 61 | 62 | .margin-top-50 { 63 | margin-top: 50px; 64 | } 65 | 66 | .margin-top-75 { 67 | margin-top: 75px; 68 | } 69 | 70 | .margin-bottom-25 { 71 | margin-bottom: 25px; 72 | } 73 | 74 | .margin-bottom-50 { 75 | margin-bottom: 50px; 76 | } 77 | 78 | .margin-bottom-75 { 79 | margin-bottom: 50px; 80 | } 81 | 82 | .margin-bottom-100 { 83 | margin-bottom: 100px; 84 | } 85 | 86 | .green { 87 | color: #228B22; 88 | } 89 | 90 | #message { 91 | font-size: 1.5em; 92 | } 93 | 94 | /*------*/ 95 | 96 | table { 97 | font-family: Arial, Helvetica, sans-serif; 98 | border-collapse: collapse; 99 | width: 100%; 100 | } 101 | 102 | table td, table th { 103 | border: 1px solid #ddd; 104 | padding: 8px; 105 | } 106 | 107 | table tr:nth-child(even){background-color: #f2f2f2;} 108 | 109 | table tr:hover {background-color: #ddd;} 110 | 111 | table th { 112 | padding-top: 12px; 113 | padding-bottom: 12px; 114 | text-align: left; 115 | background-color: #0f0f0f; 116 | color: white; 117 | } -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/verifiedid.requestservice.client.js: -------------------------------------------------------------------------------- 1 | function RequestService(onDrawQRCode, onNavigateToDeepLink, onRequestRetrieved, onPresentationVerified, onIssuanceSuccesful, onSelfieTaken, onError, pollFrequency) { 2 | this.onDrawQRCode = onDrawQRCode; 3 | this.onNavigateToDeepLink = onNavigateToDeepLink; 4 | this.onRequestRetrieved = onRequestRetrieved; 5 | this.onPresentationVerified = onPresentationVerified; 6 | this.onIssuanceSuccesful = onIssuanceSuccesful; 7 | this.onSelfieTaken = onSelfieTaken; 8 | this.onError = onError; 9 | this.pollFrequency = (pollFrequency == undefined ? 1000 : pollFrequency); 10 | this.apiCreateIssuanceRequest = '/api/issuer/issuance-request'; 11 | this.apiSetPhoto = '/api/issuer/userphoto'; 12 | this.apiCreateSelfieRequest = '/api/issuer/selfie-request'; 13 | this.apiCreatePresentationRequest = '/api/verifier/presentation-request'; 14 | this.apiPollPresentationRequest = '/api/request-status'; 15 | this.logEnabled = false; 16 | this.log = function (msg) { 17 | if (this.logEnabled) { 18 | console.log(msg); 19 | } 20 | } 21 | this.request = null; 22 | this.requestType = ""; 23 | this.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' 24 | .replace(/[xy]/g, function (c) { 25 | const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 26 | return v.toString(16); 27 | }); 28 | 29 | // function to create a presentation request 30 | this.createRequest = async function (url) { 31 | const response = await fetch(url, {method: 'GET', headers: { 'Accept': 'application/json', 'rsid': this.uuid } }); 32 | const respJson = await response.json(); 33 | console.log(respJson); 34 | if (respJson.error_description) { 35 | this.onError(this.requestType, respJson.error_description); 36 | } else { 37 | this.request = respJson; 38 | if (/Android/i.test(navigator.userAgent) || /iPhone/i.test(navigator.userAgent)) { 39 | this.log(`Mobile device (${navigator.userAgent})! Using deep link (${respJson.url}).`); 40 | this.onNavigateToDeepLink(this.requestType, respJson.id, respJson.url); 41 | } else { 42 | this.log(`Not Android or IOS. Generating QR code encoded with ${message}`); 43 | this.onDrawQRCode(this.requestType, respJson.id, respJson.url, respJson.pin); 44 | this.pollRequestStatus(respJson.id); 45 | } 46 | } 47 | }; 48 | this.createPresentationRequest = function () { 49 | this.requestType = "presentation"; 50 | this.createRequest(this.apiCreatePresentationRequest) 51 | }; 52 | this.createIssuanceRequest = function () { 53 | this.requestType = "issuance"; 54 | this.createRequest(this.apiCreateIssuanceRequest) 55 | }; 56 | this.createSelfieRequest = function () { 57 | this.requestType = "selfie"; 58 | this.createRequest(this.apiCreateSelfieRequest); 59 | }; 60 | 61 | // function to pull for presentation status 62 | this.pollRequestStatus = function (id) { 63 | var _rsThis = this; 64 | var pollFlag = setInterval(async function () { 65 | var tmNow = (new Date()).getTime() / 1000; 66 | if ((tmNow - 10) > _rsThis.request.expiry) { 67 | clearInterval(pollFlag); 68 | _rsThis.log(`${(tmNow - 10)} > ${_rsThis.request.expiry}`); 69 | _rsThis.onError( _rsThis.requestType, { error: "timeout", error_description: `The ${_rsThis.requestType} request was not process in time.` }); 70 | return; 71 | } 72 | const response = await fetch(`${_rsThis.apiPollPresentationRequest}?id=${id}`); 73 | const respMsg = await response.json(); 74 | _rsThis.log(respMsg); 75 | if (respMsg.error_description) { 76 | clearInterval(pollFlag); 77 | _rsThis.onError(_rsThis.requestType, respMsg.error_description); 78 | } else { 79 | if (respMsg.status == 'request_retrieved') { 80 | _rsThis.log(`onRequestRetrieved()`); 81 | _rsThis.onRequestRetrieved(_rsThis.requestType); 82 | } 83 | if (respMsg.status == 'presentation_verified') { 84 | clearInterval(pollFlag); 85 | _rsThis.log(`onPresentationVerified( ${id}, ... )`); 86 | _rsThis.onPresentationVerified(id, respMsg); 87 | } 88 | if (respMsg.status == 'issuance_successful') { 89 | clearInterval(pollFlag); 90 | _rsThis.log(`onIssuanceSuccesful( ${id}, ... )`); 91 | _rsThis.onIssuanceSuccesful(id, respMsg); 92 | } 93 | if (respMsg.status == 'selfie_taken') { 94 | clearInterval(pollFlag); 95 | _rsThis.log(`onSelfieTaken( ${id}, ... )`); 96 | _rsThis.onSelfieTaken(id, respMsg); 97 | } 98 | if (respMsg.status == 'presentation_error' || respMsg.status == 'issuance_error') { 99 | clearInterval(pollFlag); 100 | _rsThis.log(`onError(...)`); 101 | _rsThis.onError(_rsThis.requestType, respMsg); 102 | } 103 | } 104 | }, this.pollFrequency); 105 | }; // pollRequestStatus 106 | 107 | this.setUserPhoto = async function (base64Image) { 108 | this.log('setUserPhoto(): ' + base64Image ); 109 | const response = await fetch(this.apiSetPhoto, { 110 | headers: { 'Accept': 'application/json', 'Content-Type': 'image/jpeg', 'rsid': this.uuid }, 111 | method: 'POST', 112 | body: base64Image 113 | }); 114 | const respJson = await response.json(); 115 | this.log(respJson); 116 | if (respJson.error_description) { 117 | this.onError(this.requestType, respJson.error_description); 118 | return -1; 119 | } else { 120 | return respJson.id; 121 | } 122 | }; 123 | 124 | } // RequestService 125 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/verifiedid.uihandler.js: -------------------------------------------------------------------------------- 1 | // callback methods from RequestService class 2 | function renderQRCode(url) { 3 | document.getElementById('qrcode').style.display = "block"; 4 | document.getElementById("qrcode").getElementsByTagName("img")[0].style.opacity = "1.0"; 5 | qrcode.makeCode(url); 6 | } 7 | function dimQRCode() { 8 | document.getElementById("qrcode").getElementsByTagName("img")[0].style.opacity = "0.1"; 9 | } 10 | function hideQRCode() { 11 | document.getElementById("qrcode").style.display = "none"; 12 | document.getElementById("qrcode").getElementsByTagName("img")[0].style.display = "none"; 13 | var pinCode = document.getElementById('pinCode'); 14 | if ( null != pinCode ) { 15 | pinCode.style.display = "none"; 16 | } 17 | } 18 | function displayMessage(msg) { 19 | document.getElementById("message-wrapper").style.display = "block"; 20 | document.getElementById('message').innerHTML = msg; 21 | } 22 | function drawQRCode(requestType, id, url, pinCode) { 23 | renderQRCode(url); 24 | if (requestType == "presentation") { 25 | document.getElementById('verify-credential').style.display = "none"; 26 | displayMessage("Waiting for QR code to be scanned"); 27 | } else if (requestType == "issuance") { 28 | document.getElementById('issue-credential').style.display = "none"; 29 | document.getElementById('take-selfie').style.display = "none"; 30 | displayMessage("Waiting for QR code to be scanned"); 31 | if ( pinCode != undefined ) { 32 | document.getElementById('pinCode').innerHTML = "Pin code: " + pinCode; 33 | document.getElementById('pinCode').style.display = "block"; 34 | } 35 | } else if (requestType == "selfie") { 36 | displayMessage("Waiting for QR code to be scanned with QR code reader app"); 37 | } 38 | } 39 | function navigateToDeepLink(requestType, id, url) { 40 | document.getElementById('verify-credential').style.display = "none"; 41 | document.getElementById('check-result').style.display = "block"; 42 | window.location.href = url; 43 | } 44 | function requestRetrieved(requestType) { 45 | dimQRCode(); 46 | if (requestType == "presentation") { 47 | displayMessage("QR code scanned. Waiting for Verified ID credential to be shared from wallet..."); 48 | } else { 49 | displayMessage("QR code scanned. Waiting for Verified ID credential to be added to wallet..."); 50 | } 51 | } 52 | function presentationVerified(id, response) { 53 | hideQRCode(); 54 | displayMessage("Presentation verified:

" + JSON.stringify(response.claims)); 55 | window.location = 'presentation-verified.html?id=' + id; 56 | } 57 | function issuanceComplete(id, response) { 58 | hideQRCode(); 59 | displayMessage("Issuance completed"); 60 | } 61 | function selfieTaken(id, response) { 62 | hideQRCode(); 63 | displayMessage("Selfie taken"); 64 | document.getElementById('selfie').src = "data:image/png;base64," + response.photo; 65 | document.getElementById('selfie').style.display = "block"; 66 | } 67 | function requestError(requestType, response) { 68 | hideQRCode(); 69 | console.log(JSON.stringify(response)); 70 | displayMessage(`${requestType} error: ` + JSON.stringify(response)); 71 | } 72 | // method to post selfie taken before starting an issuance request 73 | function setUserPhoto() { 74 | if ("none" != document.getElementById('selfie').style.display && document.getElementById('selfie').src != "") { 75 | photoId = requestService.setUserPhoto(document.getElementById('selfie').src); 76 | } 77 | } 78 | 79 | function hideShowPhotoElements(val) { 80 | document.getElementById("take-selfie").style.display = val; 81 | document.getElementById("imageUpload").style.display = val; 82 | document.getElementById("photo-help").style.display = val; 83 | } 84 | 85 | function uploadImage(e) { 86 | if (e.target.files) { 87 | var reader = new FileReader(); 88 | reader.readAsDataURL(e.target.files[0]); 89 | reader.onloadend = function (e) { 90 | var imageObj = new Image(); 91 | imageObj.src = e.target.result; 92 | imageObj.onload = function (ev) { 93 | var canvas = document.createElement("canvas"); 94 | canvas.width = 480; 95 | canvas.height = 640; 96 | console.log(`img size: ${imageObj.naturalWidth} x ${imageObj.naturalHeight}`); 97 | canvas.getContext('2d').drawImage(imageObj, 0, 0, imageObj.naturalWidth, imageObj.naturalHeight, 0, 0, canvas.width, canvas.height); 98 | document.getElementById("selfie").src = canvas.toDataURL('image/jpeg'); 99 | document.getElementById("selfie").style.display = "block"; 100 | } 101 | } 102 | } 103 | } 104 | 105 | // RequestService object that drives the interaction with backend APIs 106 | // verifiedid.requestservice.client.js 107 | var requestService = new RequestService(drawQRCode, 108 | navigateToDeepLink, 109 | requestRetrieved, 110 | presentationVerified, 111 | issuanceComplete, 112 | selfieTaken, 113 | requestError 114 | ); 115 | // If to do console.log (a lot) 116 | requestService.logEnabled = true; 117 | 118 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/public/verifier.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Verifiable Credentials Request API Sample - Verifier 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |

Verifiable Credential Presentation

20 |

21 |

22 | 23 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 92 | 93 |
94 | 95 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto>=0.24.0 2 | cffi>=1.11.5 3 | idna>=2.7 4 | pycparser>=2.19 5 | six>=1.11.0 6 | requests>=2.21.0 7 | cryptography>=3.2 8 | Flask>=1.0.2 9 | Flask_Caching>=1.10.1 10 | Werkzeug>=0.15.3 11 | gevent>=1.4.0 12 | configparser>=3.7.4 13 | msal>=1.13.0 14 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/setenv.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem for access token 4 | set azTenantId= 5 | set azClientId= 6 | set azClientSecret= 7 | rem set azCertificateName= 8 | rem set azCertificateLocation= 9 | rem set azCertificatePrivateKeyLocation= 10 | 11 | rem your tenant's DID 12 | set DidAuthority=did:web:...your-domain....com 13 | set clientName=Node.js Verified ID sample 14 | set purpose=To prove you are an Verified ID expert 15 | 16 | rem for Issuance 17 | set CredentialManifest=https://verifiedid.did.msidentity.com/v1.0/tenants/...etc... 18 | 19 | rem for Presentation (multiple acceptedIssuers can be separated by ;) 20 | set CredentialType=VerifiedCredentialExpert 21 | set acceptedIssuers=%DidAuthority% 22 | set issuancePinCodeLength=4 23 | 24 | rem for using FaceCheck in presentation requests 25 | set sourcePhotoClaimName= 26 | set matchConfidenceThreshold=70 27 | 28 | echo Environment Variables loaded for tenant %azTenantId% and type %CredentialType% 29 | echo run app via command "python app.py" 30 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/setenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # for access token 4 | export azTenantId= 5 | export azClientId= 6 | export azClientSecret= 7 | # export azCertificateName= 8 | # export azCertificateLocation= 9 | # export azCertificatePrivateKeyLocation= 10 | 11 | # your tenant's DID 12 | export DidAuthority=did:web:...your-domain....com 13 | export clientName=Node.js Verified ID sample 14 | export purpose=To prove you are an Verified ID expert 15 | 16 | # for Issuance 17 | export CredentialManifest=https://verifiedid.did.msidentity.com/v1.0/tenants/...etc... 18 | 19 | # for Presentation (multiple acceptedIssuers can be separated by ;) 20 | export CredentialType=VerifiedCredentialExpert 21 | export acceptedIssuers=$DidAuthority 22 | export issuancePinCodeLength=4 23 | 24 | # for using FaceCheck in presentation requests 25 | export sourcePhotoClaimName= 26 | export matchConfidenceThreshold=70 27 | 28 | echo Environment Variables loaded for tenant $azTenantId and type $CredentialType 29 | echo run app via command "python app.py" 30 | -------------------------------------------------------------------------------- /1-python-api-idtokenhint/verifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from flask import Flask 4 | from flask import request,Response,redirect 5 | from flask_caching import Cache 6 | from flask.json import jsonify 7 | import json 8 | import logging 9 | import sys, os, tempfile, uuid, time, datetime 10 | import configparser 11 | import argparse 12 | import requests 13 | import jwt, base64 14 | import msal 15 | 16 | from __main__ import app 17 | from __main__ import cache 18 | from __main__ import log 19 | from __main__ import config 20 | from __main__ import msalCca 21 | from __main__ import base64JwtTokenToJson 22 | 23 | presentationConfig = { 24 | "authority": "...set in code...", 25 | "includeQRCode": False, 26 | "callback": { 27 | "url": "...set in code...", 28 | "state": "...set in code...", 29 | "headers": { 30 | "api-key": "...set in code..." 31 | } 32 | }, 33 | "registration": { 34 | "clientName": config["clientName"], 35 | "purpose": config["purpose"] 36 | }, 37 | "includeReceipt": True, 38 | "requestedCredentials": [ 39 | { 40 | "type": config["CredentialType"], 41 | "acceptedIssuers": [ config["acceptedIssuers"] ], 42 | "configuration": { 43 | "validation": { 44 | "allowRevoked": True, 45 | "validateLinkedDomain": True 46 | } 47 | } 48 | } 49 | ] 50 | } 51 | 52 | idx = sys.argv.index('-p') if '-p' in sys.argv else -1 53 | if idx >= 0: 54 | print("Loading presentationRequest from file: " + sys.argv[idx+1]) 55 | presentationConfig = json.load(open(sys.argv[idx+1])) 56 | 57 | presentationConfig["authority"] = config["DidAuthority"] 58 | presentationConfig["callback"]["headers"]["api-key"] = config["apiKey"] 59 | 60 | @app.route("/api/verifier/presentation-request", methods = ['GET']) 61 | def presentationRequest(): 62 | """ This method is called from the UI to initiate the presentation of the verifiable credential """ 63 | id = str(uuid.uuid4()) 64 | cache.set( id, json.dumps({ 65 | "status" : "request_created", 66 | "message": "Waiting for QR code to be scanned" 67 | })) 68 | accessToken = "" 69 | result = msalCca.acquire_token_for_client( scopes=["3db474b9-6a0c-4840-96ac-1fceb342124f/.default"] ) 70 | if "access_token" in result: 71 | print( result['access_token'] ) 72 | accessToken = result['access_token'] 73 | else: 74 | print(result.get("error") + result.get("error_description")) 75 | payload = presentationConfig.copy() 76 | payload["callback"]["url"] = str(request.url_root).replace("http://", "https://") + "api/request-callback" 77 | payload["callback"]["state"] = id 78 | print( json.dumps(payload) ) 79 | post_headers = { "content-type": "application/json", "Authorization": "Bearer " + accessToken } 80 | client_api_request_endpoint = config["msIdentityHostName"] + "verifiableCredentials/createPresentationRequest" 81 | print( client_api_request_endpoint ) 82 | r = requests.post( client_api_request_endpoint 83 | , headers=post_headers, data=json.dumps(payload)) 84 | resp = r.json() 85 | print(resp) 86 | resp["id"] = id 87 | response = Response( json.dumps(resp), status=200, mimetype='application/json') 88 | response.headers.add('Access-Control-Allow-Origin', '*') 89 | return response 90 | 91 | @app.route("/api/verifier/get-presentation-details", methods = ['GET']) 92 | def getPresentationDetails(): 93 | respData = { 94 | 'clientName': presentationConfig["registration"]["clientName"], 95 | 'purpose': presentationConfig["registration"]["purpose"], 96 | 'DidAuthority': config["DidAuthority"], 97 | 'type': presentationConfig["requestedCredentials"][0]["type"], 98 | 'acceptedIssuers': presentationConfig["requestedCredentials"][0]["acceptedIssuers"] 99 | } 100 | return Response( json.dumps(respData), status=200, mimetype='application/json') 101 | 102 | @app.route("/api/verifier/load-template", methods = ['POST']) 103 | def loadTemplate(): 104 | """ 105 | UI passes a template for presentation request so we can request other credentials 106 | """ 107 | body = request.data.decode() 108 | buf = "" 109 | for line in body.splitlines(): 110 | if line.lstrip().startswith("//") == False: 111 | buf = buf + line 112 | template = json.loads(buf) 113 | print(template) 114 | global presentationConfig 115 | presentationConfig = template 116 | presentationConfig["authority"] = config["DidAuthority"] 117 | presentationConfig["callback"]["headers"]["api-key"] = config["apiKey"] 118 | response = Response( json.dumps({'status': 'template loaded'}), status=200, mimetype='application/json') 119 | return response 120 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 |
4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Entra Verified ID Samples 2 | 3 | This repo contains a set of Microsoft Entra Verified ID samples (former Azure AD Verifiable Credentials) 4 | 5 | ## Samples 6 | | Sample | Description | 7 | |------|--------| 8 | | 1-python-api-idtokenhint | Python sample for using the VC Request Service API to issue and verify verifiable credentials with a credential contract which allows the VC Request API to pass in a payload for the Verifiable Credentials| 9 | 10 | 11 | 12 | Microsoft provides a simple to use REST API to issue and verify verifiable credentials. You can use the programming language you prefer to the REST API. Instead of needing to understand the different protocols and encryption algoritms for Verifiable Credentials and DIDs you only need to understand how to format a JSON structure as parameter for the VC Request API. 13 | 14 | ![API Overview](ReadmeFiles/SampleArchitectureOverview.svg) 15 | 16 | ## Issuance 17 | 18 | ### Issuance JSON structure 19 | 20 | To call the VC Client API to start the issuance process, the VC Request API needs a JSON structure payload like below. 21 | 22 | ```JSON 23 | { 24 | "authority": "did:ion: ...of the Issuer", 25 | "includeQRCode": true, 26 | "registration": { 27 | "clientName": "the verifier's client name" 28 | }, 29 | "callback": { 30 | "url": "https://contoso.com/api/issuer/issuanceCallback", 31 | "state": "you pass your state here to correlate it when you get the callback", 32 | "headers": { 33 | "api-key": "API key to help protect your callback API" 34 | } 35 | }, 36 | "type": "your credentialType", 37 | "manifest": "https://verifiedid.did.msidentity.com/v1.0/3c32ed40-8a10-465b-8ba4-0b1e86882668/verifiableCredential/contracts/VerifiedCredentialExpert", 38 | "pin": { 39 | "value": "012345", 40 | "length": 6 41 | }, 42 | "claims": { 43 | "firstName": "Megan", 44 | "lastName": "Bowen" 45 | } 46 | } 47 | ``` 48 | 49 | - **authority** - is the DID identifier for your registered Verifiable Credential from portal.azure.com. 50 | - **includeQRCode** - If you want the VC Client API to return a `data:image/png;base64` string of the QR code to present in the browser. If you select `false`, you must create the QR code yourself (which is not difficult). 51 | - **registration.clientName** - name of your app which will be shown in the Microsoft Authenticator 52 | - **callback.url** - a callback endpoint in your application. The VC Request API will call this endpoint when the issuance is completed. 53 | - **callback.state** - A state value you provide so you can correlate this request when you get callback confirmation 54 | - **callback.headers** - Any HTTP Header values that you would like the VC Request API to pass back in the callbacks. Here you could set your own API key, for instance 55 | - **type** - the name of your credentialType. This value is configured in the rules file. 56 | - **manifest** - url of your manifest for your VC. This comes from your defined Verifiable Credential in portal.azure.com 57 | - **pin** - If you want to require a pin code in the Microsoft Authenticator for this issuance request. This can be useful if it is a self issuing situation where there is no possibility of asking the user to prove their identity via a login. If you don't want to use the pin functionality, you should not have the pin section in the JSON structure. 58 | - **claims** - optional, extra claims you want to include in the VC. 59 | 60 | In the response message from the VC Request API, it will include a URL to the request which is hosted at the Microsoft VC request service, which means that once the Microsoft Authenticator has scanned the QR code, it will contact the VC Request service directly and not your application directly. Your application will get a callback from the VC Request service via the callback. 61 | 62 | ```json 63 | { 64 | "requestId": "799f23ea-524a-45af-99ad-cf8e5018814e", 65 | "url": "openid://vc?request_uri=https://verifiedid.did.msidentity.com/v1.0/abc/verifiablecredentials/request/178319f7-20be-4945-80fb-7d52d47ae82e", 66 | "expiry": 1622227690, 67 | "qrCode": "data:image/png;base64,iVBORw0KGgoA" 68 | } 69 | ``` 70 | 71 | ### Issuance Callback 72 | 73 | In your callback endpoint, you will get a callback with the below message when the QR code is scanned. This callback is typically used to modify the UI, hide the QR code to prevent scanning again and show the pincode to use when the user wants to accept the Verifiable Credential. 74 | 75 | ```JSON 76 | { 77 | "requestStatus":"request_retrieved", 78 | "requestId":"9463da82-e397-45b6-a7a2-2c4223b9fdd0", 79 | "state": "...what you passed as the state value..." 80 | } 81 | ``` 82 | 83 | Once the VC is issued, you get a second callback which contains information if the issuance of the verifiable credential to the user was succesful or not. 84 | 85 | This callback is typically used to notify the user on the issuance website the process is completed and continue with whatever the website needs or wants the user to do. 86 | 87 | ### Successful Issuance flow response 88 | ```JSON 89 | { 90 | "requestStatus":"issuance_successful", 91 | "requestId":"9463da82-e397-45b6-a7a2-2c4223b9fdd0", 92 | "state": "...what you passed as the state value..." 93 | } 94 | ``` 95 | ### Unuccesful Issuance flow response 96 | ```JSON 97 | { 98 | "requestStatus":"issuance_failed", 99 | "requestId":"9463da82-e397-45b6-a7a2-2c4223b9fdd0", 100 | "state": "...what you passed as the state value...", 101 | "error": { 102 | "code":"IssuanceFlowFailed", 103 | "message":"issuance_service_error", 104 | } 105 | } 106 | ``` 107 | When the issuance fails this can be caused by several reasons. The following details are currently provided in the error part of the response: 108 | | Message | Definition | 109 | |---|---| 110 | | fetch_contract_error | The user has canceled the flow | 111 | | issuance_service_error | VC Issuance service was not able to validate requirements / something went wrong on Microsoft AAD VC Issuance service side. | 112 | | unspecified_error | Something went wrong that doesn’t fall into this bucket | 113 | 114 | 115 | ## Verification 116 | 117 | ### Verification JSON structure 118 | 119 | To call the VC Request API to start the verification process, the application creates a JSON structure like below. Since the WebApp asks the user to present a VC, the request is also called `presentation request`. 120 | 121 | ```JSON 122 | { 123 | "authority": "did:ion: did-of-the-Verifier", 124 | "includeQRCode": true, 125 | "registration": { 126 | "clientName": "the verifier's client name", 127 | "purpose": "the purpose why the verifier asks for a VC" 128 | }, 129 | "callback": { 130 | "url": "https://contoso.com/api/verifier/presentationCallback", 131 | "state": "you pass your state here to correlate it when you get the callback", 132 | "headers": { 133 | "api-key": "API key to help protect your callback API" 134 | } 135 | }, 136 | "includeReceipt": false, 137 | "requestedCredentials": [ 138 | { 139 | "type": "your credentialType", 140 | "purpose": "the purpose why the verifier asks for a VC", 141 | "acceptedIssuers": [ "did:ion: ...of the Issuer" ], 142 | "configuration": { 143 | "validation": { 144 | "allowRevoked": true, 145 | "validateLinkedDomain": true 146 | } 147 | } 148 | } 149 | ] 150 | } 151 | ``` 152 | 153 | Much of the data is the same in this JSON structure, but some differences needs explaining. 154 | 155 | - **authority** vs **acceptedIssuers** - The Verifier and the Issuer may be two different entities. For example, the Verifier might be a online service, like a car rental service, while the DID it is asking for is the issuing entity for drivers licenses. Note that `acceptedIssuers` is a collection of DIDs, which means you can ask for multiple VCs from the user coming from different trusted issuers. 156 | - **requestedCredentials** - please also note that the `requestedCredentials` is a collection too, which means you can ask to create a presentation request that contains multiple DIDs. 157 | - **includeReceipt** - if set to true, the `presentation_verified` callback will contain the `receipt` element. 158 | 159 | ### Verification Callback 160 | 161 | In your callback endpoint, you will get a callback with the below message when the QR code is scanned. 162 | 163 | When the QR code is scanned, you get a short callback like this. 164 | ```JSON 165 | { 166 | "requestStatus":"request_retrieved", 167 | "requestId":"c18d8035-3fc8-4c27-a5db-9801e6232569", 168 | "state": "...what you passed as the state value..." 169 | } 170 | ``` 171 | 172 | Once the VC is verified, you get a second, more complete, callback which contains all the details on what whas presented by the user. 173 | 174 | ```JSON 175 | { 176 | "requestStatus":"presentation_verified", 177 | "requestId":"c18d8035-3fc8-4c27-a5db-9801e6232569", 178 | "state": "...what you passed as the state value...", 179 | "subject": "did:ion: ... of the VC holder...", 180 | "issuers": [ 181 | { 182 | "issuer": "did:ion of the issuer of this verifiable credential ", 183 | "type": [ "VerifiableCredential", "your credentialType" ], 184 | "claims": { 185 | "lastName":"Bowen", 186 | "firstName":"Megan" 187 | }, 188 | "credentialState": { 189 | "revocationStatus": "VALID" 190 | }, 191 | "domainValidation": { 192 | "url": "https://did.woodgrovedemo.com" 193 | } 194 | } 195 | ], 196 | "receipt":{ 197 | "id_token": "...JWT Token of VC...", 198 | "vp_token": "...JWT Token of VP..." 199 | } 200 | } 201 | } 202 | ``` 203 | Some notable attributes in the message: 204 | - **claims** - parsed claims from the VC 205 | - **credentialState.revocationStatus** - indicates the current revocation status at the time of the presentaion 206 | - **domainValidation** - If you asked for domain validation via passing `validateLinkedDomain` **true** in the request, you will get the validated domain name in the response. 207 | - **receipt.id_token** - The JWT token of the presentation response 208 | - **receipt.vp_token** - The JWT token of the credential in the presentation response. In the token, the `vp.verifiableCredential` contains the VCs for the presented credentials. 209 | 210 | 211 | ## Setup 212 | 213 | Before you can run any of these samples make sure your environment is setup correctly. You can follow the setup instructions [here](https://aka.ms/vcsetup) 214 | 215 | ## Resources 216 | 217 | For more information, see MSAL.NET's conceptual documentation: 218 | 219 | - [Quickstart: Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) 220 | - [Quickstart: Configure a client application to access web APIs](https://docs.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis) 221 | - [Acquiring a token for an application with client credential flows](https://aka.ms/msal-net-client-credentials) 222 | -------------------------------------------------------------------------------- /ReadmeFiles/SampleArchitectureOverview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 77 | 78 | Page-1 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | Boundary.50 87 | 88 | Sheet.51 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Boundary 100 | 101 | Sheet.48 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | Azure Active Directory 110 | Azure Active Directory 111 | 112 | Sheet.2 113 | 114 | 115 | 116 | 117 | 118 | 119 | Sheet.3 120 | 121 | 122 | 123 | 124 | 125 | 126 | Sheet.4 127 | 128 | 129 | 130 | 131 | 132 | 133 | Sheet.5 134 | 135 | 136 | 137 | 138 | 139 | 140 | Sheet.6 141 | 142 | 143 | 144 | 145 | 146 | 147 | Sheet.7 148 | 149 | 150 | 151 | 152 | 153 | 154 | Sheet.8 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | Azure Active Directory 165 | 166 | 167 | Microsoft Azure 168 | Microsoft Azure 169 | 170 | Sheet.10 171 | 179 | 180 | 181 | Sheet.11 182 | 188 | 189 | 190 | 191 | 192 | Microsoft Azure 194 | 195 | 196 | Web App (opaque) (was websites) 197 | Web App 198 | 199 | Sheet.13 200 | 203 | 204 | 205 | Sheet.14 206 | 207 | Sheet.15 208 | 209 | Sheet.16 210 | 216 | 217 | 218 | 219 | Sheet.17 220 | 221 | Sheet.18 222 | 223 | Sheet.19 224 | 225 | Sheet.20 226 | 229 | 230 | 231 | 232 | Sheet.21 233 | 236 | 237 | 238 | Sheet.22 239 | 240 | Sheet.23 241 | 243 | 244 | 245 | 246 | Sheet.24 247 | 248 | Sheet.25 249 | 251 | 252 | 253 | 254 | Sheet.26 255 | 256 | Sheet.27 257 | 260 | 261 | 262 | 263 | Sheet.28 264 | 265 | Sheet.29 266 | 268 | 269 | 270 | 271 | Sheet.30 272 | 273 | Sheet.31 274 | 276 | 277 | 278 | 279 | Sheet.32 280 | 281 | Sheet.33 282 | 284 | 285 | 286 | 287 | Sheet.34 288 | 290 | 291 | 292 | Sheet.35 293 | 295 | 296 | 297 | 298 | Sheet.36 299 | 300 | Sheet.37 301 | 303 | 304 | 305 | 306 | Sheet.38 307 | 308 | Sheet.39 309 | 311 | 312 | 313 | 314 | Sheet.40 315 | 316 | Sheet.41 317 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | Web App 327 | 328 | 329 | Dynamic connector 330 | ① Acquire token client credential (app password) 331 | 332 | 333 | 334 | 335 | ① Acquire tokenclient credential(app password) 338 | 339 | API App 340 | VC Request API 341 | 342 | 343 | 351 | VC Request API 352 | 353 | Dynamic connector.45 354 | ② Create issuance or verification request 355 | 356 | 357 | 358 | 359 | ② Create issuance or verification request 360 | 361 | Server Farm 362 | local 363 | 364 | Sheet.53 365 | 366 | 367 | 368 | 371 | 372 | 373 | Sheet.54 374 | 375 | 376 | 377 | 381 | 382 | 383 | Sheet.55 384 | 385 | 386 | 387 | 388 | 389 | 390 | Sheet.56 391 | 392 | 393 | 394 | 395 | 396 | 397 | Sheet.57 398 | 399 | 400 | 401 | 404 | 405 | 406 | Sheet.58 407 | 408 | 409 | 410 | 414 | 415 | 416 | Sheet.59 417 | 418 | 419 | 420 | 423 | 424 | 425 | Sheet.60 426 | 427 | 428 | 429 | 433 | 434 | 435 | 436 | 437 | local 438 | 439 | 440 | 441 | --------------------------------------------------------------------------------