├── .github └── workflows │ └── main_woodgrove-groceries-auth-api.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── Controllers ├── OnAttributeCollectionStartController.cs ├── OnAttributeCollectionSubmitController.cs ├── OnOtpSendController.cs ├── OnTokenIssuanceStartController.cs ├── OtherServices │ └── ActAsDemoController.cs ├── Temporary │ ├── EchoController.cs │ └── SignUpStartsTestController.cs └── onPageRenderStartController.cs ├── Helpers └── AppInsightsHelper.cs ├── LICENSE ├── Models ├── ActAsEntity.cs ├── ActAsRequest.cs ├── AllRequest.cs ├── AttributeCollectionRequest.cs ├── AttributeCollectionStartResponse.cs ├── AttributeCollectionSubmitResponse.cs ├── AuthenticationContext.cs ├── AzureAppServiceClaims.cs ├── OnOtpSendRequest.cs ├── OnOtpSendResponse.cs ├── PageRenderStartRequest.cs ├── PageRenderStartResponse.cs ├── TokenIssuanceStartRequest.cs ├── TokenIssuanceStartResponse.cs └── UserSignUpInfo.cs ├── Program.cs ├── Properties └── launchSettings.json ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── appsettings.Development.json ├── appsettings.json ├── woodgroveapi.csproj ├── woodgroveapi.sln └── wwwroot ├── Help ├── TokenIssuanceStart_LocalAccount.json ├── TokenIssuanceStart_SocailAccount.json ├── attributeCollectionStart-LocalAccount.json ├── attributeCollectionStart-SocialAccount.json ├── attributeCollectionSubmit-LocalAccount.json ├── attributeCollectionSubmit-SocialAccount.json └── onOtpSend.json └── help.html /.github/workflows/main_woodgrove-groceries-auth-api.yml: -------------------------------------------------------------------------------- 1 | # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy 2 | # More GitHub Actions for Azure: https://github.com/Azure/actions 3 | 4 | name: Build and deploy ASP.Net Core app to Azure Web App - woodgrove-groceries-auth-api 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: windows-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up .NET Core 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: '9.x' 23 | 24 | - name: Build with dotnet 25 | run: dotnet build --configuration Release 26 | 27 | - name: dotnet publish 28 | run: dotnet publish -c Release -o "${{env.DOTNET_ROOT}}/myapp" 29 | 30 | - name: Upload artifact for deployment job 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: .net-app 34 | path: ${{env.DOTNET_ROOT}}/myapp 35 | 36 | deploy: 37 | runs-on: windows-latest 38 | needs: build 39 | environment: 40 | name: 'Production' 41 | url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} 42 | permissions: 43 | id-token: write #This is required for requesting the JWT 44 | 45 | steps: 46 | - name: Download artifact from build job 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: .net-app 50 | 51 | - name: Login to Azure 52 | uses: azure/login@v2 53 | with: 54 | client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_D4DD8896E90F4963BDA1B155B7B58786 }} 55 | tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_E90F9A6A42044446B33CFCAA01593F42 }} 56 | subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_36E53C9A567B4ED6A9304D8FC60B4FA2 }} 57 | 58 | - name: Deploy to Azure Web App 59 | id: deploy-to-webapp 60 | uses: azure/webapps-deploy@v3 61 | with: 62 | app-name: 'woodgrove-groceries-auth-api' 63 | slot-name: 'Production' 64 | package: . 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | appsettings.Development.json 16 | 17 | # Mono auto generated files 18 | mono_crash.* 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | [Ww][Ii][Nn]32/ 28 | [Aa][Rr][Mm]/ 29 | [Aa][Rr][Mm]64/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # Tye 67 | .tye/ 68 | 69 | # ASP.NET Scaffolding 70 | ScaffoldingReadMe.txt 71 | 72 | # StyleCop 73 | StyleCopReport.xml 74 | 75 | # Files built by Visual Studio 76 | *_i.c 77 | *_p.c 78 | *_h.h 79 | *.ilk 80 | *.meta 81 | *.obj 82 | *.iobj 83 | *.pch 84 | *.pdb 85 | *.ipdb 86 | *.pgc 87 | *.pgd 88 | *.rsp 89 | *.sbr 90 | *.tlb 91 | *.tli 92 | *.tlh 93 | *.tmp 94 | *.tmp_proj 95 | *_wpftmp.csproj 96 | *.log 97 | *.tlog 98 | *.vspscc 99 | *.vssscc 100 | .builds 101 | *.pidb 102 | *.svclog 103 | *.scc 104 | 105 | # Chutzpah Test files 106 | _Chutzpah* 107 | 108 | # Visual C++ cache files 109 | ipch/ 110 | *.aps 111 | *.ncb 112 | *.opendb 113 | *.opensdf 114 | *.sdf 115 | *.cachefile 116 | *.VC.db 117 | *.VC.VC.opendb 118 | 119 | # Visual Studio profiler 120 | *.psess 121 | *.vsp 122 | *.vspx 123 | *.sap 124 | 125 | # Visual Studio Trace Files 126 | *.e2e 127 | 128 | # TFS 2012 Local Workspace 129 | $tf/ 130 | 131 | # Guidance Automation Toolkit 132 | *.gpState 133 | 134 | # ReSharper is a .NET coding add-in 135 | _ReSharper*/ 136 | *.[Rr]e[Ss]harper 137 | *.DotSettings.user 138 | 139 | # TeamCity is a build add-in 140 | _TeamCity* 141 | 142 | # DotCover is a Code Coverage Tool 143 | *.dotCover 144 | 145 | # AxoCover is a Code Coverage Tool 146 | .axoCover/* 147 | !.axoCover/settings.json 148 | 149 | # Coverlet is a free, cross platform Code Coverage Tool 150 | coverage*.json 151 | coverage*.xml 152 | coverage*.info 153 | 154 | # Visual Studio code coverage results 155 | *.coverage 156 | *.coveragexml 157 | 158 | # NCrunch 159 | _NCrunch_* 160 | .*crunch*.local.xml 161 | nCrunchTemp_* 162 | 163 | # MightyMoose 164 | *.mm.* 165 | AutoTest.Net/ 166 | 167 | # Web workbench (sass) 168 | .sass-cache/ 169 | 170 | # Installshield output folder 171 | [Ee]xpress/ 172 | 173 | # DocProject is a documentation generator add-in 174 | DocProject/buildhelp/ 175 | DocProject/Help/*.HxT 176 | DocProject/Help/*.HxC 177 | DocProject/Help/*.hhc 178 | DocProject/Help/*.hhk 179 | DocProject/Help/*.hhp 180 | DocProject/Help/Html2 181 | DocProject/Help/html 182 | 183 | # Click-Once directory 184 | publish/ 185 | 186 | # Publish Web Output 187 | *.[Pp]ublish.xml 188 | *.azurePubxml 189 | # Note: Comment the next line if you want to checkin your web deploy settings, 190 | # but database connection strings (with potential passwords) will be unencrypted 191 | *.pubxml 192 | *.publishproj 193 | 194 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 195 | # checkin your Azure Web App publish settings, but sensitive information contained 196 | # in these scripts will be unencrypted 197 | PublishScripts/ 198 | 199 | # NuGet Packages 200 | *.nupkg 201 | # NuGet Symbol Packages 202 | *.snupkg 203 | # The packages folder can be ignored because of Package Restore 204 | **/[Pp]ackages/* 205 | # except build/, which is used as an MSBuild target. 206 | !**/[Pp]ackages/build/ 207 | # Uncomment if necessary however generally it will be regenerated when needed 208 | #!**/[Pp]ackages/repositories.config 209 | # NuGet v3's project.json files produces more ignorable files 210 | *.nuget.props 211 | *.nuget.targets 212 | 213 | # Microsoft Azure Build Output 214 | csx/ 215 | *.build.csdef 216 | 217 | # Microsoft Azure Emulator 218 | ecf/ 219 | rcf/ 220 | 221 | # Windows Store app package directories and files 222 | AppPackages/ 223 | BundleArtifacts/ 224 | Package.StoreAssociation.xml 225 | _pkginfo.txt 226 | *.appx 227 | *.appxbundle 228 | *.appxupload 229 | 230 | # Visual Studio cache files 231 | # files ending in .cache can be ignored 232 | *.[Cc]ache 233 | # but keep track of directories ending in .cache 234 | !?*.[Cc]ache/ 235 | 236 | # Others 237 | ClientBin/ 238 | ~$* 239 | *~ 240 | *.dbmdl 241 | *.dbproj.schemaview 242 | *.jfm 243 | *.pfx 244 | *.publishsettings 245 | orleans.codegen.cs 246 | 247 | # Including strong name files can present a security risk 248 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 249 | #*.snk 250 | 251 | # Since there are multiple workflows, uncomment next line to ignore bower_components 252 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 253 | #bower_components/ 254 | 255 | # RIA/Silverlight projects 256 | Generated_Code/ 257 | 258 | # Backup & report files from converting an old project file 259 | # to a newer Visual Studio version. Backup files are not needed, 260 | # because we have git ;-) 261 | _UpgradeReport_Files/ 262 | Backup*/ 263 | UpgradeLog*.XML 264 | UpgradeLog*.htm 265 | ServiceFabricBackup/ 266 | *.rptproj.bak 267 | 268 | # SQL Server files 269 | *.mdf 270 | *.ldf 271 | *.ndf 272 | 273 | # Business Intelligence projects 274 | *.rdl.data 275 | *.bim.layout 276 | *.bim_*.settings 277 | *.rptproj.rsuser 278 | *- [Bb]ackup.rdl 279 | *- [Bb]ackup ([0-9]).rdl 280 | *- [Bb]ackup ([0-9][0-9]).rdl 281 | 282 | # Microsoft Fakes 283 | FakesAssemblies/ 284 | 285 | # GhostDoc plugin setting file 286 | *.GhostDoc.xml 287 | 288 | # Node.js Tools for Visual Studio 289 | .ntvs_analysis.dat 290 | node_modules/ 291 | 292 | # Visual Studio 6 build log 293 | *.plg 294 | 295 | # Visual Studio 6 workspace options file 296 | *.opt 297 | 298 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 299 | *.vbw 300 | 301 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 302 | *.vbp 303 | 304 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 305 | *.dsw 306 | *.dsp 307 | 308 | # Visual Studio 6 technical files 309 | *.ncb 310 | *.aps 311 | 312 | # Visual Studio LightSwitch build output 313 | **/*.HTMLClient/GeneratedArtifacts 314 | **/*.DesktopClient/GeneratedArtifacts 315 | **/*.DesktopClient/ModelManifest.xml 316 | **/*.Server/GeneratedArtifacts 317 | **/*.Server/ModelManifest.xml 318 | _Pvt_Extensions 319 | 320 | # Paket dependency manager 321 | .paket/paket.exe 322 | paket-files/ 323 | 324 | # FAKE - F# Make 325 | .fake/ 326 | 327 | # CodeRush personal settings 328 | .cr/personal 329 | 330 | # Python Tools for Visual Studio (PTVS) 331 | __pycache__/ 332 | *.pyc 333 | 334 | # Cake - Uncomment if you are using it 335 | # tools/** 336 | # !tools/packages.config 337 | 338 | # Tabs Studio 339 | *.tss 340 | 341 | # Telerik's JustMock configuration file 342 | *.jmconfig 343 | 344 | # BizTalk build output 345 | *.btp.cs 346 | *.btm.cs 347 | *.odx.cs 348 | *.xsd.cs 349 | 350 | # OpenCover UI analysis results 351 | OpenCover/ 352 | 353 | # Azure Stream Analytics local run output 354 | ASALocalRun/ 355 | 356 | # MSBuild Binary and Structured Log 357 | *.binlog 358 | 359 | # NVidia Nsight GPU debugger configuration file 360 | *.nvuser 361 | 362 | # MFractors (Xamarin productivity tool) working folder 363 | .mfractor/ 364 | 365 | # Local History for Visual Studio 366 | .localhistory/ 367 | 368 | # Visual Studio History (VSHistory) files 369 | .vshistory/ 370 | 371 | # BeatPulse healthcheck temp database 372 | healthchecksdb 373 | 374 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 375 | MigrationBackup/ 376 | 377 | # Ionide (cross platform F# VS Code tools) working folder 378 | .ionide/ 379 | 380 | # Fody - auto-generated XML schema 381 | FodyWeavers.xsd 382 | 383 | # VS Code files for those working on multiple tools 384 | .vscode/* 385 | !.vscode/settings.json 386 | !.vscode/tasks.json 387 | !.vscode/launch.json 388 | !.vscode/extensions.json 389 | *.code-workspace 390 | 391 | # Local History for Visual Studio Code 392 | .history/ 393 | 394 | # Windows Installer files from build outputs 395 | *.cab 396 | *.msi 397 | *.msix 398 | *.msm 399 | *.msp 400 | 401 | # JetBrains Rider 402 | *.sln.iml 403 | 404 | ## 405 | ## Visual studio for Mac 406 | ## 407 | 408 | 409 | # globs 410 | Makefile.in 411 | *.userprefs 412 | *.usertasks 413 | config.make 414 | config.status 415 | aclocal.m4 416 | install-sh 417 | autom4te.cache/ 418 | *.tar.gz 419 | tarballs/ 420 | test-results/ 421 | 422 | # Mac bundle stuff 423 | *.dmg 424 | *.app 425 | 426 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 427 | # General 428 | .DS_Store 429 | .AppleDouble 430 | .LSOverride 431 | 432 | # Icon must end with two \r 433 | Icon 434 | 435 | 436 | # Thumbnails 437 | ._* 438 | 439 | # Files that might appear in the root of a volume 440 | .DocumentRevisions-V100 441 | .fseventsd 442 | .Spotlight-V100 443 | .TemporaryItems 444 | .Trashes 445 | .VolumeIcon.icns 446 | .com.apple.timemachine.donotpresent 447 | 448 | # Directories potentially created on remote AFP share 449 | .AppleDB 450 | .AppleDesktop 451 | Network Trash Folder 452 | Temporary Items 453 | .apdisk 454 | 455 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 456 | # Windows thumbnail cache files 457 | Thumbs.db 458 | ehthumbs.db 459 | ehthumbs_vista.db 460 | 461 | # Dump file 462 | *.stackdump 463 | 464 | # Folder config file 465 | [Dd]esktop.ini 466 | 467 | # Recycle Bin used on file shares 468 | $RECYCLE.BIN/ 469 | 470 | # Windows Installer files 471 | *.cab 472 | *.msi 473 | *.msix 474 | *.msm 475 | *.msp 476 | 477 | # Windows shortcuts 478 | *.lnk 479 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md. 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/net9.0/woodgroveapi.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/woodgroveapi.sln", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/woodgroveapi.sln", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/woodgroveapi.sln" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Controllers/OnAttributeCollectionStartController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights; 2 | using Microsoft.ApplicationInsights.DataContracts; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using woodgroveapi.Helpers; 6 | using woodgroveapi.Models; 7 | 8 | namespace woodgroveapi.Controllers; 9 | 10 | //[Authorize] 11 | [ApiController] 12 | [Route("[controller]")] 13 | public class OnAttributeCollectionStartController : ControllerBase 14 | { 15 | private readonly ILogger _logger; 16 | private TelemetryClient _telemetry; 17 | 18 | public OnAttributeCollectionStartController(ILogger logger, TelemetryClient telemetry) 19 | { 20 | _logger = logger; 21 | _telemetry = telemetry; 22 | } 23 | 24 | [HttpPost(Name = "OnAttributeCollectionStart")] 25 | public AttributeCollectionStartResponse PostAsync([FromBody] AttributeCollectionRequest requestPayload) 26 | { 27 | // For Azure App Service with Easy Auth, validate the azp claim value 28 | // if (!AzureAppServiceClaimsHeader.Authorize(this.Request)) 29 | // { 30 | // Response.StatusCode = (int)HttpStatusCode.Unauthorized; 31 | // return null; 32 | // } 33 | 34 | // Track the page view 35 | AppInsightsHelper.TrackApi("OnAttributeCollectionStart", this._telemetry, requestPayload.data); 36 | 37 | // Message object to return to Microsoft Entra ID 38 | AttributeCollectionStartResponse r = new AttributeCollectionStartResponse(); 39 | r.data.actions[0].odatatype = AttributeCollectionStartResponse_ActionTypes.SetPrefillValues; 40 | r.data.actions[0].inputs = new AttributeCollectionStartResponse_Inputs(); 41 | 42 | // Return the country and city 43 | r.data.actions[0].inputs.country = "es"; 44 | // r.data.actions[0].inputs.city = "Madrind"; 45 | 46 | // Return a promo code 47 | Random random = new Random(); 48 | r.data.actions[0].inputs.promoCode = $"#{random.Next(1236, 9873)}"; 49 | 50 | return r; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Controllers/OnAttributeCollectionSubmitController.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Microsoft.ApplicationInsights; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using woodgroveapi.Helpers; 6 | using woodgroveapi.Models; 7 | 8 | namespace woodgroveapi.Controllers; 9 | 10 | //[Authorize] 11 | [ApiController] 12 | [Route("[controller]")] 13 | public class OnAttributeCollectionSubmitController : ControllerBase 14 | { 15 | private readonly ILogger _logger; 16 | private TelemetryClient _telemetry; 17 | 18 | public OnAttributeCollectionSubmitController(ILogger logger, TelemetryClient telemetry) 19 | { 20 | _logger = logger; 21 | _telemetry = telemetry; 22 | } 23 | 24 | [HttpPost(Name = "OnAttributeCollectionSubmit")] 25 | public AttributeCollectionSubmitResponse PostAsync([FromBody] AttributeCollectionRequest requestPayload) 26 | { 27 | // For Azure App Service with Easy Auth, validate the azp claim value 28 | // if (!AzureAppServiceClaimsHeader.Authorize(this.Request)) 29 | // { 30 | // Response.StatusCode = (int)HttpStatusCode.Unauthorized; 31 | // return null; 32 | // } 33 | 34 | // Track the page view 35 | AppInsightsHelper.TrackApi("OnAttributeCollectionSubmit", this._telemetry, requestPayload.data); 36 | 37 | // List of countries and cities where Woodgrove operates 38 | Dictionary CountriesList = new Dictionary(); 39 | CountriesList.Add("au", " Sydney, Brisbane, Melbourne"); 40 | CountriesList.Add("es", " Madrid, Barcelona, Seville"); 41 | CountriesList.Add("us", " New York, Chicago, Boston, Seattle"); 42 | 43 | // Message object to return to Microsoft Entra ID 44 | AttributeCollectionSubmitResponse r = new AttributeCollectionSubmitResponse(); 45 | 46 | // Check the input attributes and return a generic error message 47 | if (requestPayload.data.userSignUpInfo == null || 48 | requestPayload.data.userSignUpInfo.attributes == null || 49 | requestPayload.data.userSignUpInfo.attributes.country == null || 50 | requestPayload.data.userSignUpInfo.attributes.country.value == null || 51 | requestPayload.data.userSignUpInfo.attributes.city == null || 52 | requestPayload.data.userSignUpInfo.attributes.city.value == null) 53 | { 54 | r.data.actions[0].odatatype = AttributeCollectionSubmitResponse_ActionTypes.ShowBlockPage; 55 | r.data.actions[0].message = "Can't find the country and/or city attributes."; 56 | return r; 57 | } 58 | 59 | // Demonstrates the use of block response 60 | if (requestPayload.data.userSignUpInfo.attributes.city.value!.ToLower() == "block") 61 | { 62 | r.data.actions[0].odatatype = AttributeCollectionSubmitResponse_ActionTypes.ShowBlockPage; 63 | r.data.actions[0].message = "You can't create an account with 'block' city."; 64 | return r; 65 | } 66 | 67 | // Demonstrates the use of update response 68 | if (requestPayload.data.userSignUpInfo.attributes.city.value!.ToLower() == "modify") 69 | { 70 | r.data.actions[0].odatatype = AttributeCollectionSubmitResponse_ActionTypes.ModifyAttributeValues; 71 | r.data.actions[0].attributes = new AttributeCollectionSubmitResponse_Attribute(); 72 | 73 | // Modify the displayName to capitalized string 74 | if (requestPayload.data.userSignUpInfo.attributes.displayName != null && 75 | string.IsNullOrEmpty(requestPayload.data.userSignUpInfo.attributes.displayName.value) == false) 76 | { 77 | string displayName = requestPayload.data.userSignUpInfo.attributes.displayName.value!.ToLower(); 78 | r.data.actions[0].attributes.DisplayName = 79 | string.Concat(displayName[0].ToString().ToUpper(), displayName.AsSpan(1)); 80 | } 81 | 82 | r.data.actions[0].attributes.City = "Madrid"; 83 | 84 | // Update a property that is not on the sign-up page 85 | r.data.actions[0].attributes.PreferredLanguage = "en-us"; 86 | 87 | return r; 88 | } 89 | 90 | // Check the country name in on the supported list 91 | if (!CountriesList.ContainsKey(requestPayload.data.userSignUpInfo.attributes.country.value!)) 92 | { 93 | r.data.actions[0].odatatype = AttributeCollectionSubmitResponse_ActionTypes.ShowValidationError; 94 | r.data.actions[0].message = "Please fix the following issues to proceed."; 95 | r.data.actions[0].attributeErrors = new AttributeCollectionSubmitResponse_AttributeError(); 96 | r.data.actions[0].attributeErrors.country = $"We don't operate in '{requestPayload.data.userSignUpInfo.attributes.country.value}'"; 97 | return r; 98 | } 99 | 100 | // Get the countries' cities 101 | string cities = CountriesList[requestPayload.data.userSignUpInfo.attributes.country.value!]; 102 | 103 | // Check if the city provided by user in the supported list 104 | if (!(cities + ",").ToLower().Contains($" {requestPayload.data.userSignUpInfo.attributes.city.value!.ToLower()},")) 105 | { 106 | r.data.actions[0].odatatype = AttributeCollectionSubmitResponse_ActionTypes.ShowValidationError; 107 | r.data.actions[0].message = "Please fix the following issues to proceed."; 108 | r.data.actions[0].attributeErrors = new AttributeCollectionSubmitResponse_AttributeError(); 109 | r.data.actions[0].attributeErrors.city = $"We don't operate in this city. Please select one of the following:{cities}"; 110 | } 111 | else 112 | { 113 | // No issues have been identified, proceed to create the account 114 | r.data.actions[0].odatatype = AttributeCollectionSubmitResponse_ActionTypes.ContinueWithDefaultBehavior; 115 | } 116 | 117 | return r; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Controllers/OnOtpSendController.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Azure.Communication.Email; 3 | using Microsoft.ApplicationInsights; 4 | using Microsoft.AspNetCore.Authentication.JwtBearer; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using woodgroveapi.Helpers; 8 | using woodgroveapi.Models; 9 | 10 | namespace woodgroveapi.Controllers; 11 | 12 | 13 | [Authorize(AuthenticationSchemes = "EntraExternalIdCustomAuthToken")] 14 | [ApiController] 15 | [Route("[controller]")] 16 | public class OnOtpSendController : ControllerBase 17 | { 18 | private readonly ILogger _logger; 19 | private readonly IConfiguration _configuration; 20 | private TelemetryClient _telemetry; 21 | 22 | public OnOtpSendController(ILogger logger, TelemetryClient telemetry, IConfiguration configuration) 23 | { 24 | _logger = logger; 25 | _configuration = configuration; 26 | _telemetry = telemetry; 27 | } 28 | 29 | [HttpPost(Name = "OnOtpSend")] 30 | public async Task PostAsync([FromBody] OnOtpSendRequest requestPayload) 31 | { 32 | try 33 | { 34 | // Track the page view 35 | IDictionary moreProperties = new Dictionary(); 36 | if (requestPayload.data.otpContext.identifier.IndexOf("@") > 0) 37 | moreProperties.Add("Identifier", requestPayload.data.otpContext.identifier.Substring(0, 1) + "_" + requestPayload.data.otpContext.identifier.Split("@")[1]); 38 | 39 | AppInsightsHelper.TrackApi("OnOtpSend", this._telemetry, requestPayload.data, moreProperties); 40 | 41 | //For Azure App Service with Easy Auth, validate the azp claim value 42 | // if (!AzureAppServiceClaimsHeader.Authorize(this.Request)) 43 | // { 44 | // AppInsightsHelper.TrackError("OnOtpSend", new Exception("Unauthorized"), this._telemetry, requestPayload.data); 45 | // Response.StatusCode = (int)HttpStatusCode.Unauthorized; 46 | // return null; 47 | // } 48 | 49 | if (_configuration.GetSection("AppSettings:EmailConnectionString").Value == "") 50 | { 51 | return new OnOtpSendResponse(); 52 | } 53 | 54 | var emailClient = new EmailClient(_configuration.GetSection("AppSettings:EmailConnectionString").Value); 55 | 56 | var subject = "Your Woodgrove account verification code"; 57 | var htmlContent = @$" 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 90 | 93 | 94 | 95 |
Woodgrove Groceries live demo
Your Woodgrove verification code
To access Woodgrove Groceries's app, please copy and enter the code below into the sign-up or sign-in page. This code is valid for 30 minutes.
Your account verification code:
75 | {requestPayload.data.otpContext.onetimecode} 77 | 78 |
If you didn't request a code, you can ignore this email.
Best regards,
88 | 89 | 91 | Privacy Statement 92 |
96 |
97 | "; 98 | 99 | var sender = "donotreply@woodgrovedemo.com"; 100 | 101 | EmailSendOperation emailSendOperation = await emailClient.SendAsync( 102 | Azure.WaitUntil.Started, 103 | sender, 104 | requestPayload.data.otpContext.identifier, 105 | subject, 106 | htmlContent); 107 | 108 | } 109 | catch (System.Exception ex) 110 | { 111 | AppInsightsHelper.TrackError("OnOtpSend", ex, this._telemetry, requestPayload.data); 112 | } 113 | 114 | return new OnOtpSendResponse(); 115 | } 116 | 117 | 118 | } -------------------------------------------------------------------------------- /Controllers/OnTokenIssuanceStartController.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Microsoft.ApplicationInsights; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Caching.Memory; 6 | using woodgroveapi.Helpers; 7 | using woodgroveapi.Models; 8 | 9 | namespace woodgroveapi.Controllers; 10 | 11 | 12 | //[Authorize] 13 | [ApiController] 14 | [Route("[controller]")] 15 | public class OnTokenIssuanceStartController : ControllerBase 16 | { 17 | private readonly ILogger _logger; 18 | private TelemetryClient _telemetry; 19 | private readonly IMemoryCache _memoryCache; 20 | 21 | public OnTokenIssuanceStartController(ILogger logger, IMemoryCache memoryCache, TelemetryClient telemetry) 22 | { 23 | _logger = logger; 24 | _memoryCache = memoryCache; 25 | _telemetry = telemetry; 26 | } 27 | 28 | [HttpPost(Name = "OnTokenIssuanceStart")] 29 | public TokenIssuanceStartResponse PostAsync([FromBody] TokenIssuanceStartRequest requestPayload) 30 | { 31 | // Track the page view 32 | AppInsightsHelper.TrackApi("OnTokenIssuanceStart", this._telemetry, requestPayload.data); 33 | 34 | //For Azure App Service with Easy Auth, validate the azp claim value 35 | //if (!AzureAppServiceClaimsHeader.Authorize(this.Request)) 36 | //{ 37 | // Response.StatusCode = (int)HttpStatusCode.Unauthorized; 38 | // return null; 39 | //} 40 | 41 | // Read the correlation ID from the Microsoft Entra ID request 42 | string correlationId = requestPayload.data.authenticationContext.correlationId; ; 43 | 44 | // Claims to return to Microsoft Entra ID 45 | TokenIssuanceStartResponse r = new TokenIssuanceStartResponse(); 46 | r.data.actions[0].claims.CorrelationId = correlationId; 47 | r.data.actions[0].claims.ApiVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(); 48 | 49 | // Loyalty program data 50 | Random random = new Random(); 51 | string[] tiers = { "Silver", "Gold", "Platinum", "Diamond" }; 52 | r.data.actions[0].claims.LoyaltyNumber = random.Next(123467, 999989).ToString(); 53 | r.data.actions[0].claims.LoyaltySince = DateTime.Now.AddDays((-1) * random.Next(30, 365)).ToString("dd MMMM yyyy"); 54 | r.data.actions[0].claims.LoyaltyTier = tiers[random.Next(0, tiers.Length)]; 55 | 56 | // Custom roles 57 | r.data.actions[0].claims.CustomRoles.Add("Writer"); 58 | r.data.actions[0].claims.CustomRoles.Add("Editor"); 59 | 60 | 61 | // Try to get the cache object for the current user 62 | string userId = requestPayload.data.authenticationContext.user!.id!; 63 | 64 | if (_memoryCache.TryGetValue(userId, out ActAsEntity actAsEntity)) 65 | { 66 | r.data.actions[0].claims.ActAs = actAsEntity!.ActAs; 67 | 68 | // Remove the user's data from the cache 69 | _memoryCache.Remove(userId); 70 | } 71 | return r; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Controllers/OtherServices/ActAsDemoController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Caching.Memory; 5 | using woodgroveapi.Models; 6 | 7 | namespace woodgroveapi.Controllers; 8 | 9 | 10 | [Authorize(AuthenticationSchemes = "EntraExternalIdUserToken")] 11 | [ApiController] 12 | [Route("[controller]")] 13 | public class ActAsDemoController : ControllerBase 14 | { 15 | private readonly ILogger _logger; 16 | private TelemetryClient _telemetry; 17 | private readonly IMemoryCache _memoryCache; 18 | 19 | public ActAsDemoController(ILogger logger, IMemoryCache memoryCache, TelemetryClient telemetry) 20 | { 21 | _logger = logger; 22 | _memoryCache = memoryCache; 23 | _telemetry = telemetry; 24 | } 25 | 26 | [HttpPost(Name = "ActAs")] 27 | public IActionResult OnPost([FromBody] ActAsRequest request) 28 | { 29 | // Check the user object ID 30 | string? oid = User.Claims.FirstOrDefault(c => c.Type.ToLower() == "oid")?.Value; 31 | if (oid == null) 32 | { 33 | return Unauthorized(); 34 | } 35 | 36 | // Create cache object 37 | ActAsEntity actAsEntity = new ActAsEntity() 38 | { 39 | UserId = oid, 40 | ActAs = request.ActAs 41 | }; 42 | 43 | var cacheEntryOptions = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(5)); 44 | _memoryCache.Set(oid, actAsEntity, cacheEntryOptions); 45 | 46 | return Ok(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Controllers/Temporary/EchoController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights; 2 | using Microsoft.ApplicationInsights.DataContracts; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using woodgroveapi.Models; 6 | 7 | namespace woodgroveapi.Controllers; 8 | 9 | 10 | //[Authorize] 11 | [ApiController] 12 | [Route("[controller]")] 13 | public class EchoController : ControllerBase 14 | { 15 | private readonly ILogger _logger; 16 | private TelemetryClient _telemetry; 17 | 18 | 19 | public EchoController(ILogger logger, TelemetryClient telemetry) 20 | { 21 | _logger = logger; 22 | _telemetry = telemetry; 23 | } 24 | 25 | [HttpPost(Name = "Echo")] 26 | public async Task PostAsync() 27 | { 28 | // Track the page view 29 | PageViewTelemetry pageView = new PageViewTelemetry("Echo"); 30 | _telemetry.TrackPageView(pageView); 31 | 32 | _logger.LogInformation($"#### call to: {this.GetType().Name}"); 33 | 34 | // Validate that REST API received a bearer token in the authorization header. 35 | if (Request.Headers.Authorization.Count == 0) 36 | { 37 | _logger.LogInformation("#### authorization header not found"); 38 | } 39 | else 40 | { 41 | _logger.LogInformation($"#### authorization header: {Request.Headers.Authorization[0]}"); 42 | } 43 | 44 | // Echo the input data 45 | string requestBody = await new StreamReader(this.Request.Body).ReadToEndAsync(); 46 | 47 | _logger.LogInformation($"#### {requestBody}"); 48 | 49 | return "Echo"; 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Controllers/Temporary/SignUpStartsTestController.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Microsoft.ApplicationInsights; 3 | using Microsoft.AspNetCore.Mvc; 4 | using woodgroveapi.Helpers; 5 | using woodgroveapi.Models; 6 | 7 | namespace woodgroveapi.Controllers; 8 | 9 | //[Authorize] 10 | [ApiController] 11 | [Route("[controller]")] 12 | public class SignUpStartsTestController : ControllerBase 13 | { 14 | private readonly ILogger _logger; 15 | private TelemetryClient _telemetry; 16 | private readonly IConfiguration _configuration; 17 | 18 | public SignUpStartsTestController(ILogger logger, TelemetryClient telemetry, IConfiguration configuration) 19 | { 20 | _logger = logger; 21 | _telemetry = telemetry; 22 | _configuration = configuration; 23 | } 24 | 25 | [HttpPost(Name = "SignUpStartsTest")] 26 | public AttributeCollectionStartResponse PostAsync([FromBody] AttributeCollectionRequest requestPayload) 27 | { 28 | //For Azure App Service with Easy Auth, validate the azp claim value 29 | // if (!AzureAppServiceClaimsHeader.Authorize(this.Request)) 30 | // { 31 | // AppInsightsHelper.TrackError("SignUpStartsTest", new Exception("Unauthorized"), this._telemetry, requestPayload.data); 32 | // Response.StatusCode = (int)HttpStatusCode.Unauthorized; 33 | // return null; 34 | // } 35 | 36 | var SimulateDelayInMiliSeconds = 0; 37 | int.TryParse(_configuration.GetSection("Demos:SimulateDelayMilliseconds").Value, out SimulateDelayInMiliSeconds); 38 | 39 | if (SimulateDelayInMiliSeconds > 0) 40 | Thread.Sleep(SimulateDelayInMiliSeconds); 41 | 42 | // Track the page view 43 | AppInsightsHelper.TrackApi("SignUpStartsTest", this._telemetry, requestPayload.data); 44 | 45 | // Message object to return to Microsoft Entra ID 46 | AttributeCollectionStartResponse r = new AttributeCollectionStartResponse(); 47 | r.data.actions[0].odatatype = AttributeCollectionStartResponse_ActionTypes.SetPrefillValues; 48 | r.data.actions[0].inputs = new AttributeCollectionStartResponse_Inputs(); 49 | 50 | // Return the country and city 51 | r.data.actions[0].inputs.country = "es"; 52 | // r.data.actions[0].inputs.city = "Madrind"; 53 | 54 | // Return a promo code 55 | Random random = new Random(); 56 | r.data.actions[0].inputs.promoCode = $"Promo code #{random.Next(1236, 9873)}"; 57 | 58 | return r; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Controllers/onPageRenderStartController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using woodgroveapi.Helpers; 5 | using woodgroveapi.Models; 6 | 7 | namespace woodgroveapi.Controllers; 8 | 9 | 10 | //[Authorize] 11 | [ApiController] 12 | [Route("[controller]")] 13 | public class OnPageRenderStartController : ControllerBase 14 | { 15 | private readonly ILogger _logger; 16 | private TelemetryClient _telemetry; 17 | 18 | public OnPageRenderStartController(ILogger logger, TelemetryClient telemetry) 19 | { 20 | _logger = logger; 21 | _telemetry = telemetry; 22 | } 23 | 24 | [HttpPost(Name = "OnPageRenderStart")] 25 | public PageRenderStartResponse PostAsync([FromBody] PageRenderStartRequest requestPayload) 26 | { 27 | //For Azure App Service with Easy Auth, validate the azp claim value 28 | //if (!AzureAppServiceClaimsHeader.Authorize(this.Request)) 29 | //{ 30 | // Response.StatusCode = (int)HttpStatusCode.Unauthorized; 31 | // return null; 32 | //} 33 | 34 | // Track the page view 35 | IDictionary moreProperties = new Dictionary(); 36 | moreProperties.Add("Page", requestPayload.data.pageId); 37 | AppInsightsHelper.TrackApi("OnPageRenderStart", this._telemetry, requestPayload.data, moreProperties); 38 | 39 | PageRenderStartResponse r = new PageRenderStartResponse(); 40 | r.type = requestPayload.type; 41 | r.source = requestPayload.source; 42 | 43 | var branding = r.data.actions[0].tenantBranding; 44 | string appUrl = ""; 45 | string welcome = ""; 46 | 47 | switch (requestPayload.data.authenticationContext.clientServicePrincipal.appId) 48 | { 49 | case "7a30b8ed-42a3-4d1e-89ad-14d4ca3c9a52": 50 | appUrl = "https://woodgrovebanking.com"; 51 | welcome = "**Woodgrove online bank**"; 52 | break; 53 | 54 | case "65d59577-c9d1-485b-87a5-80b92a99fbfa": 55 | appUrl = "https://woodgroverestaurants.com"; 56 | welcome = "**Woodgrove restaurant**"; 57 | break; 58 | 59 | default: 60 | appUrl = "https://woodgrovedemo.com"; 61 | welcome = "**Woodgrove groceries** online store"; 62 | break; 63 | } 64 | 65 | r.data.actions[0].tenantBranding = RetrieveBranding(appUrl, welcome); 66 | return r; 67 | } 68 | 69 | private PageRenderStartResponse_TenantBranding RetrieveBranding(string appUrl, string welcome) 70 | { 71 | PageRenderStartResponse_TenantBranding branding = new PageRenderStartResponse_TenantBranding(); 72 | 73 | branding.customCSS = $"{appUrl}/Company-branding/customcss.css"; 74 | branding.backgroundImage = $"{appUrl}/Company-branding/background.jpeg"; 75 | branding.favicon = $"{appUrl}/Company-branding/favicon.png"; 76 | 77 | // Header 78 | branding.loginPageLayoutConfiguration = new PageRenderStartResponse_LoginPageLayoutConfiguration(); 79 | branding.loginPageLayoutConfiguration.isHeaderShown = true; 80 | branding.loginPageLayoutConfiguration.isFooterShown = true; 81 | branding.headerLogo = $"{appUrl}/Company-branding/headerlogo.png"; 82 | 83 | // Sign in box 84 | branding.signInPageText = $"Welcome to {welcome}. Sign-in with your credentials, or create a new account. You can also sign-in with your *social accounts*, such as Facebook or Google. For help, please [contact us](https://woodgrovedemo.com/help)."; 85 | branding.bannerLogo = $"{appUrl}/Company-branding/bannerlogo.png"; 86 | 87 | // Terms of use 88 | branding.customTermsOfUseText = "Woodgrove terms of use"; 89 | branding.customTermsOfUseUrl = $"{appUrl}/tos"; 90 | 91 | // Privacy & Cookies statement 92 | branding.customPrivacyAndCookiesText = "Privacy & Cookies statement"; 93 | branding.customPrivacyAndCookiesUrl = $"{appUrl}/privacy"; 94 | 95 | //branding.contentCustomization = new PageRenderStartResponse_ContentCustomization(); 96 | // branding.contentCustomization.attributeCollection= new PageRenderStartResponse_AttributeCollection(); 97 | // branding.contentCustomization.attributeCollection.signIn_Description = "This is my test"; 98 | // branding.contentCustomization.attributeCollection.signIn_Title = "This is my test"; 99 | 100 | 101 | //branding.contentCustomization.attributeCollection = "[{\"key\": \"SignIn_Description\", \"value\": \"This is my test\" }, { \"key\": \"SignIn_Title\", \"value\": \"This is my test\" }]"; 102 | 103 | 104 | 105 | // branding.contentCustomization.attributeCollection = new List(); 106 | // branding.contentCustomization.attributeCollection.Add( new PageRenderStartResponse_AttributeCollection("SignIn_Description", "This is my test")); 107 | // branding.contentCustomization.attributeCollection.Add( new PageRenderStartResponse_AttributeCollection("SignIn_Title", "This is my test")); 108 | 109 | return branding; 110 | 111 | } 112 | } -------------------------------------------------------------------------------- /Helpers/AppInsightsHelper.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Net; 3 | using System.Security.Cryptography; 4 | using Microsoft.ApplicationInsights; 5 | using Microsoft.ApplicationInsights.DataContracts; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | using Microsoft.Extensions.Caching.Memory; 8 | using woodgroveapi.Models; 9 | 10 | 11 | namespace woodgroveapi.Helpers; 12 | 13 | public class AppInsightsHelper 14 | { 15 | 16 | public static void TrackApi(string name, TelemetryClient telemetry, AllRequestData requestData, IDictionary? moreProperties = null) 17 | { 18 | PageViewTelemetry pageView = new PageViewTelemetry(name); 19 | pageView.Properties.Add("TenantId", requestData.tenantId); 20 | pageView.Properties.Add("CorrelationId", requestData.authenticationContext.correlationId); 21 | pageView.Properties.Add("EventListenerId", requestData.authenticationEventListenerId); 22 | pageView.Properties.Add("AuthenticationExtensionId", requestData.customAuthenticationExtensionId); 23 | pageView.Properties.Add("Protocol", requestData.authenticationContext.protocol); 24 | pageView.Properties.Add("AppDisplayName", requestData.authenticationContext.clientServicePrincipal.appDisplayName); 25 | pageView.Properties.Add("AppId", requestData.authenticationContext.clientServicePrincipal.appId); 26 | 27 | // OnAttributeCollectionStart's specific properties 28 | if (requestData is AttributeCollectionRequest_Data) 29 | { 30 | AttributeCollectionRequest_Data attributeCollectionRequestData = (AttributeCollectionRequest_Data)requestData; 31 | 32 | if (attributeCollectionRequestData.userSignUpInfo.identities != null && 33 | attributeCollectionRequestData.userSignUpInfo.identities.Count > 0) 34 | { 35 | pageView.Properties.Add("UserSignUpInfoIssuer", attributeCollectionRequestData.userSignUpInfo.identities[0].issuer); 36 | pageView.Properties.Add("UserSignUpInfoSignInType", attributeCollectionRequestData.userSignUpInfo.identities[0].signInType); 37 | 38 | // For local account get the email suffix 39 | if (attributeCollectionRequestData.userSignUpInfo.identities[0].signInType == "emailAddress") 40 | { 41 | string email = attributeCollectionRequestData.userSignUpInfo.identities[0].issuerAssignedId; 42 | pageView.Properties.Add("UserSignUpInfoEmailSuffix", email.Substring(email.IndexOf('@'))); 43 | } 44 | } 45 | } 46 | 47 | if (moreProperties != null) 48 | foreach (var item in moreProperties) 49 | { 50 | pageView.Properties.Add(item.Key, item.Value); 51 | } 52 | 53 | telemetry.TrackPageView(pageView); 54 | } 55 | 56 | public static void TrackError(string name, Exception ex, TelemetryClient telemetry, AllRequestData requestData, IDictionary? moreProperties = null) 57 | { 58 | ExceptionTelemetry exception = new ExceptionTelemetry(ex); 59 | exception.Properties.Add("ApiName", name); 60 | exception.Properties.Add("TenantId", requestData.tenantId); 61 | exception.Properties.Add("CorrelationId", requestData.authenticationContext.correlationId); 62 | exception.Properties.Add("EventListenerId", requestData.authenticationEventListenerId); 63 | exception.Properties.Add("AuthenticationExtensionId", requestData.customAuthenticationExtensionId); 64 | exception.Properties.Add("Protocol", requestData.authenticationContext.protocol); 65 | exception.Properties.Add("AppDisplayName", requestData.authenticationContext.clientServicePrincipal.appDisplayName); 66 | exception.Properties.Add("AppId", requestData.authenticationContext.clientServicePrincipal.appId); 67 | 68 | if (moreProperties != null) 69 | foreach (var item in moreProperties) 70 | { 71 | exception.Properties.Add(item.Key, item.Value); 72 | } 73 | 74 | telemetry.TrackException(exception); 75 | } 76 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /Models/ActAsEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | namespace woodgroveapi.Models; 3 | public class ActAsEntity 4 | { 5 | public string UserId { get; set; } 6 | public string ActAs { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /Models/ActAsRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | namespace woodgroveapi.Models; 3 | public class ActAsRequest 4 | { 5 | public string ActAs { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /Models/AllRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace woodgroveapi.Models 4 | { 5 | 6 | public class AllRequestData 7 | { 8 | [JsonPropertyName("@odata.type")] 9 | public string odatatype { get; set; } 10 | public string tenantId { get; set; } 11 | public string authenticationEventListenerId { get; set; } 12 | public string customAuthenticationExtensionId { get; set; } 13 | public AuthenticationContext authenticationContext { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /Models/AttributeCollectionRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace woodgroveapi.Models 5 | { 6 | 7 | public class AttributeCollectionRequest 8 | { 9 | [JsonPropertyName("data")] 10 | public AttributeCollectionRequest_Data data { get; set; } 11 | public AttributeCollectionRequest() 12 | { 13 | data = new AttributeCollectionRequest_Data(); 14 | } 15 | 16 | public override string ToString() 17 | { 18 | return JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); 19 | } 20 | } 21 | 22 | public class AttributeCollectionRequest_Data : AllRequestData 23 | { 24 | public UserSignUpInfo userSignUpInfo { get; set; } 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /Models/AttributeCollectionStartResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace woodgroveapi.Models 4 | { 5 | 6 | public class AttributeCollectionStartResponse 7 | { 8 | [JsonPropertyName("data")] 9 | public AttributeCollectionStartResponse_Data data { get; set; } 10 | public AttributeCollectionStartResponse() 11 | { 12 | data = new AttributeCollectionStartResponse_Data(); 13 | data.odatatype = "microsoft.graph.onAttributeCollectionStartResponseData"; 14 | 15 | this.data.actions = new List(); 16 | this.data.actions.Add(new AttributeCollectionStartResponse_Action()); 17 | } 18 | } 19 | 20 | public class AttributeCollectionStartResponse_Data 21 | { 22 | [JsonPropertyName("@odata.type")] 23 | public string odatatype { get; set; } 24 | public List actions { get; set; } 25 | } 26 | 27 | public class AttributeCollectionStartResponse_Action 28 | { 29 | [JsonPropertyName("@odata.type")] 30 | public string odatatype { get; set; } 31 | 32 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 33 | public string message { get; set; } 34 | 35 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 36 | public AttributeCollectionStartResponse_Inputs inputs { get; set; } 37 | } 38 | 39 | public class AttributeCollectionStartResponse_Inputs 40 | { 41 | /// 42 | /// Country 43 | /// 44 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 45 | public string country { get; set; } 46 | 47 | /// 48 | /// City 49 | /// 50 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 51 | public string city { get; set; } 52 | 53 | /// 54 | /// Promotion code 55 | /// 56 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 57 | [JsonPropertyName("extension_0cae61cc83e94edd978ec2fde3c5f2f3_PromoCode")] 58 | public string promoCode { get; set; } 59 | } 60 | 61 | public class AttributeCollectionStartResponse_ActionTypes 62 | { 63 | public const string SetPrefillValues = "microsoft.graph.attributeCollectionStart.setPrefillValues"; 64 | public const string ContinueWithDefaultBehavior = "microsoft.graph.attributeCollectionStart.continueWithDefaultBehavior"; 65 | public const string ShowBlockPage = "microsoft.graph.attributeCollectionStart.showBlockPage"; 66 | } 67 | } -------------------------------------------------------------------------------- /Models/AttributeCollectionSubmitResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace woodgroveapi.Models 4 | { 5 | 6 | public class AttributeCollectionSubmitResponse 7 | { 8 | [JsonPropertyName("data")] 9 | public AttributeCollectionSubmitResponse_Data data { get; set; } 10 | public AttributeCollectionSubmitResponse() 11 | { 12 | data = new AttributeCollectionSubmitResponse_Data(); 13 | data.odatatype = "microsoft.graph.onAttributeCollectionSubmitResponseData"; 14 | 15 | this.data.actions = new List(); 16 | this.data.actions.Add(new AttributeCollectionSubmitResponse_Action()); 17 | } 18 | } 19 | 20 | public class AttributeCollectionSubmitResponse_Data 21 | { 22 | [JsonPropertyName("@odata.type")] 23 | public string odatatype { get; set; } 24 | public List actions { get; set; } 25 | } 26 | 27 | public class AttributeCollectionSubmitResponse_Action 28 | { 29 | [JsonPropertyName("@odata.type")] 30 | public string odatatype { get; set; } 31 | 32 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 33 | public string message { get; set; } 34 | 35 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 36 | public AttributeCollectionSubmitResponse_Attribute attributes { get; set; } 37 | 38 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 39 | public AttributeCollectionSubmitResponse_AttributeError attributeErrors { get; set; } 40 | } 41 | 42 | public class AttributeCollectionSubmitResponse_Attribute 43 | { 44 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 45 | [JsonPropertyName("displayName")] 46 | public string? DisplayName { get; set; } 47 | 48 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 49 | [JsonPropertyName("city")] 50 | public string? City { get; set; } 51 | 52 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 53 | [JsonPropertyName("preferredLanguage")] 54 | public string? PreferredLanguage { get; set; } 55 | 56 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 57 | [JsonPropertyName("extension_0cae61cc83e94edd978ec2fde3c5f2f3_SpecialDiet")] 58 | public string? SpecialDiet { get; set; } 59 | } 60 | public class AttributeCollectionSubmitResponse_AttributeError 61 | { 62 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 63 | public string? city { get; set; } 64 | 65 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 66 | [JsonPropertyName("displayName")] 67 | public string? DisplayName { get; set; } 68 | 69 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 70 | public string? country { get; set; } 71 | 72 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 73 | [JsonPropertyName("extension_0cae61cc83e94edd978ec2fde3c5f2f3_SpecialDiet")] 74 | public string? specialDiet { get; set; } 75 | } 76 | 77 | public class AttributeCollectionSubmitResponse_ActionTypes 78 | { 79 | public const string ShowValidationError = "microsoft.graph.attributeCollectionSubmit.showValidationError"; 80 | public const string ContinueWithDefaultBehavior = "microsoft.graph.attributeCollectionSubmit.continueWithDefaultBehavior"; 81 | public const string ModifyAttributeValues = "microsoft.graph.attributeCollectionSubmit.modifyAttributeValues"; 82 | public const string ShowBlockPage = "microsoft.graph.attributeCollectionSubmit.showBlockPage"; 83 | } 84 | } -------------------------------------------------------------------------------- /Models/AuthenticationContext.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace woodgroveapi.Models 5 | { 6 | public class AuthenticationContext 7 | { 8 | public string correlationId { get; set; } 9 | public AuthenticationContext_Client client { get; set; } 10 | public string protocol { get; set; } 11 | public AuthenticationContext_ServicePrincipal clientServicePrincipal { get; set; } 12 | public AuthenticationContext_ServicePrincipal resourceServicePrincipal { get; set; } 13 | public AuthenticationContext_User? user { get; set; } 14 | } 15 | 16 | public class AuthenticationContext_Client 17 | { 18 | public string ip { get; set; } 19 | public string locale { get; set; } 20 | public string market { get; set; } 21 | } 22 | 23 | public class AuthenticationContext_ServicePrincipal 24 | { 25 | public string id { get; set; } 26 | public string appId { get; set; } 27 | public string appDisplayName { get; set; } 28 | public string displayName { get; set; } 29 | } 30 | 31 | public class AuthenticationContext_User 32 | { 33 | // Display name 34 | [StringLength(120, ErrorMessage = "DisplayName length can't be more than 120.")] 35 | public string? displayName { get; set; } 36 | 37 | // User object ID 38 | [StringLength(120, ErrorMessage = "ID length can't be more than 120.")] 39 | public string? id { get; set; } 40 | 41 | // UPN 42 | [StringLength(120, ErrorMessage = "Surname length can't be more than 120.")] 43 | public string? userPrincipalName { get; set; } 44 | 45 | // User type 46 | [StringLength(120, ErrorMessage = "UserType length can't be more than 120.")] 47 | public string? userType { get; set; } 48 | 49 | // Mail address 50 | [StringLength(120, ErrorMessage = "Mail length can't be more than 120.")] 51 | public string? mail { get; set; } 52 | } 53 | } -------------------------------------------------------------------------------- /Models/AzureAppServiceClaims.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace woodgroveapi.Models 5 | { 6 | 7 | public class AzureAppServiceClaimsHeader 8 | { 9 | public string auth_typ { get; set; } 10 | public List claims { get; set; } 11 | public string name_typ { get; set; } 12 | public string role_typ { get; set; } 13 | 14 | public static bool Authorize(HttpRequest req) 15 | { 16 | // For all language frameworks, App Service makes the claims in the incoming token 17 | // available to your code by injecting them into the request headers. 18 | // For more information, https://learn.microsoft.com/azure/app-service/configure-authentication-user-identities 19 | if (req.Headers.TryGetValue("x-ms-client-principal", out var xMsClientPrincipal)) 20 | { 21 | var json = Encoding.UTF8.GetString(Convert.FromBase64String(xMsClientPrincipal[0]!)); 22 | AzureAppServiceClaimsHeader header = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; 23 | 24 | AzureAppServiceClaim azp = header.claims.Find(x => x.typ == "azp")!; 25 | 26 | // Validate that the 'azp' claim contains the 99045fe1-7639-4a75-9d4a-577b6ca3810f value. 27 | // This value ensures that the Microsoft Entra is the one who calls the API. 28 | // For more information, https://learn.microsoft.com/azure/active-directory/develop/custom-extension-overview#protect-your-rest-api 29 | return (azp != null) && 30 | azp.val == "99045fe1-7639-4a75-9d4a-577b6ca3810f"; 31 | } 32 | 33 | return false; 34 | } 35 | } 36 | 37 | public class AzureAppServiceClaim 38 | { 39 | public string typ { get; set; } 40 | public string val { get; set; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Models/OnOtpSendRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace woodgroveapi.Models 4 | { 5 | 6 | public class OnOtpSendRequest 7 | { 8 | [JsonPropertyName("data")] 9 | public OnOtpSendRequest_Data data { get; set; } 10 | public OnOtpSendRequest() 11 | { 12 | data = new OnOtpSendRequest_Data(); 13 | } 14 | } 15 | 16 | public class OnOtpSendRequest_Data: AllRequestData 17 | { 18 | public OtpContext otpContext { get; set; } 19 | } 20 | 21 | public class OtpContext 22 | { 23 | public string identifier { get; set; } 24 | public string onetimecode { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /Models/OnOtpSendResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace woodgroveapi.Models 5 | { 6 | 7 | public class OnOtpSendResponse 8 | { 9 | public OnOtpSendResponse_Data data { get; set; } 10 | 11 | public OnOtpSendResponse() 12 | { 13 | data = new OnOtpSendResponse_Data(); 14 | 15 | this.data.actions = new List(); 16 | this.data.actions.Add(new OnOtpSendResponse_Action()); 17 | } 18 | } 19 | 20 | public class OnOtpSendResponse_Data 21 | { 22 | [JsonPropertyName("@odata.type")] 23 | public string odatatype { get; set; } 24 | public List actions { get; set; } 25 | 26 | public OnOtpSendResponse_Data() 27 | { 28 | this.odatatype = "microsoft.graph.OnOtpSendResponseData"; 29 | } 30 | } 31 | 32 | public class OnOtpSendResponse_Action 33 | { 34 | [JsonPropertyName("@odata.type")] 35 | public string odatatype { get; set; } 36 | 37 | public OnOtpSendResponse_Action() 38 | { 39 | odatatype = "microsoft.graph.OtpSend.continueWithDefaultBehavior"; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Models/PageRenderStartRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace woodgroveapi.Models 4 | { 5 | 6 | public class PageRenderStartRequest 7 | { 8 | [JsonPropertyName("data")] 9 | public PageRenderStartRequest_Data data { get; set; } 10 | 11 | public string type { get; set; } 12 | 13 | public string source { get; set; } 14 | 15 | // Constructor 16 | public PageRenderStartRequest() 17 | { 18 | data = new PageRenderStartRequest_Data(); 19 | } 20 | } 21 | 22 | public class PageRenderStartRequest_Data: AllRequestData 23 | { 24 | public string pageId { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /Models/PageRenderStartResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace woodgroveapi.Models 5 | { 6 | 7 | 8 | public class PageRenderStartResponse 9 | { 10 | public string type { get; set; } 11 | public string source { get; set; } 12 | public PageRenderStartResponse_Data data { get; set; } 13 | 14 | public PageRenderStartResponse() 15 | { 16 | data = new PageRenderStartResponse_Data(); 17 | 18 | this.data.actions = new List(); 19 | this.data.actions.Add(new PageRenderStartResponse_Action()); 20 | } 21 | } 22 | 23 | public class PageRenderStartResponse_Data 24 | { 25 | public List actions { get; set; } 26 | } 27 | 28 | public class PageRenderStartResponse_Action 29 | { 30 | [JsonPropertyName("@odata.type")] 31 | public string odatatype { get; set; } 32 | public PageRenderStartResponse_TenantBranding tenantBranding { get; set; } 33 | 34 | public PageRenderStartResponse_Action() 35 | { 36 | odatatype = "microsoft.graph.PageRenderStart.OverrideBranding"; 37 | tenantBranding = new PageRenderStartResponse_TenantBranding(); 38 | } 39 | } 40 | 41 | public class PageRenderStartResponse_ContentCustomization 42 | { 43 | public PageRenderStartResponse_ContentCustomization() { } 44 | 45 | public List attributeCollection { get; set; } 46 | 47 | // [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 48 | // public PageRenderStartResponse_AttributeCollection attributeCollection { get; set; } 49 | 50 | 51 | // [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 52 | // public string attributeCollection { get; set; } 53 | 54 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 55 | public string adminConsent { get; set; } 56 | 57 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 58 | public string registrationCampaign { get; set; } 59 | 60 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 61 | public string conditionalAccess { get; set; } 62 | } 63 | 64 | // public class PageRenderStartResponse_AttributeCollection 65 | // { 66 | // public string signIn_Description { get; set; } 67 | // public string signIn_Title { get; set; } 68 | // } 69 | 70 | public class PageRenderStartResponse_AttributeCollection 71 | { 72 | public PageRenderStartResponse_AttributeCollection() 73 | { 74 | 75 | } 76 | 77 | public PageRenderStartResponse_AttributeCollection(string key, string value) 78 | { 79 | this.key = key; 80 | this.value = value; 81 | } 82 | public string key { get; set; } 83 | public string value { get; set; } 84 | } 85 | 86 | public class PageRenderStartResponse_LoginPageLayoutConfiguration 87 | { 88 | public bool isHeaderShown { get; set; } 89 | public int layoutTemplateType { get; set; } 90 | public bool isFooterShown { get; set; } 91 | } 92 | 93 | public class PageRenderStartResponse_LoginPageTextVisibilitySettings 94 | { 95 | public bool hideCannotAccessYourAccount { get; set; } 96 | public bool hideForgotMyPassword { get; set; } 97 | public bool hideTermsOfUse { get; set; } 98 | public bool hidePrivacyAndCookies { get; set; } 99 | } 100 | 101 | public class PageRenderStartResponse_TenantBranding 102 | { 103 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 104 | public string customAccountResetCredentialsUrl { get; set; } 105 | 106 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 107 | public string customCannotAccessYourAccountText { get; set; } 108 | 109 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 110 | public string customForgotMyPasswordText { get; set; } 111 | 112 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 113 | public string backgroundColor { get; set; } 114 | 115 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 116 | public string bannerLogo { get; set; } 117 | 118 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 119 | public string signInPageText { get; set; } 120 | 121 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 122 | public string customCSS { get; set; } 123 | 124 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 125 | public string customPrivacyAndCookiesUrl { get; set; } 126 | 127 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 128 | public string customPrivacyAndCookiesText { get; set; } 129 | 130 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 131 | public string customTermsOfUseUrl { get; set; } 132 | 133 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 134 | public string customTermsOfUseText { get; set; } 135 | 136 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 137 | public string backgroundImage { get; set; } 138 | 139 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 140 | public string headerLogo { get; set; } 141 | 142 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 143 | public PageRenderStartResponse_LoginPageLayoutConfiguration loginPageLayoutConfiguration { get; set; } 144 | 145 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 146 | public PageRenderStartResponse_ContentCustomization contentCustomization { get; set; } 147 | 148 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 149 | public PageRenderStartResponse_LoginPageTextVisibilitySettings loginPageTextVisibilitySettings { get; set; } 150 | 151 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 152 | public string favicon { get; set; } 153 | 154 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 155 | public string squareLogoDark { get; set; } 156 | 157 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 158 | public string squareLogo { get; set; } 159 | 160 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 161 | public string usernameHintText { get; set; } 162 | } 163 | 164 | 165 | } -------------------------------------------------------------------------------- /Models/TokenIssuanceStartRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace woodgroveapi.Models 4 | { 5 | 6 | public class TokenIssuanceStartRequest 7 | { 8 | [JsonPropertyName("data")] 9 | public TokenIssuanceStartRequest_Data data { get; set; } 10 | public TokenIssuanceStartRequest() 11 | { 12 | data = new TokenIssuanceStartRequest_Data(); 13 | } 14 | } 15 | 16 | public class TokenIssuanceStartRequest_Data: AllRequestData 17 | { 18 | 19 | } 20 | } -------------------------------------------------------------------------------- /Models/TokenIssuanceStartResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace woodgroveapi.Models 4 | { 5 | 6 | public class TokenIssuanceStartResponse 7 | { 8 | [JsonPropertyName("data")] 9 | public TokenIssuanceStartResponse_Data data { get; set; } 10 | public TokenIssuanceStartResponse() 11 | { 12 | data = new TokenIssuanceStartResponse_Data(); 13 | data.odatatype = "microsoft.graph.onTokenIssuanceStartResponseData"; 14 | 15 | this.data.actions = new List(); 16 | this.data.actions.Add(new TokenIssuanceStartResponse_Action()); 17 | } 18 | } 19 | 20 | public class TokenIssuanceStartResponse_Data 21 | { 22 | [JsonPropertyName("@odata.type")] 23 | public string odatatype { get; set; } 24 | public List actions { get; set; } 25 | } 26 | 27 | public class TokenIssuanceStartResponse_Action 28 | { 29 | [JsonPropertyName("@odata.type")] 30 | public string odatatype { get; set; } 31 | 32 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 33 | public TokenIssuanceStartResponse_Claims claims { get; set; } 34 | 35 | public TokenIssuanceStartResponse_Action() 36 | { 37 | odatatype = "microsoft.graph.tokenIssuanceStart.provideClaimsForToken"; 38 | claims = new TokenIssuanceStartResponse_Claims(); 39 | } 40 | } 41 | 42 | public class TokenIssuanceStartResponse_Claims 43 | { 44 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 45 | public string CorrelationId { get; set; } 46 | 47 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 48 | public string LoyaltyNumber { get; set; } 49 | 50 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 51 | public string LoyaltySince { get; set; } 52 | 53 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 54 | public string LoyaltyTier { get; set; } 55 | 56 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 57 | public string ApiVersion { get; set; } 58 | 59 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 60 | [JsonPropertyName("act_as")] 61 | public string ActAs { get; set; } 62 | 63 | public List CustomRoles { get; set; } 64 | public TokenIssuanceStartResponse_Claims() 65 | { 66 | CustomRoles = new List(); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Models/UserSignUpInfo.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace woodgroveapi.Models 5 | { 6 | 7 | public class UserSignUpInfo 8 | { 9 | public UserSignUpInfo_Attributes attributes { get; set; } 10 | public List? identities { get; set; } 11 | } 12 | 13 | public class UserSignUpInfo_Attributes 14 | { 15 | public UserSignUpInfo_Attribute? email { get; set; } 16 | public UserSignUpInfo_Attribute? city { get; set; } 17 | public UserSignUpInfo_Attribute? country { get; set; } 18 | public UserSignUpInfo_Attribute? displayName { get; set; } 19 | 20 | [JsonPropertyName("extension_0cae61cc83e94edd978ec2fde3c5f2f3_SpecialDiet")] 21 | public UserSignUpInfo_Attribute? specialDiet { get; set; } 22 | [JsonPropertyName("extension_0cae61cc83e94edd978ec2fde3c5f2f3_PromoCode")] 23 | public UserSignUpInfo_Attribute? promoCode { get; set; } 24 | } 25 | 26 | public class UserSignUpInfo_Attribute 27 | { 28 | public string? value { get; set; } 29 | 30 | [JsonPropertyName("@odata.type")] 31 | public string odatatype { get; set; } 32 | public string attributeType { get; set; } 33 | } 34 | 35 | public class UserSignUpInfo_Identities 36 | { 37 | public string signInType { get; set; } 38 | public string issuer { get; set; } 39 | public string issuerAssignedId { get; set; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging.AzureAppServices; 2 | using Microsoft.AspNetCore.Authentication.JwtBearer; 3 | using Microsoft.OpenApi.Models; 4 | using Microsoft.IdentityModel.Tokens; 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | 8 | // Add Azure stream log service 9 | builder.Logging.AddAzureWebAppDiagnostics(); 10 | builder.Services.Configure(options => 11 | { 12 | options.FileName = "azure-diagnostics-"; 13 | options.FileSizeLimit = 50 * 1024; 14 | options.RetainedFileCountLimit = 5; 15 | }); 16 | builder.Logging.AddFilter((provider, category, logLevel) => 17 | { 18 | return provider!.ToLower().Contains("woodgroveapi"); 19 | }); 20 | 21 | ConfigurationSection entraExternalIdCustomAuthTokenSettings = (ConfigurationSection)builder.Configuration.GetSection("EntraExternalIdCustomAuthToken"); 22 | 23 | // Reference: 24 | // There is an issue validating the first party token with 25 | // https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer 26 | // https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication 27 | builder.Services.AddAuthentication() 28 | .AddJwtBearer("EntraExternalIdCustomAuthToken", jwtOptions => 29 | { 30 | jwtOptions.MetadataAddress = entraExternalIdCustomAuthTokenSettings["MetadataAddress"]!; 31 | jwtOptions.Audience = entraExternalIdCustomAuthTokenSettings["Audience"]; 32 | jwtOptions.IncludeErrorDetails = true; 33 | jwtOptions.MapInboundClaims = false; 34 | jwtOptions.TokenValidationParameters = new TokenValidationParameters 35 | { 36 | ValidateIssuer = true, 37 | ValidateAudience = true, 38 | ValidateIssuerSigningKey = true, 39 | ValidateLifetime = true 40 | }; 41 | jwtOptions.Events = new JwtBearerEvents 42 | { 43 | OnTokenValidated = context => 44 | { 45 | // Validate the authorized party (the app who issued the token) 46 | string? clientappId = context?.Principal?.Claims.FirstOrDefault(x => x.Type == "azp" && x.Value == "99045fe1-7639-4a75-9d4a-577b6ca3810f")?.Value; 47 | if (clientappId == null) 48 | { 49 | context!.Fail("Invalid azp claim value"); 50 | } 51 | return Task.CompletedTask; 52 | } 53 | }; 54 | }); 55 | 56 | 57 | ConfigurationSection entraExternalIdUserToken = (ConfigurationSection)builder.Configuration.GetSection("EntraExternalIdUserToken"); 58 | builder.Services.AddAuthentication() 59 | .AddJwtBearer("EntraExternalIdUserToken", jwtOptions => 60 | { 61 | jwtOptions.MetadataAddress = entraExternalIdUserToken["MetadataAddress"]!; 62 | jwtOptions.Audience = entraExternalIdUserToken["Audience"]; 63 | jwtOptions.IncludeErrorDetails = true; 64 | jwtOptions.MapInboundClaims = false; 65 | jwtOptions.TokenValidationParameters = new TokenValidationParameters 66 | { 67 | ValidateIssuer = true, 68 | ValidateAudience = true, 69 | ValidateIssuerSigningKey = true, 70 | ValidateLifetime = true 71 | }; 72 | }); 73 | 74 | // Add in memory cache 75 | builder.Services.AddMemoryCache(); 76 | 77 | builder.Services.AddControllers(); 78 | 79 | // The following line enables Application Insights telemetry collection. 80 | builder.Services.AddApplicationInsightsTelemetry(); 81 | 82 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 83 | builder.Services.AddEndpointsApiExplorer(); 84 | builder.Services.AddSwaggerGen(options => 85 | { 86 | options.SwaggerDoc("v1", new OpenApiInfo 87 | { 88 | Version = "v1", 89 | Title = "Woodgrove custom authentication extension API", 90 | Description = "This dotnet Web API endpoint demonstrate how to use Microsoft Entra External ID's custom authentication extension for various events. Checkout the [source code](https://github.com/microsoft/woodgrove-api) and [request samples](./help.html)

Assembly version " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(), 91 | }); 92 | }); 93 | 94 | 95 | var app = builder.Build(); 96 | 97 | // Configure the HTTP request pipeline. 98 | //if (app.Environment.IsDevelopment()) 99 | //{ 100 | app.UseSwagger(); 101 | app.UseSwaggerUI(options => 102 | { 103 | options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); 104 | options.RoutePrefix = string.Empty; 105 | }); 106 | //} 107 | 108 | app.UseHttpsRedirection(); 109 | app.UseStaticFiles(); 110 | 111 | app.UseAuthentication(); 112 | app.UseAuthorization(); 113 | 114 | app.MapControllers(); 115 | 116 | app.Run(); 117 | -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:10192", 8 | "sslPort": 44366 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5097", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7086;http://localhost:5097", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Woodgrove groceries demo of the claims augmentation REST API 2 | 3 | This dotnet C# Web API demonstrates how to use Microsoft Entra External ID's custom authentication extension for various events. 4 | 5 | ## Endpoints 6 | 7 | The sample code provides an implementation of the following endpoints: 8 | 9 | ### Token issuance start 10 | 11 | The *TokenIssuanceStart* event is triggered when a token is about to be issued by Microsoft Entra External ID to your application. When the event is triggered your custom extension REST API is called to fetch attributes from external systems. In this demo, the [TokenIssuanceStartController](./Controllers/TokenIssuanceStartController.cs) returns the following claims: 12 | 13 | - **CorrelationId** the correlation ID that was sent by the issuer to your REST API. 14 | - **ApiVersion** a fixed value with your REST API version. This attribute can help you debug your REST API and check if your latest version is in used. 15 | - **LoyaltyNumber** a random numeric value that represents an imaginary loyally number. 16 | - **LoyaltySince** a random date that the that represents an imaginary time the user joined the loyalty program. 17 | - **LoyaltyTier** a random string that the that represents an imaginary loyalty program tier. 18 | 19 | The REST API endpoint URL: 20 | 21 | ```http 22 | POST https://auth-api.woodgrovedemo.com/OnTokenIssuanceStart 23 | ``` 24 | 25 | ### On attribute collection start 26 | 27 | The *OnAttributeCollectionStart* is fired at the beginning of the attribute collection process and can be used to prevent the user from signing up (such as based on the domain they are authenticating from) or modify the initial attributes to be collected (such as including additional attributes to collect based on the user’s identity provider). 28 | 29 | ### On attribute collection submit 30 | 31 | OnAttributeCollectionSubmit event is fired after the user provides attribute information during signing up and can be used to validate the information provided by the user (such as an invitation code or partner number), modify the collected attributes (such as address validation), and either allow the user to continue in the journey or show a validation or block page. 32 | 33 | This demo validates the city name, against a list of cities and countries we compiled. You can find the list of countries and cities in the [OnAttributeCollectionSubmitController](./Controllers/OnAttributeCollectionSubmitController.cs). 34 | 35 | The REST API endpoint URL: 36 | 37 | ```http 38 | POST https://auth-api.woodgrovedemo.com/OnAttributeCollectionSubmit 39 | ``` 40 | 41 | ## Protect access to your REST API 42 | 43 | To ensure the communications between Microsoft Entra custom extension and your REST API are secured appropriately, Microsoft Entra External ID uses OAuth 2.0 client credentials grant flow to issue an access token for the resource application registered with your custom authentication extension. 44 | 45 | When the custom extension calls your REST API, it sends an HTTP Authorization header with a bearer token issued by Microsoft Entra ID. You REST API validate the access token and its claims values. 46 | 47 | ### [Option 1] Validate the access token in your code 48 | 49 | This example uses the [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer) library to validate the access token. 50 | 51 | This demo REST API can be used without authentication (see option 2 below). If you run your own REST API, uncomment the `[Authorize]` attribute in the controllers. The following example shows how a controller should look like: 52 | 53 | ```csharp 54 | [Authorize] 55 | [ApiController] 56 | [Route("[controller]")] 57 | public class TokenIssuanceStartController : ControllerBase 58 | { 59 | // Rest of your code 60 | } 61 | ``` 62 | 63 | ### [Option 2] Validate the access token via Azure Service App 64 | 65 | [Azure App Service](https://learn.microsoft.com/azure/app-service/) enables you to build and host web apps and and RESTful APIs in the programming language of your choice without managing the infrastructure. 66 | 67 | Azure App Service provides built-in [authentication and authorization capabilities](https://learn.microsoft.com/azure/app-service/overview-authentication-authorization) (sometimes referred to as "Easy Auth"), so you can validate the access token sends by Microsoft Entra External ID by writing minimal code in RESTful API. 68 | 69 | To enable authentication into your App Service app, follow these steps: 70 | 71 | 1. Sign in to the [Azure portal](https://portal.azure.com/) and navigate to the app service hosting your web API. 72 | 1. From the left navigation, select **Authentication** > **Add identity provider** > **Microsoft**. 73 | 1. For **App registration type**, choose **Provide the details of an existing app registration** 74 | 1. Fill in the following configuration details: 75 | 76 | |Field|Description| 77 | |-|-| 78 | |Application (client) ID| The application ID that is associated with your custom extension. You can find this application under the API authentication in your custom extension. | 79 | |Client Secret| Enter any value, such as 12345. | 80 | |Issuer Url| Use `https://login.microsoftonline.com//v2.0`, and replace the *\* with the **Directory (tenant) ID** in which the app registration was created. | 81 | |Allowed Token Audiences| Use the same value as the *Application (client) ID*. | 82 | 83 | 1. For the **Restrict access**, select **Require authentication**. 84 | 1. For the **Unauthenticated requests**, select **HTTP 401 Unauthorized: recommended for APIs**. 85 | 1. Unselect the **Token store** option. 86 | 1. Select **Add**. 87 | 88 | You're now ready to use the Microsoft identity platform for authentication in your app. The App Service makes the claims in the incoming token available to your code by injecting them into the `X-MS-CLIENT-PRINCIPAL` request header (Base64 encoded JSON representation of available claims). 89 | 90 | To ensure the communications between the custom extension and your REST API are [secured appropriately](https://learn.microsoft.com/azure/active-directory/develop/custom-extension-overview#protect-your-rest-api), validate that the respective `azp` claim equals the `99045fe1-7639-4a75-9d4a-577b6ca3810f` value. 91 | 92 | In your REST API use the code in the [AzureAppServiceClaims class](./Models/AzureAppServiceClaims.cs). Then, in the controller call the `Authorize` function that checks the `azp` claim value. 93 | 94 | ```csharp 95 | if (!AzureAppServiceClaimsHeader.Authorize(this.Request)) 96 | { 97 | Response.StatusCode = (int)HttpStatusCode.Unauthorized; 98 | return null; 99 | } 100 | ``` 101 | 102 | ### [Option 3] Validate the access token via Azure APIM 103 | 104 | [Azure API Management](https://learn.microsoft.com/azure/api-management/api-management-key-concepts) offers a scalable, multi-cloud API management platform for securing, publishing, and analyzing APIs. The [validate-azure-ad-token](https://learn.microsoft.com/azure/api-management/validate-azure-ad-token-policy) policy enforces the existence and validity of a JSON web token (JWT) that was provided by the Microsoft Entra External ID. 105 | 106 | The following example policy, when added to the `` policy section, checks the value of the `audience` and the `azp` claims in an access token obtained from Microsoft Entra External ID that is presented in the `Authorization` header. It returns an error message if the token is not valid. Configure this policy at a policy scope that it protects all custom authentication extensions REST API endpoints. 107 | 108 | ```xml 109 | 110 | 111 | 99045fe1-7639-4a75-9d4a-577b6ca3810f 112 | 113 | 114 | Your application ID 115 | 116 | 117 | ``` 118 | 119 | Use the following values: 120 | 121 | - **tenant-id** your Microsoft Entra External ID tenant ID. 122 | - **Audience** the application ID that is associated with your custom extension. You can find this application under the API authentication in your custom extension. 123 | 124 | 125 | ## Data models 126 | 127 | The code sample has the following data models: 128 | 129 | - TokenIssuanceStart event [request](./Models/TokenIssuanceStartRequest.cs) and [response](./Models/TokenIssuanceStartResponse.cs) 130 | - OnAttributeCollectionSubmit event [request](./Models/OnAttributeCollectionSubmitRequest.cs) and [response](./Models/OnAttributeCollectionSubmitResponse.cs) 131 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. 7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "EntraExternalIdCustomAuthToken": { 3 | "MetadataAddress" : "https://tenant-name.ciamlogin.com/00000000-0000-0000-00000000000000000/v2.0/.well-known/openid-configuration", 4 | "Audience": "11111111-0000-0000-00000000000000000" 5 | }, 6 | "EntraExternalIdUserToken": { 7 | "MetadataAddress" : "https://tenant-name.ciamlogin.com/00000000-0000-0000-00000000000000000/v2.0/.well-known/openid-configuration", 8 | "Audience": "22222222-0000-0000-00000000000000000" 9 | }, 10 | "ApplicationInsights": { 11 | "ConnectionString": "" 12 | }, 13 | "AppSettings": { 14 | "EmailConnectionString": "", 15 | "CloudflareSecret": "" 16 | }, 17 | "Demos": { 18 | "SimulateDelayMilliseconds": 0 19 | }, 20 | "Logging": { 21 | "LogLevel": { 22 | "Default": "Information", 23 | "Microsoft.AspNetCore": "Warning", 24 | "Microsoft.Identity": "Warning", 25 | "woodgroveapi.Controllers": "Warning" 26 | } 27 | }, 28 | "AllowedHosts": "*" 29 | } 30 | -------------------------------------------------------------------------------- /woodgroveapi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 1.0.0.15 8 | CS8618 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /woodgroveapi.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33627.172 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "woodgroveapi", "woodgroveapi.csproj", "{7491B8B9-D7DD-4C51-BF2F-3C1386A1DE28}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {7491B8B9-D7DD-4C51-BF2F-3C1386A1DE28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {7491B8B9-D7DD-4C51-BF2F-3C1386A1DE28}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {7491B8B9-D7DD-4C51-BF2F-3C1386A1DE28}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {7491B8B9-D7DD-4C51-BF2F-3C1386A1DE28}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {9EEA37C0-90B0-4D26-8EB0-6C630415241A} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /wwwroot/Help/TokenIssuanceStart_LocalAccount.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "microsoft.graph.authenticationEvent.tokenIssuanceStart", 3 | "source": "/tenants/12345678-0000-0000-0000-000000000000/applications/44444444-0000-0000-0000-000000000000", 4 | "data": { 5 | "@odata.type": "microsoft.graph.onTokenIssuanceStartCalloutData", 6 | "tenantId": "12345678-0000-0000-0000-000000000000", 7 | "authenticationEventListenerId": "55555555-3333-0000-0000-000000000000", 8 | "customAuthenticationExtensionId": "66666666-3333-0000-0000-000000000000", 9 | "authenticationContext": { 10 | "correlationId": "0c83d76c-b8da-45bb-ac7a-f9bb5e0db340", 11 | "client": { 12 | "ip": "2a0d:0000:0000:0000:0000:0000:e02d:78d3", 13 | "locale": "en-us", 14 | "market": "en-us" 15 | }, 16 | "protocol": "OAUTH2.0", 17 | "clientServicePrincipal": { 18 | "id": "33333333-0000-0000-0000-000000000000", 19 | "appId": "44444444-0000-0000-0000-000000000000", 20 | "appDisplayName": "My Test application", 21 | "displayName": "My Test application" 22 | }, 23 | "resourceServicePrincipal": { 24 | "id": "33333333-0000-0000-0000-000000000000", 25 | "appId": "44444444-0000-0000-0000-000000000000", 26 | "appDisplayName": "My Test application", 27 | "displayName": "My Test application" 28 | }, 29 | "user": { 30 | "displayName": "Emily", 31 | "id": "7f122226-0000-0000-0000-000000000000", 32 | "userPrincipalName": "7f122226-0000-0000-0000-000000000000@woodgrovedemo.onmicrosoft.com", 33 | "userType": "Member" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /wwwroot/Help/TokenIssuanceStart_SocailAccount.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "microsoft.graph.authenticationEvent.tokenIssuanceStart", 3 | "source": "/tenants/12345678-0000-0000-0000-000000000000/applications/44444444-0000-0000-0000-000000000000", 4 | "data": { 5 | "@odata.type": "microsoft.graph.onTokenIssuanceStartCalloutData", 6 | "tenantId": "12345678-0000-0000-0000-000000000000", 7 | "authenticationEventListenerId": "55555555-3333-0000-0000-000000000000", 8 | "customAuthenticationExtensionId": "66666666-3333-0000-0000-000000000000", 9 | "authenticationContext": { 10 | "correlationId": "035548e6-d6aa-46bc-8f1e-bd3f384b094a", 11 | "client": { 12 | "ip": "2a0d:0000:0000:0000:0000:0000:e02d:78d3", 13 | "locale": "en-us", 14 | "market": "en-us" 15 | }, 16 | "protocol": "OAUTH2.0", 17 | "clientServicePrincipal": { 18 | "id": "33333333-0000-0000-0000-000000000000", 19 | "appId": "44444444-0000-0000-0000-000000000000", 20 | "appDisplayName": "My Test application", 21 | "displayName": "My Test application" 22 | }, 23 | "resourceServicePrincipal": { 24 | "id": "33333333-0000-0000-0000-000000000000", 25 | "appId": "44444444-0000-0000-0000-000000000000", 26 | "appDisplayName": "My Test application", 27 | "displayName": "My Test application" 28 | }, 29 | "user": { 30 | "displayName": "Someone", 31 | "id": "92476d48-0000-0000-0000-000000000000", 32 | "mail": "someone@gmail.com", 33 | "userType": "Member" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /wwwroot/Help/attributeCollectionStart-LocalAccount.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "microsoft.graph.authenticationEvent.attributeCollectionStart", 3 | "source": "/tenants/12345678-0000-0000-0000-000000000000/applications/00000003-0000-0000-0000-000000000000", 4 | "data": { 5 | "@odata.type": "microsoft.graph.onAttributeCollectionStartCalloutData", 6 | "userSignUpInfo": { 7 | "attributes": { 8 | "EmailAddress": { 9 | "value": "someone@contoso.com", 10 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 11 | "attributeType": "builtIn" 12 | } 13 | }, 14 | "identities": [ 15 | { 16 | "signInType": "federated", 17 | "issuer": "mail", 18 | "issuerAssignedId": "someone@contoso.com" 19 | } 20 | ] 21 | }, 22 | "tenantId": "12345678-0000-0000-0000-000000000000", 23 | "authenticationEventListenerId": "55555555-1111-0000-0000-000000000000", 24 | "customAuthenticationExtensionId": "66666666-1111-0000-0000-000000000000", 25 | "authenticationContext": { 26 | "correlationId": "6b264006-9d46-4409-bdb7-a501b6c22527", 27 | "client": { 28 | "ip": "2a0d:0000:0000:0000:0000:0000:e02d:78d3", 29 | "locale": "en-us", 30 | "market": "en-us" 31 | }, 32 | "protocol": "OAUTH2.0", 33 | "clientServicePrincipal": { 34 | "id": "33333333-0000-0000-0000-000000000000", 35 | "appId": "44444444-0000-0000-0000-000000000000", 36 | "appDisplayName": "My Test application", 37 | "displayName": "My Test application" 38 | }, 39 | "resourceServicePrincipal": { 40 | "id": "e42d06a3-38f6-40ab-86a8-3019e1468863", 41 | "appId": "00000003-0000-0000-0000-000000000000", 42 | "appDisplayName": "Microsoft Graph", 43 | "displayName": "Microsoft Graph" 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /wwwroot/Help/attributeCollectionStart-SocialAccount.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "microsoft.graph.authenticationEvent.attributeCollectionStart", 3 | "source": "/tenants/12345678-0000-0000-0000-000000000000/applications/00000003-0000-0000-0000-000000000000", 4 | "data": { 5 | "@odata.type": "microsoft.graph.onAttributeCollectionStartCalloutData", 6 | "userSignUpInfo": { 7 | "attributes": { 8 | "EmailAddress": { 9 | "value": "someone@gmail.com", 10 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 11 | "attributeType": "builtIn" 12 | } 13 | }, 14 | "identities": [ 15 | { 16 | "signInType": "federated", 17 | "issuer": "google.com", 18 | "issuerAssignedId": "1234567890" 19 | } 20 | ] 21 | }, 22 | "tenantId": "12345678-0000-0000-0000-000000000000", 23 | "authenticationEventListenerId": "55555555-1111-0000-0000-000000000000", 24 | "customAuthenticationExtensionId": "66666666-1111-0000-0000-000000000000", 25 | "authenticationContext": { 26 | "correlationId": "220b4804-64ce-4e90-b9d2-03c3fb85a11c", 27 | "client": { 28 | "ip": "2a0d:0000:0000:0000:0000:0000:e02d:78d3", 29 | "locale": "en-us", 30 | "market": "en-us" 31 | }, 32 | "protocol": "OAUTH2.0", 33 | "clientServicePrincipal": { 34 | "id": "33333333-0000-0000-0000-000000000000", 35 | "appId": "44444444-0000-0000-0000-000000000000", 36 | "appDisplayName": "My Test application", 37 | "displayName": "My Test application" 38 | }, 39 | "resourceServicePrincipal": { 40 | "id": "e42d06a3-38f6-40ab-86a8-3019e1468863", 41 | "appId": "00000003-0000-0000-0000-000000000000", 42 | "appDisplayName": "Microsoft Graph", 43 | "displayName": "Microsoft Graph" 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /wwwroot/Help/attributeCollectionSubmit-LocalAccount.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "microsoft.graph.authenticationEvent.attributeCollectionSubmit", 3 | "source": "/tenants/12345678-0000-0000-0000-000000000000/applications/00000003-0000-0000-0000-000000000000", 4 | "data": { 5 | "@odata.type": "microsoft.graph.onAttributeCollectionSubmitCalloutData", 6 | "userSignUpInfo": { 7 | "attributes": { 8 | "email": { 9 | "value": "someone@contoso.com", 10 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 11 | "attributeType": "builtIn" 12 | }, 13 | "city": { 14 | "value": "Sydney", 15 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 16 | "attributeType": "builtIn" 17 | }, 18 | "country": { 19 | "value": "au", 20 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 21 | "attributeType": "builtIn" 22 | }, 23 | "displayName": { 24 | "value": "Emily", 25 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 26 | "attributeType": "builtIn" 27 | }, 28 | "extension_9ce7f42908d14395aed7c48e9b6b957f_SpecialDiet": { 29 | "value": "Eggs", 30 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 31 | "attributeType": "directorySchemaExtension" 32 | } 33 | } 34 | }, 35 | "tenantId": "12345678-0000-0000-0000-000000000000", 36 | "authenticationEventListenerId": "55555555-2222-0000-0000-000000000000", 37 | "customAuthenticationExtensionId": "66666666-2222-0000-0000-000000000000", 38 | "authenticationContext": { 39 | "correlationId": "859e2a76-b9ea-41fd-82c8-f8815a2b5123", 40 | "client": { 41 | "ip": "2a0d:0000:0000:0000:0000:0000:e02d:78d3", 42 | "locale": "en-us", 43 | "market": "en-us" 44 | }, 45 | "protocol": "OAUTH2.0", 46 | "clientServicePrincipal": { 47 | "id": "33333333-0000-0000-0000-000000000000", 48 | "appId": "44444444-0000-0000-0000-000000000000", 49 | "appDisplayName": "My Test application", 50 | "displayName": "My Test application" 51 | }, 52 | "resourceServicePrincipal": { 53 | "id": "e42d06a3-38f6-40ab-86a8-3019e1468863", 54 | "appId": "00000003-0000-0000-0000-000000000000", 55 | "appDisplayName": "Microsoft Graph", 56 | "displayName": "Microsoft Graph" 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /wwwroot/Help/attributeCollectionSubmit-SocialAccount.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "microsoft.graph.authenticationEvent.attributeCollectionSubmit", 3 | "source": "/tenants/12345678-0000-0000-0000-000000000000/applications/00000003-0000-0000-0000-000000000000", 4 | "data": { 5 | "@odata.type": "microsoft.graph.onAttributeCollectionSubmitCalloutData", 6 | "userSignUpInfo": { 7 | "attributes": { 8 | "email": { 9 | "value": "someone@gmail.com", 10 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 11 | "attributeType": "builtIn" 12 | }, 13 | "city": { 14 | "value": "Sydney", 15 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 16 | "attributeType": "builtIn" 17 | }, 18 | "country": { 19 | "value": "au", 20 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 21 | "attributeType": "builtIn" 22 | }, 23 | "displayName": { 24 | "value": "Emily", 25 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 26 | "attributeType": "builtIn" 27 | }, 28 | "extension_9ce7f42908d14395aed7c48e9b6b957f_SpecialDiet": { 29 | "value": "Eggs", 30 | "@odata.type": "microsoft.graph.stringDirectoryAttributeValue", 31 | "attributeType": "directorySchemaExtension" 32 | } 33 | } 34 | }, 35 | "tenantId": "12345678-0000-0000-0000-000000000000", 36 | "authenticationEventListenerId": "55555555-2222-0000-0000-000000000000", 37 | "customAuthenticationExtensionId": "66666666-2222-0000-0000-000000000000", 38 | "authenticationContext": { 39 | "correlationId": "61154773-72eb-497e-a826-11250395836a", 40 | "client": { 41 | "ip": "2a0d:0000:0000:0000:0000:0000:e02d:78d3", 42 | "locale": "en-us", 43 | "market": "en-us" 44 | }, 45 | "protocol": "OAUTH2.0", 46 | "clientServicePrincipal": { 47 | "id": "33333333-0000-0000-0000-000000000000", 48 | "appId": "44444444-0000-0000-0000-000000000000", 49 | "appDisplayName": "My Test application", 50 | "displayName": "My Test application" 51 | }, 52 | "resourceServicePrincipal": { 53 | "id": "e42d06a3-38f6-40ab-86a8-3019e1468863", 54 | "appId": "00000003-0000-0000-0000-000000000000", 55 | "appDisplayName": "Microsoft Graph", 56 | "displayName": "Microsoft Graph" 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /wwwroot/Help/onOtpSend.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "microsoft.graph.authenticationEvent.emailOtpSend", 3 | "source": "/tenants/12345678-0000-0000-0000-000000000000/applications/44444444-0000-0000-0000-000000000000", 4 | "data": { 5 | "@odata.type": "microsoft.graph.onOtpSendCalloutData", 6 | "otpContext": { 7 | "identifier": "casey@contoso.com", 8 | "oneTimeCode": "17479132" 9 | }, 10 | "tenantId": "12345678-0000-0000-0000-000000000000", 11 | "authenticationEventListenerId": "55555555-4444-0000-0000-000000000000", 12 | "customAuthenticationExtensionId": "66666666-4444-0000-0000-000000000000", 13 | "authenticationContext": { 14 | "correlationId": "d486c3ad-efc6-4f38-9dbd-3f3023158d8d", 15 | "client": { 16 | "ip": "0.0.0.0", 17 | "locale": "en-us", 18 | "market": "en-us" 19 | }, 20 | "protocol": "OAUTH2.0", 21 | "requestType": "signUp", 22 | "clientServicePrincipal": { 23 | "id": "33333333-0000-0000-0000-000000000000", 24 | "appId": "44444444-0000-0000-0000-000000000000", 25 | "appDisplayName": "My Test application", 26 | "displayName": "My Test application" 27 | }, 28 | "resourceServicePrincipal": { 29 | "id": "33333333-0000-0000-0000-000000000000", 30 | "appId": "44444444-0000-0000-0000-000000000000", 31 | "appDisplayName": "My Test application", 32 | "displayName": "My Test application" 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /wwwroot/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Woodgrove API demo help 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

Woodgrove API demo help

17 |

Configure pre- and post- attribute collection, and on token issuance start event custom extensions

18 |
19 | 20 |
21 |
22 |
23 |

Token issuance start

24 |

The token issuance start event is triggered once a user completes all of their authentication challenges, and a security token is about to be issued. You can use a custom authentication extension and a custom claims provider to add this external data into tokens returned to your application.

25 | 29 |
30 |
31 |

Attribute collection start event

32 |

The attributeCollectionStart is fired at the beginning of the attribute collection process and can be used to prevent the user from signing up (such as based on the domain they are authenticating from) or modify the initial attributes to be collected (such as including additional attributes to collect based on the user’s IdP).

33 | 37 |
38 |
39 |

Attribute collection submit event

40 |

The OnAttributeCollectionSubmit is fired after the user provides attribute information during signing up and can be used to validate the information provided by the user (such as an invitation code or partner number), modify the collected attributes (such as address validation), and either allow the user to continue in the journey or show a validation or block page.

41 | 45 |
46 |
47 |
48 | 49 | 50 | 51 | --------------------------------------------------------------------------------