├── .EditorConfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── Blazor.WebAuthentication.sln ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── algorithms.csv ├── generateCoseAlgorithms.ps1 ├── generateCoseAlgorithms.sh ├── samples ├── KristofferStrube.Blazor.WebAuthentication.API │ ├── KristofferStrube.Blazor.WebAuthentication.API.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── WebAuthenticationAPI.cs │ ├── appsettings.Development.json │ └── appsettings.json └── KristofferStrube.Blazor.WebAuthentication.WasmExample │ ├── App.razor │ ├── KristofferStrube.Blazor.WebAuthentication.WasmExample.csproj │ ├── Pages │ ├── Index.razor │ └── Index.razor.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Shared │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ ├── NavMenu.razor │ └── NavMenu.razor.css │ ├── WebAuthenticationClient.cs │ ├── _Imports.razor │ └── wwwroot │ ├── 404.html │ ├── css │ ├── app.css │ ├── bootstrap-icons │ │ ├── bootstrap-icons.css │ │ ├── bootstrap-icons.json │ │ ├── bootstrap-icons.min.css │ │ ├── bootstrap-icons.scss │ │ └── fonts │ │ │ ├── bootstrap-icons.woff │ │ │ └── bootstrap-icons.woff2 │ └── bootstrap │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ ├── favicon.png │ ├── icon-192.png │ └── index.html ├── src ├── KristofferStrube.Blazor.CredentialManagement │ ├── BaseJSWrapper.cs │ ├── Converters │ │ ├── CredentialMediationRequirementConverter.cs │ │ └── IJSWrapperConverter.cs │ ├── Credential.cs │ ├── CredentialsContainer.cs │ ├── CredentialsService.cs │ ├── Extensions │ │ ├── IJSRuntimeExtensions.cs │ │ └── IServiceCollectionExtensions.cs │ ├── KristofferStrube.Blazor.CredentialManagement.csproj │ ├── Options │ │ ├── CredentialCreationOptions.cs │ │ ├── CredentialMediationRequirement.cs │ │ └── CredentialRequestOptions.cs │ └── wwwroot │ │ └── KristofferStrube.Blazor.CredentialManagement.js └── KristofferStrube.Blazor.WebAuthentication │ ├── AttestationStatements │ ├── AndroidSafetyNetAttestationStatement.cs │ ├── AttestationStatement.cs │ ├── NoneAttestationStatement.cs │ ├── PackedAttestationStatement.cs │ └── TPMAttestationStatement.cs │ ├── AuthenticatorAssertionResponse.cs │ ├── AuthenticatorAttestationResponse.cs │ ├── AuthenticatorResponse.cs │ ├── COSEAlgorithms.cs │ ├── CollectedClientData.cs │ ├── Converters │ ├── AttestationConveyancePreferenceConverter.cs │ ├── AttestationFormatConverter.cs │ ├── AuthenticatorTransportConverter.cs │ ├── PublicKeyCredentialTypeConverter.cs │ └── UserVerificationRequirementConverter.cs │ ├── Extensions │ └── IJSRuntimeExtensions.cs │ ├── JSONRepresentations │ ├── AuthenticationExtensionsClientOutputsJSON.cs │ ├── AuthenticationResponseJSON.cs │ ├── AuthenticatorAssertionResponseJSON.cs │ ├── AuthenticatorAttestationResponseJSON.cs │ ├── PublicKeyCredentialJSON.cs │ └── RegistrationResponseJSON.cs │ ├── KristofferStrube.Blazor.WebAuthentication.csproj │ ├── Options │ ├── AttestationConveyancePreference.cs │ ├── AttestationFormat.cs │ ├── AuthenticatorTransport.cs │ ├── CredentialCreationOptions.cs │ ├── CredentialRequestOptions.cs │ ├── PublicKeyCredentialCreationOptions.cs │ ├── PublicKeyCredentialDescriptor.cs │ ├── PublicKeyCredentialEntity.cs │ ├── PublicKeyCredentialParameters.cs │ ├── PublicKeyCredentialRequestOptions.cs │ ├── PublicKeyCredentialRpEntity.cs │ ├── PublicKeyCredentialType.cs │ ├── PublicKeyCredentialUserEntity.cs │ └── UserVerificationRequirement.cs │ ├── PublicKeyCredential.cs │ └── wwwroot │ └── KristofferStrube.Blazor.WebAuthentication.js ├── tests └── KristofferStrube.Blazor.WebAuthentication.Tests │ ├── AttestationStatementTests.cs │ └── KristofferStrube.Blazor.WebAuthentication.Tests.csproj └── tools └── KristofferStrube.Blazor.COSEGenerator ├── KristofferStrube.Blazor.COSEGenerator.csproj └── Program.cs /.EditorConfig: -------------------------------------------------------------------------------- 1 | [*] 2 | # All files 3 | dotnet_style_qualification_for_field = false 4 | dotnet_style_qualification_for_property = false 5 | dotnet_style_qualification_for_method = false 6 | dotnet_style_qualification_for_event = false 7 | dotnet_diagnostic.IDE0003.severity = warning 8 | dotnet_style_predefined_type_for_locals_parameters_members = true 9 | dotnet_style_predefined_type_for_member_access = true 10 | dotnet_diagnostic.IDE0049.severity = suggestion 11 | csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async 12 | dotnet_diagnostic.IDE0036.severity = error 13 | dotnet_style_require_accessibility_modifiers = always 14 | dotnet_diagnostic.IDE0040.severity = warning 15 | dotnet_style_readonly_field = true 16 | dotnet_diagnostic.IDE0044.severity = error 17 | csharp_prefer_static_local_function = true 18 | dotnet_diagnostic.IDE0062.severity = warning 19 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity 20 | dotnet_diagnostic.IDE0047.severity = warning 21 | dotnet_diagnostic.IDE0048.severity = warning 22 | dotnet_diagnostic.IDE0010.severity = error 23 | dotnet_style_object_initializer = true 24 | dotnet_diagnostic.IDE0017.severity = suggestion 25 | csharp_style_inlined_variable_declaration = true 26 | dotnet_diagnostic.IDE0018.severity = suggestion 27 | dotnet_style_collection_initializer = true 28 | dotnet_diagnostic.IDE0028.severity = warning 29 | dotnet_style_prefer_auto_properties = true 30 | dotnet_diagnostic.IDE0032.severity = suggestion 31 | dotnet_style_explicit_tuple_names = true 32 | dotnet_diagnostic.IDE0033.severity = warning 33 | csharp_prefer_simple_default_expression = false 34 | dotnet_diagnostic.IDE0034.severity = warning 35 | dotnet_style_prefer_inferred_tuple_names = true 36 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 37 | dotnet_diagnostic.IDE0037.severity = suggestion 38 | csharp_style_prefer_local_over_anonymous_function = true 39 | dotnet_diagnostic.IDE0039.severity = warning 40 | csharp_style_deconstructed_variable_declaration = true 41 | dotnet_diagnostic.IDE0042.severity = suggestion 42 | dotnet_style_prefer_conditional_expression_over_assignment = true 43 | dotnet_diagnostic.IDE0045.severity = warning 44 | dotnet_style_prefer_conditional_expression_over_return = true 45 | dotnet_diagnostic.IDE0046.severity = warning 46 | dotnet_style_prefer_compound_assignment = true 47 | dotnet_diagnostic.IDE0054.severity = warning 48 | dotnet_diagnostic.IDE0074.severity = warning 49 | csharp_style_prefer_index_operator = true 50 | dotnet_diagnostic.IDE0056.severity = warning 51 | csharp_style_prefer_range_operator = true 52 | dotnet_diagnostic.IDE0057.severity = warning 53 | dotnet_diagnostic.IDE0070.severity = error 54 | dotnet_style_prefer_simplified_interpolation = true 55 | dotnet_diagnostic.IDE0071.severity = warning 56 | dotnet_diagnostic.IDE0072.severity = error 57 | dotnet_style_prefer_simplified_boolean_expressions = true 58 | dotnet_diagnostic.IDE0075.severity = warning 59 | dotnet_diagnostic.IDE0082.severity = error 60 | csharp_style_implicit_object_creation_when_type_is_apparent = true 61 | dotnet_diagnostic.IDE0090.severity = error 62 | dotnet_diagnostic.IDE0180.severity = warning 63 | csharp_style_namespace_declarations = file_scoped 64 | dotnet_diagnostic.IDE0160.severity = error 65 | dotnet_diagnostic.IDE0161.severity = error 66 | csharp_style_throw_expression = true 67 | dotnet_diagnostic.IDE0016.severity = warning 68 | dotnet_style_coalesce_expression = true 69 | dotnet_diagnostic.IDE0029.severity = warning 70 | dotnet_diagnostic.IDE0030.severity = warning 71 | dotnet_style_null_propagation = true 72 | dotnet_diagnostic.IDE0031.severity = warning 73 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true 74 | dotnet_diagnostic.IDE0041.severity = warning 75 | csharp_style_prefer_null_check_over_type_check = true 76 | dotnet_diagnostic.IDE0150.severity = warning 77 | csharp_style_conditional_delegate_call = false 78 | dotnet_diagnostic.IDE1005.severity = warning 79 | csharp_style_var_for_built_in_types = false 80 | csharp_style_var_when_type_is_apparent = true 81 | csharp_style_var_elsewhere = false 82 | dotnet_diagnostic.IDE0007.severity = warning 83 | dotnet_diagnostic.IDE0008.severity = warning 84 | dotnet_diagnostic.IDE0001.severity = error 85 | dotnet_diagnostic.IDE0002.severity = error 86 | dotnet_diagnostic.IDE0004.severity = error 87 | dotnet_diagnostic.IDE0005.severity = error 88 | dotnet_diagnostic.IDE0035.severity = warning 89 | dotnet_diagnostic.IDE0051.severity = warning 90 | dotnet_diagnostic.IDE0052.severity = warning 91 | csharp_style_unused_value_expression_statement_preference = discard_variable 92 | dotnet_diagnostic.IDE0058.severity = warning 93 | csharp_style_unused_value_assignment_preference = discard_variable 94 | dotnet_diagnostic.IDE0059.severity = warning -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish application' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**/README.md' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | # Checkout the code 15 | - uses: actions/checkout@v2 16 | 17 | # Install .NET 8.0 SDK 18 | - name: Setup .NET 8 preview 19 | uses: actions/setup-dotnet@v1 20 | with: 21 | dotnet-version: '8.0.x' 22 | include-prerelease: true 23 | 24 | # Settings for elmah.io 25 | - name: Replace elmah.io API_KEY 26 | uses: richardrigutins/replace-in-files@v2 27 | with: 28 | files: 'samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/Program.cs' 29 | search-text: '' 30 | replacement-text: '${{ secrets.ELMAH_IO_API_KEY }}' 31 | 32 | - name: Replace elmah.io LOG_ID 33 | uses: richardrigutins/replace-in-files@v2 34 | with: 35 | files: 'samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/Program.cs' 36 | search-text: '' 37 | replacement-text: '${{ secrets.ELMAH_IO_LOG_ID }}' 38 | 39 | # Generate the website 40 | - name: Publish 41 | run: dotnet publish samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/KristofferStrube.Blazor.WebAuthentication.WasmExample.csproj --configuration Release /p:EnvironmentName=Production --output build 42 | 43 | # Publish the website 44 | - name: GitHub Pages action 45 | if: ${{ github.ref == 'refs/heads/main' }} # Publish only when the push is on main 46 | uses: peaceiris/actions-gh-pages@v3.6.1 47 | with: 48 | github_token: ${{ secrets.PUBLISH_TOKEN }} 49 | publish_branch: gh-pages 50 | publish_dir: build/wwwroot 51 | allow_empty_commit: false 52 | keep_files: false 53 | force_orphan: true 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from `dotnet new gitignore` 5 | 6 | # dotenv files 7 | .env 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # Tye 69 | .tye/ 70 | 71 | # ASP.NET Scaffolding 72 | ScaffoldingReadMe.txt 73 | 74 | # StyleCop 75 | StyleCopReport.xml 76 | 77 | # Files built by Visual Studio 78 | *_i.c 79 | *_p.c 80 | *_h.h 81 | *.ilk 82 | *.meta 83 | *.obj 84 | *.iobj 85 | *.pch 86 | *.pdb 87 | *.ipdb 88 | *.pgc 89 | *.pgd 90 | *.rsp 91 | *.sbr 92 | *.tlb 93 | *.tli 94 | *.tlh 95 | *.tmp 96 | *.tmp_proj 97 | *_wpftmp.csproj 98 | *.log 99 | *.tlog 100 | *.vspscc 101 | *.vssscc 102 | .builds 103 | *.pidb 104 | *.svclog 105 | *.scc 106 | 107 | # Chutzpah Test files 108 | _Chutzpah* 109 | 110 | # Visual C++ cache files 111 | ipch/ 112 | *.aps 113 | *.ncb 114 | *.opendb 115 | *.opensdf 116 | *.sdf 117 | *.cachefile 118 | *.VC.db 119 | *.VC.VC.opendb 120 | 121 | # Visual Studio profiler 122 | *.psess 123 | *.vsp 124 | *.vspx 125 | *.sap 126 | 127 | # Visual Studio Trace Files 128 | *.e2e 129 | 130 | # TFS 2012 Local Workspace 131 | $tf/ 132 | 133 | # Guidance Automation Toolkit 134 | *.gpState 135 | 136 | # ReSharper is a .NET coding add-in 137 | _ReSharper*/ 138 | *.[Rr]e[Ss]harper 139 | *.DotSettings.user 140 | 141 | # TeamCity is a build add-in 142 | _TeamCity* 143 | 144 | # DotCover is a Code Coverage Tool 145 | *.dotCover 146 | 147 | # AxoCover is a Code Coverage Tool 148 | .axoCover/* 149 | !.axoCover/settings.json 150 | 151 | # Coverlet is a free, cross platform Code Coverage Tool 152 | coverage*.json 153 | coverage*.xml 154 | coverage*.info 155 | 156 | # Visual Studio code coverage results 157 | *.coverage 158 | *.coveragexml 159 | 160 | # NCrunch 161 | _NCrunch_* 162 | .*crunch*.local.xml 163 | nCrunchTemp_* 164 | 165 | # MightyMoose 166 | *.mm.* 167 | AutoTest.Net/ 168 | 169 | # Web workbench (sass) 170 | .sass-cache/ 171 | 172 | # Installshield output folder 173 | [Ee]xpress/ 174 | 175 | # DocProject is a documentation generator add-in 176 | DocProject/buildhelp/ 177 | DocProject/Help/*.HxT 178 | DocProject/Help/*.HxC 179 | DocProject/Help/*.hhc 180 | DocProject/Help/*.hhk 181 | DocProject/Help/*.hhp 182 | DocProject/Help/Html2 183 | DocProject/Help/html 184 | 185 | # Click-Once directory 186 | publish/ 187 | 188 | # Publish Web Output 189 | *.[Pp]ublish.xml 190 | *.azurePubxml 191 | # Note: Comment the next line if you want to checkin your web deploy settings, 192 | # but database connection strings (with potential passwords) will be unencrypted 193 | *.pubxml 194 | *.publishproj 195 | 196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 197 | # checkin your Azure Web App publish settings, but sensitive information contained 198 | # in these scripts will be unencrypted 199 | PublishScripts/ 200 | 201 | # NuGet Packages 202 | *.nupkg 203 | # NuGet Symbol Packages 204 | *.snupkg 205 | # The packages folder can be ignored because of Package Restore 206 | **/[Pp]ackages/* 207 | # except build/, which is used as an MSBuild target. 208 | !**/[Pp]ackages/build/ 209 | # Uncomment if necessary however generally it will be regenerated when needed 210 | #!**/[Pp]ackages/repositories.config 211 | # NuGet v3's project.json files produces more ignorable files 212 | *.nuget.props 213 | *.nuget.targets 214 | 215 | # Microsoft Azure Build Output 216 | csx/ 217 | *.build.csdef 218 | 219 | # Microsoft Azure Emulator 220 | ecf/ 221 | rcf/ 222 | 223 | # Windows Store app package directories and files 224 | AppPackages/ 225 | BundleArtifacts/ 226 | Package.StoreAssociation.xml 227 | _pkginfo.txt 228 | *.appx 229 | *.appxbundle 230 | *.appxupload 231 | 232 | # Visual Studio cache files 233 | # files ending in .cache can be ignored 234 | *.[Cc]ache 235 | # but keep track of directories ending in .cache 236 | !?*.[Cc]ache/ 237 | 238 | # Others 239 | ClientBin/ 240 | ~$* 241 | *~ 242 | *.dbmdl 243 | *.dbproj.schemaview 244 | *.jfm 245 | *.pfx 246 | *.publishsettings 247 | orleans.codegen.cs 248 | 249 | # Including strong name files can present a security risk 250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 251 | #*.snk 252 | 253 | # Since there are multiple workflows, uncomment next line to ignore bower_components 254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 255 | #bower_components/ 256 | 257 | # RIA/Silverlight projects 258 | Generated_Code/ 259 | 260 | # Backup & report files from converting an old project file 261 | # to a newer Visual Studio version. Backup files are not needed, 262 | # because we have git ;-) 263 | _UpgradeReport_Files/ 264 | Backup*/ 265 | UpgradeLog*.XML 266 | UpgradeLog*.htm 267 | ServiceFabricBackup/ 268 | *.rptproj.bak 269 | 270 | # SQL Server files 271 | *.mdf 272 | *.ldf 273 | *.ndf 274 | 275 | # Business Intelligence projects 276 | *.rdl.data 277 | *.bim.layout 278 | *.bim_*.settings 279 | *.rptproj.rsuser 280 | *- [Bb]ackup.rdl 281 | *- [Bb]ackup ([0-9]).rdl 282 | *- [Bb]ackup ([0-9][0-9]).rdl 283 | 284 | # Microsoft Fakes 285 | FakesAssemblies/ 286 | 287 | # GhostDoc plugin setting file 288 | *.GhostDoc.xml 289 | 290 | # Node.js Tools for Visual Studio 291 | .ntvs_analysis.dat 292 | node_modules/ 293 | 294 | # Visual Studio 6 build log 295 | *.plg 296 | 297 | # Visual Studio 6 workspace options file 298 | *.opt 299 | 300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 301 | *.vbw 302 | 303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 304 | *.vbp 305 | 306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 307 | *.dsw 308 | *.dsp 309 | 310 | # Visual Studio 6 technical files 311 | *.ncb 312 | *.aps 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # CodeRush personal settings 330 | .cr/personal 331 | 332 | # Python Tools for Visual Studio (PTVS) 333 | __pycache__/ 334 | *.pyc 335 | 336 | # Cake - Uncomment if you are using it 337 | # tools/** 338 | # !tools/packages.config 339 | 340 | # Tabs Studio 341 | *.tss 342 | 343 | # Telerik's JustMock configuration file 344 | *.jmconfig 345 | 346 | # BizTalk build output 347 | *.btp.cs 348 | *.btm.cs 349 | *.odx.cs 350 | *.xsd.cs 351 | 352 | # OpenCover UI analysis results 353 | OpenCover/ 354 | 355 | # Azure Stream Analytics local run output 356 | ASALocalRun/ 357 | 358 | # MSBuild Binary and Structured Log 359 | *.binlog 360 | 361 | # NVidia Nsight GPU debugger configuration file 362 | *.nvuser 363 | 364 | # MFractors (Xamarin productivity tool) working folder 365 | .mfractor/ 366 | 367 | # Local History for Visual Studio 368 | .localhistory/ 369 | 370 | # Visual Studio History (VSHistory) files 371 | .vshistory/ 372 | 373 | # BeatPulse healthcheck temp database 374 | healthchecksdb 375 | 376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 377 | MigrationBackup/ 378 | 379 | # Ionide (cross platform F# VS Code tools) working folder 380 | .ionide/ 381 | 382 | # Fody - auto-generated XML schema 383 | FodyWeavers.xsd 384 | 385 | # VS Code files for those working on multiple tools 386 | .vscode/* 387 | !.vscode/settings.json 388 | !.vscode/tasks.json 389 | !.vscode/launch.json 390 | !.vscode/extensions.json 391 | *.code-workspace 392 | 393 | # Local History for Visual Studio Code 394 | .history/ 395 | 396 | # Windows Installer files from build outputs 397 | *.cab 398 | *.msi 399 | *.msix 400 | *.msm 401 | *.msp 402 | 403 | # JetBrains Rider 404 | *.sln.iml 405 | .idea 406 | 407 | ## 408 | ## Visual studio for Mac 409 | ## 410 | 411 | 412 | # globs 413 | Makefile.in 414 | *.userprefs 415 | *.usertasks 416 | config.make 417 | config.status 418 | aclocal.m4 419 | install-sh 420 | autom4te.cache/ 421 | *.tar.gz 422 | tarballs/ 423 | test-results/ 424 | 425 | # Mac bundle stuff 426 | *.dmg 427 | *.app 428 | 429 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 430 | # General 431 | .DS_Store 432 | .AppleDouble 433 | .LSOverride 434 | 435 | # Icon must end with two \r 436 | Icon 437 | 438 | 439 | # Thumbnails 440 | ._* 441 | 442 | # Files that might appear in the root of a volume 443 | .DocumentRevisions-V100 444 | .fseventsd 445 | .Spotlight-V100 446 | .TemporaryItems 447 | .Trashes 448 | .VolumeIcon.icns 449 | .com.apple.timemachine.donotpresent 450 | 451 | # Directories potentially created on remote AFP share 452 | .AppleDB 453 | .AppleDesktop 454 | Network Trash Folder 455 | Temporary Items 456 | .apdisk 457 | 458 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 459 | # Windows thumbnail cache files 460 | Thumbs.db 461 | ehthumbs.db 462 | ehthumbs_vista.db 463 | 464 | # Dump file 465 | *.stackdump 466 | 467 | # Folder config file 468 | [Dd]esktop.ini 469 | 470 | # Recycle Bin used on file shares 471 | $RECYCLE.BIN/ 472 | 473 | # Windows Installer files 474 | *.cab 475 | *.msi 476 | *.msix 477 | *.msm 478 | *.msp 479 | 480 | # Windows shortcuts 481 | *.lnk 482 | 483 | # Vim temporary swap files 484 | *.swp 485 | /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/appsettings.development.json 486 | -------------------------------------------------------------------------------- /Blazor.WebAuthentication.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KristofferStrube.Blazor.CredentialManagement", "src\KristofferStrube.Blazor.CredentialManagement\KristofferStrube.Blazor.CredentialManagement.csproj", "{A0B974A9-0058-4DC7-AEFD-D1A4E4AA24FA}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KristofferStrube.Blazor.WebAuthentication", "src\KristofferStrube.Blazor.WebAuthentication\KristofferStrube.Blazor.WebAuthentication.csproj", "{A696790A-5532-4C70-9FE0-57892698BDAF}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KristofferStrube.Blazor.WebAuthentication.WasmExample", "samples\KristofferStrube.Blazor.WebAuthentication.WasmExample\KristofferStrube.Blazor.WebAuthentication.WasmExample.csproj", "{9811E16F-3101-45B3-B3AA-304A1391F0A0}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KristofferStrube.Blazor.COSEGenerator", "tools\KristofferStrube.Blazor.COSEGenerator\KristofferStrube.Blazor.COSEGenerator.csproj", "{07B38904-3166-4DFF-A33B-3F87B782D73A}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KristofferStrube.Blazor.WebAuthentication.API", "samples\KristofferStrube.Blazor.WebAuthentication.API\KristofferStrube.Blazor.WebAuthentication.API.csproj", "{3D9BCDE2-25B6-44C0-B5E1-69BE72EB1A21}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KristofferStrube.Blazor.WebAuthentication.Tests", "tests\KristofferStrube.Blazor.WebAuthentication.Tests\KristofferStrube.Blazor.WebAuthentication.Tests.csproj", "{57B4F0FE-1BF0-90A2-83D8-40C9B663527F}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {A0B974A9-0058-4DC7-AEFD-D1A4E4AA24FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {A0B974A9-0058-4DC7-AEFD-D1A4E4AA24FA}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {A0B974A9-0058-4DC7-AEFD-D1A4E4AA24FA}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {A0B974A9-0058-4DC7-AEFD-D1A4E4AA24FA}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {A696790A-5532-4C70-9FE0-57892698BDAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {A696790A-5532-4C70-9FE0-57892698BDAF}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {A696790A-5532-4C70-9FE0-57892698BDAF}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {A696790A-5532-4C70-9FE0-57892698BDAF}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {9811E16F-3101-45B3-B3AA-304A1391F0A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {9811E16F-3101-45B3-B3AA-304A1391F0A0}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {9811E16F-3101-45B3-B3AA-304A1391F0A0}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {9811E16F-3101-45B3-B3AA-304A1391F0A0}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {07B38904-3166-4DFF-A33B-3F87B782D73A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {07B38904-3166-4DFF-A33B-3F87B782D73A}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {07B38904-3166-4DFF-A33B-3F87B782D73A}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {07B38904-3166-4DFF-A33B-3F87B782D73A}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {3D9BCDE2-25B6-44C0-B5E1-69BE72EB1A21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {3D9BCDE2-25B6-44C0-B5E1-69BE72EB1A21}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {3D9BCDE2-25B6-44C0-B5E1-69BE72EB1A21}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {3D9BCDE2-25B6-44C0-B5E1-69BE72EB1A21}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {57B4F0FE-1BF0-90A2-83D8-40C9B663527F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {57B4F0FE-1BF0-90A2-83D8-40C9B663527F}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {57B4F0FE-1BF0-90A2-83D8-40C9B663527F}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {57B4F0FE-1BF0-90A2-83D8-40C9B663527F}.Release|Any CPU.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(NestedProjects) = preSolution 55 | {57B4F0FE-1BF0-90A2-83D8-40C9B663527F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} 56 | EndGlobalSection 57 | GlobalSection(ExtensibilityGlobals) = postSolution 58 | SolutionGuid = {C5A804B0-F754-47DE-9F85-5E8A7C937AE1} 59 | EndGlobalSection 60 | EndGlobal 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | Contributing includes many different actions. It is not only writing code. Contributions like discussing solutions in open issues are easily as valuable as contributing code as we can then find the best solution with the perspective of multiple people. 3 | 4 | ## Bugs and feature requests 5 | If you find any bugs in the project or have requests for features, then please [Open a new Issue](https://github.com/KristofferStrube/Blazor.WebAuthentication/issues/new). 6 | 7 | ## Contributing Code and Samples 8 | Before you contribute to the project, you will need to get confirmation from the library author that the contribution is welcome. 9 | This can help align the scope of the contribution so that you and the author agree on the solution and how you ensure the change is maintainable with the existing users in mind. 10 | Once you are ready to start coding try to follow these code guidelines: 11 | - Make a fork of the current `main` branch. 12 | - Follow the existing coding conventions used in the project. This includes tab style, naming conventions, etc. 13 | - If your contribution is a new feature, try to add a demo that demonstrates how this will be used in the sample project. 14 | - Any code or sample you share as a part of the resulting Pull Request should fall under the MIT license agreement. 15 | - You don't need to update the version number of the project as the maintainer will do this when making the next release after the Pull Request has been merged. 16 | - Keep your Pull Request to the point. I.e., if your Pull Request is related to fixing a bug then try not to touch any other files than the ones related to that issue as this will make the chance of the PR being merged without change requests more likely. 17 | 18 | ## Submitting a Pull Request 19 | If you don't know what a pull request is, read this article: https://help.github.com/articles/using-pull-requests. Make sure the repository can be built and that the related sample project still works as intended. It is also a good idea to familiarize yourself with the project workflow and our coding conventions. 20 | 21 | ## Ensuring that your contribution will be accepted 22 | You might also read these two blog posts on contributing code: [Open Source Contribution Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza and [Don't "Push" Your Pull Requests](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by Ilya Grigorik. These blog posts highlight good open-source collaboration etiquette and help align expectations between you and us. 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kristoffer Strube 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blazor.WebAuthentication 2 | A Blazor wrapper for the [Web Authentication](https://www.w3.org/TR/webauthn-3/) browser API. 3 | 4 | The API specifies ways to create and validate strong public-key-based credentials. It gets these credentials from the native authenticators of the device. On Windows, that's Windows Hello; on iOS/macOS, that's Touch ID or Face ID; and on Android, that's face, fingerprint, or PIN authentication. This project implements a wrapper around the API for Blazor so that we can easily and safely work with native authentication methods from the browser. 5 | 6 | *This wrapper is still under development.* 7 | 8 | # Demo 9 | The sample project can be demoed at https://kristofferstrube.github.io/Blazor.WebAuthentication/ 10 | 11 | On each page, you can find the corresponding code for the example in the top right corner. 12 | 13 | ## Logging and monitoring 14 | For the demo, I use [elmah.io](https://elmah.io) for logging and monitoring. This helps me to debug errors that might occur on specific devices or under special circumstances. The use of Error Handling JSInterop from [Blazor.WebIDL](https://github.com/KristofferStrube/Blazor.WebIDL) combined with elmah.io makes this especially useful. 15 | 16 | 17 | elmah.io gives a free Small Business subscription to any OSS project. Read more about this here: [Open Source - Monitor your open source website for free](https://elmah.io/sponsorship/opensource/) 18 | 19 | # Related articles 20 | This repository was built with inspiration and help from the following series of articles: 21 | 22 | - [Typed exceptions for JSInterop in Blazor](https://kristoffer-strube.dk/post/typed-exceptions-for-jsinterop-in-blazor/) 23 | - [Wrapping JavaScript libraries in Blazor WebAssembly/WASM](https://blog.elmah.io/wrapping-javascript-libraries-in-blazor-webassembly-wasm/) 24 | - [Call anonymous C# functions from JS in Blazor WASM](https://blog.elmah.io/call-anonymous-c-functions-from-js-in-blazor-wasm/) 25 | - [Using JS Object References in Blazor WASM to wrap JS libraries](https://blog.elmah.io/using-js-object-references-in-blazor-wasm-to-wrap-js-libraries/) 26 | - [Blazor WASM 404 error and fix for GitHub Pages](https://blog.elmah.io/blazor-wasm-404-error-and-fix-for-github-pages/) 27 | - [How to fix Blazor WASM base path problems](https://blog.elmah.io/how-to-fix-blazor-wasm-base-path-problems/) 28 | -------------------------------------------------------------------------------- /algorithms.csv: -------------------------------------------------------------------------------- 1 | Name,Value,Description,Capabilities,Change Controller,Reference,Recommended 2 | Reserved for Private Use,less than -65536,,,,[RFC9053],No 3 | Unassigned,-65536,,,,, 4 | RS1,-65535,RSASSA-PKCS1-v1_5 using SHA-1,[kty],IESG,[RFC8812][RFC9053],Deprecated 5 | A128CTR,-65534,AES-CTR w/ 128-bit key,[kty],IETF,[RFC9459],Deprecated 6 | A192CTR,-65533,AES-CTR w/ 192-bit key,[kty],IETF,[RFC9459],Deprecated 7 | A256CTR,-65532,AES-CTR w/ 256-bit key,[kty],IETF,[RFC9459],Deprecated 8 | A128CBC,-65531,AES-CBC w/ 128-bit key,[kty],IETF,[RFC9459],Deprecated 9 | A192CBC,-65530,AES-CBC w/ 192-bit key,[kty],IETF,[RFC9459],Deprecated 10 | A256CBC,-65529,AES-CBC w/ 256-bit key,[kty],IETF,[RFC9459],Deprecated 11 | Unassigned,-65528 to -261,,,,, 12 | WalnutDSA,-260,WalnutDSA signature,[kty],,[RFC9021][RFC9053],No 13 | RS512,-259,RSASSA-PKCS1-v1_5 using SHA-512,[kty],IESG,[RFC8812][RFC9053],No 14 | RS384,-258,RSASSA-PKCS1-v1_5 using SHA-384,[kty],IESG,[RFC8812][RFC9053],No 15 | RS256,-257,RSASSA-PKCS1-v1_5 using SHA-256,[kty],IESG,[RFC8812][RFC9053],No 16 | Unassigned,-256 to -48,,,,, 17 | ES256K,-47,ECDSA using secp256k1 curve and SHA-256,[kty],IESG,[RFC8812][RFC9053],No 18 | HSS-LMS,-46,HSS/LMS hash-based digital signature,[kty],,[RFC8778][RFC9053],Yes 19 | SHAKE256,-45,SHAKE-256 512-bit Hash Value,[kty],,[RFC9054][RFC9053],Yes 20 | SHA-512,-44,SHA-2 512-bit Hash,[kty],,[RFC9054][RFC9053],Yes 21 | SHA-384,-43,SHA-2 384-bit Hash,[kty],,[RFC9054][RFC9053],Yes 22 | RSAES-OAEP w/ SHA-512,-42,RSAES-OAEP w/ SHA-512,[kty],,[RFC8230][RFC9053],Yes 23 | RSAES-OAEP w/ SHA-256,-41,RSAES-OAEP w/ SHA-256,[kty],,[RFC8230][RFC9053],Yes 24 | RSAES-OAEP w/ RFC 8017 default parameters,-40,RSAES-OAEP w/ SHA-1,[kty],,[RFC8230][RFC9053],Yes 25 | PS512,-39,RSASSA-PSS w/ SHA-512,[kty],,[RFC8230][RFC9053],Yes 26 | PS384,-38,RSASSA-PSS w/ SHA-384,[kty],,[RFC8230][RFC9053],Yes 27 | PS256,-37,RSASSA-PSS w/ SHA-256,[kty],,[RFC8230][RFC9053],Yes 28 | ES512,-36,ECDSA w/ SHA-512,[kty],,[RFC9053],Yes 29 | ES384,-35,ECDSA w/ SHA-384,[kty],,[RFC9053],Yes 30 | ECDH-SS + A256KW,-34,ECDH SS w/ Concat KDF and AES Key Wrap w/ 256-bit key,[kty],,[RFC9053],Yes 31 | ECDH-SS + A192KW,-33,ECDH SS w/ Concat KDF and AES Key Wrap w/ 192-bit key,[kty],,[RFC9053],Yes 32 | ECDH-SS + A128KW,-32,ECDH SS w/ Concat KDF and AES Key Wrap w/ 128-bit key,[kty],,[RFC9053],Yes 33 | ECDH-ES + A256KW,-31,ECDH ES w/ Concat KDF and AES Key Wrap w/ 256-bit key,[kty],,[RFC9053],Yes 34 | ECDH-ES + A192KW,-30,ECDH ES w/ Concat KDF and AES Key Wrap w/ 192-bit key,[kty],,[RFC9053],Yes 35 | ECDH-ES + A128KW,-29,ECDH ES w/ Concat KDF and AES Key Wrap w/ 128-bit key,[kty],,[RFC9053],Yes 36 | ECDH-SS + HKDF-512,-28,ECDH SS w/ HKDF - generate key directly,[kty],,[RFC9053],Yes 37 | ECDH-SS + HKDF-256,-27,ECDH SS w/ HKDF - generate key directly,[kty],,[RFC9053],Yes 38 | ECDH-ES + HKDF-512,-26,ECDH ES w/ HKDF - generate key directly,[kty],,[RFC9053],Yes 39 | ECDH-ES + HKDF-256,-25,ECDH ES w/ HKDF - generate key directly,[kty],,[RFC9053],Yes 40 | Unassigned,-24 to -19,,,,, 41 | SHAKE128,-18,SHAKE-128 256-bit Hash Value,[kty],,[RFC9054][RFC9053],Yes 42 | SHA-512/256,-17,SHA-2 512-bit Hash truncated to 256-bits,[kty],,[RFC9054][RFC9053],Yes 43 | SHA-256,-16,SHA-2 256-bit Hash,[kty],,[RFC9054][RFC9053],Yes 44 | SHA-256/64,-15,SHA-2 256-bit Hash truncated to 64-bits,[kty],,[RFC9054][RFC9053],Filter Only 45 | SHA-1,-14,SHA-1 Hash,[kty],,[RFC9054][RFC9053],Filter Only 46 | direct+HKDF-AES-256,-13,Shared secret w/ AES-MAC 256-bit key,[kty],,[RFC9053],Yes 47 | direct+HKDF-AES-128,-12,Shared secret w/ AES-MAC 128-bit key,[kty],,[RFC9053],Yes 48 | direct+HKDF-SHA-512,-11,Shared secret w/ HKDF and SHA-512,[kty],,[RFC9053],Yes 49 | direct+HKDF-SHA-256,-10,Shared secret w/ HKDF and SHA-256,[kty],,[RFC9053],Yes 50 | Unassigned,-9,,,,, 51 | EdDSA,-8,EdDSA,[kty],,[RFC9053],Yes 52 | ES256,-7,ECDSA w/ SHA-256,[kty],,[RFC9053],Yes 53 | direct,-6,Direct use of CEK,[kty],,[RFC9053],Yes 54 | A256KW,-5,AES Key Wrap w/ 256-bit key,[kty],,[RFC9053],Yes 55 | A192KW,-4,AES Key Wrap w/ 192-bit key,[kty],,[RFC9053],Yes 56 | A128KW,-3,AES Key Wrap w/ 128-bit key,[kty],,[RFC9053],Yes 57 | Unassigned,-2 to -1,,,,, 58 | Reserved,0,,,,[RFC9053],No 59 | A128GCM,1,"AES-GCM mode w/ 128-bit key, 128-bit tag",[kty],,[RFC9053],Yes 60 | A192GCM,2,"AES-GCM mode w/ 192-bit key, 128-bit tag",[kty],,[RFC9053],Yes 61 | A256GCM,3,"AES-GCM mode w/ 256-bit key, 128-bit tag",[kty],,[RFC9053],Yes 62 | HMAC 256/64,4,HMAC w/ SHA-256 truncated to 64 bits,[kty],,[RFC9053],Yes 63 | HMAC 256/256,5,HMAC w/ SHA-256,[kty],,[RFC9053],Yes 64 | HMAC 384/384,6,HMAC w/ SHA-384,[kty],,[RFC9053],Yes 65 | HMAC 512/512,7,HMAC w/ SHA-512,[kty],,[RFC9053],Yes 66 | Unassigned,8-9,,,,, 67 | AES-CCM-16-64-128,10,"AES-CCM mode 128-bit key, 64-bit tag, 13-byte nonce",[kty],,[RFC9053],Yes 68 | AES-CCM-16-64-256,11,"AES-CCM mode 256-bit key, 64-bit tag, 13-byte nonce",[kty],,[RFC9053],Yes 69 | AES-CCM-64-64-128,12,"AES-CCM mode 128-bit key, 64-bit tag, 7-byte nonce",[kty],,[RFC9053],Yes 70 | AES-CCM-64-64-256,13,"AES-CCM mode 256-bit key, 64-bit tag, 7-byte nonce",[kty],,[RFC9053],Yes 71 | AES-MAC 128/64,14,"AES-MAC 128-bit key, 64-bit tag",[kty],,[RFC9053],Yes 72 | AES-MAC 256/64,15,"AES-MAC 256-bit key, 64-bit tag",[kty],,[RFC9053],Yes 73 | Unassigned,16-23,,,,, 74 | ChaCha20/Poly1305,24,"ChaCha20/Poly1305 w/ 256-bit key, 128-bit tag",[kty],,[RFC9053],Yes 75 | AES-MAC 128/128,25,"AES-MAC 128-bit key, 128-bit tag",[kty],,[RFC9053],Yes 76 | AES-MAC 256/128,26,"AES-MAC 256-bit key, 128-bit tag",[kty],,[RFC9053],Yes 77 | Unassigned,27-29,,,,, 78 | AES-CCM-16-128-128,30,"AES-CCM mode 128-bit key, 128-bit tag, 13-byte nonce",[kty],,[RFC9053],Yes 79 | AES-CCM-16-128-256,31,"AES-CCM mode 256-bit key, 128-bit tag, 13-byte nonce",[kty],,[RFC9053],Yes 80 | AES-CCM-64-128-128,32,"AES-CCM mode 128-bit key, 128-bit tag, 7-byte nonce",[kty],,[RFC9053],Yes 81 | AES-CCM-64-128-256,33,"AES-CCM mode 256-bit key, 128-bit tag, 7-byte nonce",[kty],,[RFC9053],Yes 82 | IV-GENERATION,34,For doing IV generation for symmetric algorithms.,,,[RFC9053],No 83 | -------------------------------------------------------------------------------- /generateCoseAlgorithms.ps1: -------------------------------------------------------------------------------- 1 | dotnet run --project ./tools/KristofferStrube.Blazor.COSEGenerator/ algorithms.csv ./src/KristofferStrube.Blazor.WebAuthentication/COSEAlgorithms.cs KristofferStrube.Blazor.WebAuthentication -------------------------------------------------------------------------------- /generateCoseAlgorithms.sh: -------------------------------------------------------------------------------- 1 | dotnet run --project ./tools/KristofferStrube.Blazor.COSEGenerator/ algorithms.csv ./src/KristofferStrube.Blazor.WebAuthentication/COSEAlgorithms.cs KristofferStrube.Blazor.WebAuthentication -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.API/KristofferStrube.Blazor.WebAuthentication.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.API/Program.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebAuthentication.API; 2 | 3 | WebApplicationBuilder builder = WebApplication.CreateBuilder(args); 4 | 5 | // Add services to the container. 6 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 7 | builder.Services.AddEndpointsApiExplorer(); 8 | builder.Services.AddSwaggerGen(); 9 | 10 | builder.Services.AddCors(o => o.AddPolicy("default", 11 | builder => 12 | builder.WithOrigins("https://localhost:7203", 13 | "https://kristofferstrube.github.io") 14 | .AllowAnyMethod() 15 | .AllowAnyHeader() 16 | )); 17 | 18 | WebApplication app = builder.Build(); 19 | 20 | // Configure the HTTP request pipeline. 21 | if (app.Environment.IsDevelopment()) 22 | { 23 | _ = app 24 | .UseSwagger() 25 | .UseSwaggerUI(); 26 | } 27 | 28 | app.UseHttpsRedirection(); 29 | 30 | app.UseCors("default"); 31 | 32 | app.MapWebAuthenticationAPI(); 33 | 34 | app.Run(); -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:7664", 8 | "sslPort": 44311 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5079", 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:7259;http://localhost:5079", 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 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.API/WebAuthenticationAPI.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebAuthentication.JSONRepresentations; 2 | using Microsoft.AspNetCore.Http.HttpResults; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.WebUtilities; 5 | using Microsoft.IdentityModel.Tokens; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using System.Text.Json; 9 | using static KristofferStrube.Blazor.WebAuthentication.AndroidSafetyNetAttestationStatement; 10 | 11 | namespace KristofferStrube.Blazor.WebAuthentication.API; 12 | 13 | public static class WebAuthenticationAPI 14 | { 15 | public static Dictionary Challenges = []; 16 | 17 | public static Dictionary> Credentials = []; 18 | 19 | public static Dictionary PublicKeys = []; 20 | 21 | public static IEndpointRouteBuilder MapWebAuthenticationAPI(this IEndpointRouteBuilder builder) 22 | { 23 | RouteGroupBuilder group = builder 24 | .MapGroup("WebAuthentication"); 25 | 26 | _ = group.MapGet("RegisterChallenge/{userName}", RegisterChallenge) 27 | .WithName("Register Challenge"); 28 | 29 | _ = group.MapPost("Register/{userName}", Register) 30 | .WithName("Register"); 31 | 32 | _ = group.MapGet("ValidateChallenge/{userName}", ValidateChallenge) 33 | .WithName("Validate Challenge"); 34 | 35 | _ = group.MapPost("Validate/{userName}", Validate) 36 | .WithName("Validate"); 37 | 38 | return builder; 39 | } 40 | 41 | public static Ok RegisterChallenge(string userName) 42 | { 43 | byte[] challenge = RandomNumberGenerator.GetBytes(32); 44 | Challenges[userName] = challenge; 45 | return TypedResults.Ok(challenge); 46 | } 47 | 48 | public static Results> Register(string userName, [FromBody] RegistrationResponseJSON registration) 49 | { 50 | CollectedClientData? clientData = JsonSerializer.Deserialize(Convert.FromBase64String(registration.Response.ClientDataJSON)); 51 | if (clientData is null) 52 | { 53 | return TypedResults.BadRequest("Client data was not present."); 54 | } 55 | 56 | if (!Challenges.TryGetValue(userName, out byte[]? originalChallenge)) 57 | { 58 | return TypedResults.BadRequest("Challenge did not exist."); 59 | } 60 | 61 | if (!originalChallenge.SequenceEqual(WebEncoders.Base64UrlDecode(clientData.Challenge))) 62 | { 63 | return TypedResults.BadRequest("Challenge did not match server side challenge."); 64 | } 65 | 66 | if (registration.Response.PublicKey is null) 67 | { 68 | return TypedResults.BadRequest("Response did not have a public key."); 69 | } 70 | 71 | AttestationStatement? attestationStatement; 72 | try 73 | { 74 | attestationStatement = AttestationStatement.ReadFromBase64EncodedAttestationStatement(registration.Response.AttestationObject); 75 | } 76 | catch (Exception e) 77 | { 78 | return TypedResults.BadRequest($"Could not parse attestation statement: \"{e.Message}\""); 79 | } 80 | 81 | switch (attestationStatement) 82 | { 83 | case PackedAttestationStatement packed: 84 | if (packed.Algorithm != (COSEAlgorithm)registration.Response.PublicKeyAlgorithm) 85 | { 86 | return TypedResults.BadRequest("The algorithm specified int the packed attestation format did not match the algorithm specified in the response."); 87 | } 88 | 89 | if (!VerifySignature(packed.Algorithm, Convert.FromBase64String(registration.Response.PublicKey), registration.Response.AuthenticatorData, registration.Response.ClientDataJSON, packed.Signature)) 90 | { 91 | return TypedResults.BadRequest("Signature was not valid."); 92 | } 93 | break; 94 | case TPMAttestationStatement tPMAttestationStatement: 95 | try 96 | { 97 | byte[] clientDataJSONBytes = Convert.FromBase64String(registration.Response.ClientDataJSON); 98 | byte[] clientDataHash = SHA256.Create().ComputeHash(clientDataJSONBytes); 99 | bool verified = tPMAttestationStatement.Verify(Convert.FromBase64String(registration.Response.AuthenticatorData), clientDataHash); 100 | if (!verified) 101 | { 102 | return TypedResults.BadRequest("TPM Attestation could not be verified."); 103 | } 104 | } 105 | catch 106 | { 107 | return TypedResults.BadRequest("An error occurred while verifying TPM Attestation."); 108 | } 109 | break; 110 | case AndroidSafetyNetAttestationStatement androidSafetyNet: 111 | string jwsResult = Encoding.UTF8.GetString(androidSafetyNet.Response); 112 | string[] parts = jwsResult.Split("."); 113 | string base64EncodedattestationStatementValiditiyJsonString = parts[1]; 114 | string attestationStatementValiditiyJsonString = Base64UrlEncoder.Decode(base64EncodedattestationStatementValiditiyJsonString); 115 | AttestationStatementValiditiy attestationStatementValiditiy = JsonSerializer.Deserialize(attestationStatementValiditiyJsonString); 116 | 117 | var concattedAuthenticatorAndClientData = Convert.FromBase64String(registration.Response.AuthenticatorData).Concat(Convert.FromBase64String(registration.Response.ClientDataJSON)).ToArray(); 118 | 119 | var hasher = SHA256.Create(); 120 | var hash = hasher.ComputeHash(concattedAuthenticatorAndClientData); 121 | var base64EncodedHash = Convert.ToBase64String(hash); 122 | 123 | if (base64EncodedHash != attestationStatementValiditiy.Nonce) 124 | return TypedResults.BadRequest($"Android SafetyNet nonce '{attestationStatementValiditiy.Nonce}' was not equal to hash '{base64EncodedHash}'. The full request was: {JsonSerializer.Serialize(registration)}"); 125 | 126 | break; 127 | case NoneAttestationStatement: 128 | return TypedResults.BadRequest("Attestation format 'None' was received which indicates that the device did not support attestation."); 129 | default: 130 | return TypedResults.BadRequest($"Verification of signature was not implemented for type {attestationStatement?.GetType().Name}"); 131 | } 132 | 133 | if (Credentials.TryGetValue(userName, out List? credentialList)) 134 | { 135 | credentialList.Add(Convert.FromBase64String(registration.RawId)); 136 | } 137 | else 138 | { 139 | Credentials[userName] = [Convert.FromBase64String(registration.RawId)]; 140 | } 141 | PublicKeys[registration.RawId] = ((COSEAlgorithm)registration.Response.PublicKeyAlgorithm, Convert.FromBase64String(registration.Response.PublicKey)); 142 | return TypedResults.Ok(); 143 | } 144 | 145 | public static Ok ValidateChallenge(string userName) 146 | { 147 | if (!Credentials.TryGetValue(userName, out List? credentialList)) 148 | { 149 | return TypedResults.Ok(new([], [])); 150 | } 151 | byte[] challenge = RandomNumberGenerator.GetBytes(32); 152 | Challenges[userName] = challenge; 153 | return TypedResults.Ok(new(challenge, credentialList)); 154 | } 155 | 156 | public class ValidateCredentials(byte[] challenge, List credentials) 157 | { 158 | public byte[] Challenge { get; set; } = challenge; 159 | public List Credentials { get; set; } = credentials; 160 | } 161 | 162 | public static Ok Validate(string userName, [FromBody] AuthenticationResponseJSON authentication) 163 | { 164 | CollectedClientData? clientData = JsonSerializer.Deserialize(Convert.FromBase64String(authentication.Response.ClientDataJSON)); 165 | if (clientData is null) 166 | { 167 | return TypedResults.Ok(false); 168 | } 169 | 170 | if (!Challenges.TryGetValue(userName, out byte[]? originalChallenge) 171 | || !originalChallenge.SequenceEqual(WebEncoders.Base64UrlDecode(clientData.Challenge))) 172 | { 173 | return TypedResults.Ok(false); 174 | } 175 | 176 | if (!PublicKeys.TryGetValue(authentication.RawId, out (COSEAlgorithm algorithm, byte[] key) publicKey)) 177 | { 178 | return TypedResults.Ok(false); 179 | } 180 | 181 | return TypedResults.Ok(VerifySignature(publicKey.algorithm, publicKey.key, authentication.Response.AuthenticatorData, authentication.Response.ClientDataJSON, Convert.FromBase64String(authentication.Response.Signature))); 182 | } 183 | 184 | public static bool VerifySignature(COSEAlgorithm publicKeyAlgorithm, byte[] publicKey, string authenticatorData, string clientData, byte[] signature) 185 | { 186 | if (publicKeyAlgorithm is COSEAlgorithm.ES256) 187 | { 188 | try 189 | { 190 | var dsa = ECDsa.Create(); 191 | dsa.ImportSubjectPublicKeyInfo(publicKey, out _); 192 | 193 | var Hash = SHA256.Create(); 194 | 195 | byte[] hashedClientData = Hash.ComputeHash(Convert.FromBase64String(clientData)); 196 | 197 | bool result = dsa.VerifyData(Convert.FromBase64String(authenticatorData).Concat(hashedClientData).ToArray(), signature, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence); 198 | 199 | return result; 200 | } 201 | catch (Exception) 202 | { 203 | return false; 204 | } 205 | } 206 | else if (publicKeyAlgorithm is COSEAlgorithm.RS256) 207 | { 208 | using var rsa = new RSACryptoServiceProvider(); 209 | 210 | try 211 | { 212 | rsa.ImportSubjectPublicKeyInfo(publicKey, out _); 213 | 214 | var Hash = SHA256.Create(); 215 | 216 | byte[] hashedClientData = Hash.ComputeHash(Convert.FromBase64String(clientData)); 217 | 218 | bool result = rsa.VerifyData(Convert.FromBase64String(authenticatorData).Concat(hashedClientData).ToArray(), signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 219 | 220 | return result; 221 | } 222 | catch (Exception) 223 | { 224 | return false; 225 | } 226 | finally 227 | { 228 | rsa.PersistKeyInCsp = false; 229 | } 230 | } 231 | return false; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 |

Sorry, there's nothing at this address.

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/KristofferStrube.Blazor.WebAuthentication.WasmExample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | 5a4e5a29-3e55-4bf5-831a-e180c29758cc 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using System.Security.Cryptography; 3 | @inject CredentialsService CredentialsService; 4 | 5 | Blazor Web Authentication 6 | 7 |

Blazor Web Authentication

8 | 9 | @if (!isSupported) 10 | { 11 |

The Web Authentication browser API is not supported in this browser. Try updating your browser/system or try another browser/device.

12 | return; 13 | } 14 | 15 |

16 | Here you can try to register some credentials and validate them. 17 |
18 | 19 | If you have registered some credentials for a username within 20 minutes then you can still validate it even after having refreshed this page as it is the server that remembers credentials. 20 |
21 | Usernames are not reserved so you can register the same username on multiple devices and validate with either of them. It would of cause be limited who can register new devices for a username in a real use case. 22 |
23 |

24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 | @if (publicKey is not null) 33 | { 34 | Registered a user! 🎊 35 |
36 | Public Key: @string.Join(", ", publicKey.Select(b => $"{b:X2}")) 37 |
38 | } 39 | @if (challenge is not null) 40 | { 41 |
42 | Challenge: @string.Join(", ", challenge.Select(b => $"{b:X2}")) 43 |
44 | } 45 |
46 | 47 | 48 | @if (validated is { } success) 49 | { 50 |
51 |
52 |
53 | @(success ? "You logged in and validated your credentials" : errorMessage ?? "You were not successful in logging on.") 54 |
55 | } 56 | else if (errorMessage is not null) 57 | { 58 |
59 |
60 |
@errorMessage
61 | } 62 |
63 |
64 | 65 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/Pages/Index.razor.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.CredentialManagement; 2 | using KristofferStrube.Blazor.WebAuthentication.JSONRepresentations; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.JSInterop; 5 | using System; 6 | using System.Text; 7 | using System.Text.Json; 8 | using static KristofferStrube.Blazor.WebAuthentication.WasmExample.WebAuthenticationClient; 9 | 10 | namespace KristofferStrube.Blazor.WebAuthentication.WasmExample.Pages; 11 | 12 | public partial class Index : ComponentBase 13 | { 14 | private bool isSupported = false; 15 | private string username = ""; 16 | private CredentialsContainer container = default!; 17 | private PublicKeyCredential? credential; 18 | private PublicKeyCredential? validatedCredential; 19 | private bool? validated = null; 20 | private string? errorMessage; 21 | private byte[]? challenge; 22 | private byte[]? publicKey; 23 | 24 | [Inject] 25 | public required ILogger Logger { get; set; } 26 | 27 | [Inject] 28 | public required IJSRuntime JSRuntime { get; set; } 29 | 30 | [Inject] 31 | public required WebAuthenticationClient WebAuthenticationClient { get; set; } 32 | 33 | protected override async Task OnInitializedAsync() 34 | { 35 | isSupported = await CredentialsService.IsSupportedAsync(); 36 | if (!isSupported) 37 | { 38 | return; 39 | } 40 | 41 | container = await CredentialsService.GetCredentialsAsync(); 42 | } 43 | 44 | private async Task CreateCredential() 45 | { 46 | try 47 | { 48 | if (username.Length == 0) 49 | { 50 | username = "default"; 51 | } 52 | 53 | byte[] userId = Encoding.ASCII.GetBytes(username); 54 | challenge = await WebAuthenticationClient.RegisterChallenge(username); 55 | 56 | if (challenge is null) 57 | { 58 | errorMessage = "Was not succesfull in registering a challenge before making credentials."; 59 | credential = null; 60 | return; 61 | } 62 | 63 | CredentialCreationOptions options = new() 64 | { 65 | PublicKey = new PublicKeyCredentialCreationOptions() 66 | { 67 | Rp = new PublicKeyCredentialRpEntity 68 | { 69 | Name = "Kristoffer Strube Consulting" 70 | }, 71 | User = new PublicKeyCredentialUserEntity() 72 | { 73 | Name = username, 74 | Id = userId, 75 | DisplayName = username 76 | }, 77 | Challenge = challenge, 78 | PubKeyCredParams = 79 | [ 80 | new PublicKeyCredentialParameters() 81 | { 82 | Type = PublicKeyCredentialType.PublicKey, 83 | Alg = COSEAlgorithm.ES256 84 | }, 85 | new PublicKeyCredentialParameters() 86 | { 87 | Type = PublicKeyCredentialType.PublicKey, 88 | Alg = COSEAlgorithm.RS256 89 | } 90 | ], 91 | Timeout = 360000, 92 | Hints = ["client-device"], 93 | Attestation = AttestationConveyancePreference.Direct, 94 | AttestationFormats = [AttestationFormat.TPM, AttestationFormat.Packed, AttestationFormat.AndroidKey, AttestationFormat.AndroidSafetyNet, AttestationFormat.Apple] 95 | } 96 | }; 97 | 98 | credential = await container.CreateAsync(options) is { } c ? new PublicKeyCredential(c) : null; 99 | 100 | if (credential is not null) 101 | { 102 | PublicKeyCredentialJSON registrationResponse = await credential.ToJSONAsync(); 103 | if (registrationResponse is RegistrationResponseJSON { } registration) 104 | { 105 | try 106 | { 107 | await WebAuthenticationClient.Register(username, registration); 108 | publicKey = registration.Response.PublicKey is not null ? Convert.FromBase64String(registration.Response.PublicKey) : null; 109 | } 110 | catch (Exception e) 111 | { 112 | errorMessage = $"Was not successfull in registering the credentials. {e.Message}"; 113 | credential = null; 114 | Logger.LogWarning(e, $"Error during creation of credentials. The challenge was: {string.Join(", ", challenge.Select(b => $"{b:X2}"))}; The registration response was: {JsonSerializer.Serialize(registration)};"); 115 | return; 116 | } 117 | } 118 | } 119 | 120 | errorMessage = null; 121 | validated = null; 122 | } 123 | catch (Exception exception) 124 | { 125 | errorMessage = $"{exception.GetType().Name}: \"{exception.Message}\""; 126 | credential = null; 127 | Logger.LogError(exception, "Error during creation of credentials."); 128 | } 129 | } 130 | 131 | private async Task GetCredential() 132 | { 133 | try 134 | { 135 | if (username.Length == 0) 136 | { 137 | username = "default"; 138 | } 139 | 140 | ValidateCredentials? setup = await WebAuthenticationClient.ValidateChallenge(username); 141 | if (setup is not { Challenge: { Length: > 0 } challenge, Credentials: { Count: > 0 } credentials }) 142 | { 143 | validated = null; 144 | errorMessage = "The user was not previously registered."; 145 | return; 146 | } 147 | this.challenge = challenge; 148 | 149 | List allowCredentials = new(credentials.Count); 150 | foreach (byte[] credential in credentials) 151 | { 152 | allowCredentials.Add(new PublicKeyCredentialDescriptor() 153 | { 154 | Type = PublicKeyCredentialType.PublicKey, 155 | Id = await JSRuntime.InvokeAsync("buffer", credential) 156 | }); 157 | } 158 | 159 | CredentialRequestOptions options = new() 160 | { 161 | PublicKey = new PublicKeyCredentialRequestOptions() 162 | { 163 | Challenge = challenge, 164 | Timeout = 360000, 165 | AllowCredentials = allowCredentials.ToArray() 166 | } 167 | }; 168 | 169 | validatedCredential = await container.GetAsync(options) is { } c ? new PublicKeyCredential(c) : null; 170 | 171 | if (validatedCredential is not null) 172 | { 173 | PublicKeyCredentialJSON authenticationResponse = await validatedCredential.ToJSONAsync(); 174 | if (authenticationResponse is AuthenticationResponseJSON { } authentication) 175 | { 176 | validated = await WebAuthenticationClient.Validate(username, authentication); 177 | } 178 | } 179 | 180 | errorMessage = null; 181 | } 182 | catch (Exception exception) 183 | { 184 | errorMessage = $"{exception.GetType().Name}: \"{exception.Message}\""; 185 | validatedCredential = null; 186 | validated = null; 187 | Logger.LogError(exception, "Error during validation of credentials."); 188 | } 189 | } 190 | 191 | } -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Elmah.Io.Blazor.Wasm; 2 | using KristofferStrube.Blazor.CredentialManagement; 3 | using KristofferStrube.Blazor.WebAuthentication.WasmExample; 4 | using KristofferStrube.Blazor.WebIDL; 5 | using Microsoft.AspNetCore.Components.Web; 6 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 7 | 8 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 9 | 10 | builder.RootComponents.Add("#app"); 11 | builder.RootComponents.Add("head::after"); 12 | 13 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 14 | 15 | builder.Services.AddKeyedScoped(typeof(WebAuthenticationClient), (_, _) => new HttpClient 16 | { 17 | BaseAddress = new Uri(builder.HostEnvironment.IsDevelopment() ? "https://localhost:7259/WebAuthentication/" : "https://kristoffer-strube.dk/API/WebAuthentication/") 18 | }); 19 | 20 | builder.Services.AddCredentialsService(); 21 | 22 | // For communicating with the API. 23 | builder.Services.AddScoped(); 24 | 25 | // Configuring elmah.io 26 | if (builder.HostEnvironment.IsProduction()) 27 | { 28 | builder.Logging.AddElmahIo(options => 29 | { 30 | options.ApiKey = ""; 31 | options.LogId = new Guid(""); 32 | }); 33 | } 34 | else 35 | { 36 | IConfiguration elmahIoOptions = builder.Configuration.GetSection("ElmahIo"); 37 | builder.Logging.AddElmahIo(options => 38 | { 39 | options.ApiKey = elmahIoOptions.GetValue("ApiKey"); 40 | options.LogId = new Guid(elmahIoOptions.GetValue("LogId")); 41 | }); 42 | } 43 | 44 | WebAssemblyHost app = builder.Build(); 45 | 46 | await app.Services.SetupErrorHandlingJSInterop(); 47 | 48 | await app.RunAsync(); -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:28210", 8 | "sslPort": 44363 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 17 | "applicationUrl": "http://localhost:5289", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 27 | "applicationUrl": "https://localhost:7203;http://localhost:5289", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @inject NavigationManager NavigationManager 3 | 4 |
5 | 8 | 9 |
10 |
11 | 12 | 13 | 14 |
15 | 16 |
17 | @Body 18 |
19 |
20 |
21 | 22 | @code { 23 | private string relativeUri => NavigationManager.ToBaseRelativePath(NavigationManager.Uri); 24 | 25 | protected string page => (string.IsNullOrEmpty(relativeUri) ? "Index" : relativeUri) + ".razor"; 26 | } -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row:not(.auth) { 41 | display: none; 42 | } 43 | 44 | .top-row.auth { 45 | justify-content: space-between; 46 | } 47 | 48 | .top-row ::deep a, .top-row ::deep .btn-link { 49 | margin-left: 0; 50 | } 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .page { 55 | flex-direction: row; 56 | } 57 | 58 | .sidebar { 59 | width: 250px; 60 | height: 100vh; 61 | position: sticky; 62 | top: 0; 63 | } 64 | 65 | .top-row { 66 | position: sticky; 67 | top: 0; 68 | z-index: 1; 69 | } 70 | 71 | .top-row.auth ::deep a:first-child { 72 | flex: 1; 73 | text-align: right; 74 | width: 0; 75 | } 76 | 77 | .top-row, article { 78 | padding-left: 2rem !important; 79 | padding-right: 1.5rem !important; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  9 | 10 | 19 | 20 | @code { 21 | private bool collapseNavMenu = true; 22 | 23 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 24 | 25 | private void ToggleNavMenu() 26 | { 27 | collapseNavMenu = !collapseNavMenu; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .bi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | 63 | .nav-scrollable { 64 | /* Allow sidebar to scroll for tall menus */ 65 | height: calc(100vh - 3.5rem); 66 | overflow-y: auto; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/WebAuthenticationClient.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebAuthentication.JSONRepresentations; 2 | using System.Net.Http.Json; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication.WasmExample; 5 | 6 | public class WebAuthenticationClient 7 | { 8 | private readonly HttpClient httpClient; 9 | 10 | public WebAuthenticationClient([FromKeyedServices(typeof(WebAuthenticationClient))] HttpClient httpClient) 11 | { 12 | this.httpClient = httpClient; 13 | } 14 | 15 | public async Task RegisterChallenge(string userName) 16 | { 17 | return await httpClient.GetFromJsonAsync($"RegisterChallenge/{userName}"); 18 | } 19 | 20 | public async Task Register(string userName, RegistrationResponseJSON registrationResponse) 21 | { 22 | HttpResponseMessage result = await httpClient.PostAsJsonAsync($"Register/{userName}", registrationResponse); 23 | 24 | if (!result.IsSuccessStatusCode) 25 | throw new ArgumentException(await result.Content.ReadAsStringAsync()); 26 | } 27 | 28 | public async Task ValidateChallenge(string userName) 29 | { 30 | return await httpClient.GetFromJsonAsync($"ValidateChallenge/{userName}"); 31 | } 32 | public class ValidateCredentials(byte[] challenge, List credentials) 33 | { 34 | public byte[] Challenge { get; set; } = challenge; 35 | public List Credentials { get; set; } = credentials; 36 | } 37 | 38 | public async Task Validate(string userName, AuthenticationResponseJSON authenticationResponse) 39 | { 40 | HttpResponseMessage result = await httpClient.PostAsJsonAsync($"Validate/{userName}", authenticationResponse); 41 | return await result.Content.ReadFromJsonAsync(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using KristofferStrube.Blazor.CredentialManagement 10 | @using KristofferStrube.Blazor.WebAuthentication 11 | @using KristofferStrube.Blazor.WebAuthentication.WasmExample 12 | @using KristofferStrube.Blazor.WebAuthentication.WasmExample.Shared 13 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/404.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Blazor Web Authentication 6 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | h1:focus { 6 | outline: none; 7 | } 8 | 9 | a, .btn-link { 10 | color: #0071c1; 11 | } 12 | 13 | .btn-primary { 14 | color: #fff; 15 | background-color: #1b6ec2; 16 | border-color: #1861ac; 17 | } 18 | 19 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 20 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 21 | } 22 | 23 | .content { 24 | padding-top: 1.1rem; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid red; 33 | } 34 | 35 | .validation-message { 36 | color: red; 37 | } 38 | 39 | #blazor-error-ui { 40 | background: lightyellow; 41 | bottom: 0; 42 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 43 | display: none; 44 | left: 0; 45 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 46 | position: fixed; 47 | width: 100%; 48 | z-index: 1000; 49 | } 50 | 51 | #blazor-error-ui .dismiss { 52 | cursor: pointer; 53 | position: absolute; 54 | right: 0.75rem; 55 | top: 0.5rem; 56 | } 57 | 58 | .blazor-error-boundary { 59 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 60 | padding: 1rem 1rem 1rem 3.7rem; 61 | color: white; 62 | } 63 | 64 | .blazor-error-boundary::after { 65 | content: "An error has occurred." 66 | } 67 | 68 | .loading-progress { 69 | position: relative; 70 | display: block; 71 | width: 8rem; 72 | height: 8rem; 73 | margin: 20vh auto 1rem auto; 74 | } 75 | 76 | .loading-progress circle { 77 | fill: none; 78 | stroke: #e0e0e0; 79 | stroke-width: 0.6rem; 80 | transform-origin: 50% 50%; 81 | transform: rotate(-90deg); 82 | } 83 | 84 | .loading-progress circle:last-child { 85 | stroke: #1b6ec2; 86 | stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; 87 | transition: stroke-dasharray 0.05s ease-in-out; 88 | } 89 | 90 | .loading-progress-text { 91 | position: absolute; 92 | text-align: center; 93 | font-weight: bold; 94 | inset: calc(20vh + 3.25rem) 0 auto 0.2rem; 95 | } 96 | 97 | .loading-progress-text:after { 98 | content: var(--blazor-load-percentage-text, "Loading"); 99 | } 100 | -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/css/bootstrap-icons/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KristofferStrube/Blazor.WebAuthentication/01762a50c06fa51d1b187498300b4ac6dd833d7a/samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/css/bootstrap-icons/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/css/bootstrap-icons/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KristofferStrube/Blazor.WebAuthentication/01762a50c06fa51d1b187498300b4ac6dd833d7a/samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/css/bootstrap-icons/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KristofferStrube/Blazor.WebAuthentication/01762a50c06fa51d1b187498300b4ac6dd833d7a/samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/favicon.png -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KristofferStrube/Blazor.WebAuthentication/01762a50c06fa51d1b187498300b4ac6dd833d7a/samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/icon-192.png -------------------------------------------------------------------------------- /samples/KristofferStrube.Blazor.WebAuthentication.WasmExample/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blazor Web Authentication 8 | 9 | 10 | 32 | 43 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | 58 | 59 | 60 | 61 |
62 |
63 | 64 |
65 | An unhandled error has occurred. 66 | Reload 67 | 🗙 68 |
69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/BaseJSWrapper.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.CredentialManagement.Converters; 2 | using KristofferStrube.Blazor.CredentialManagement.Extensions; 3 | using KristofferStrube.Blazor.WebIDL; 4 | using Microsoft.JSInterop; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace KristofferStrube.Blazor.CredentialManagement; 8 | 9 | [JsonConverter(typeof(IJSWrapperConverter))] 10 | public abstract class BaseJSWrapper : IJSWrapper, IAsyncDisposable 11 | { 12 | protected readonly Lazy> helperTask; 13 | 14 | public IJSRuntime JSRuntime { get; } 15 | public IJSObjectReference JSReference { get; } 16 | 17 | /// 18 | /// Constructs a wrapper instance for an equivalent JS instance. 19 | /// 20 | /// An instance. 21 | /// A JS reference to an existing JS instance that should be wrapped. 22 | internal BaseJSWrapper(IJSRuntime jSRuntime, IJSObjectReference jSReference) 23 | { 24 | helperTask = new(jSRuntime.GetHelperAsync); 25 | JSReference = jSReference; 26 | JSRuntime = jSRuntime; 27 | } 28 | 29 | public async ValueTask DisposeAsync() 30 | { 31 | if (helperTask.IsValueCreated) 32 | { 33 | IJSObjectReference module = await helperTask.Value; 34 | await module.DisposeAsync(); 35 | } 36 | GC.SuppressFinalize(this); 37 | } 38 | } -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/Converters/CredentialMediationRequirementConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.CredentialManagement.Converters; 5 | 6 | public class CredentialMediationRequirementConverter : JsonConverter 7 | { 8 | public override CredentialMediationRequirement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | throw new NotImplementedException(); 11 | } 12 | 13 | public override void Write(Utf8JsonWriter writer, CredentialMediationRequirement value, JsonSerializerOptions options) 14 | { 15 | writer.WriteStringValue(value switch 16 | { 17 | CredentialMediationRequirement.Silent => "silent", 18 | CredentialMediationRequirement.Optional => "optional", 19 | CredentialMediationRequirement.Required => "required", 20 | _ => throw new ArgumentException($"Value '{value}' was not a valid {nameof(CredentialMediationRequirement)}.") 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/Converters/IJSWrapperConverter.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebIDL; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace KristofferStrube.Blazor.CredentialManagement.Converters; 6 | 7 | public class IJSWrapperConverter : JsonConverter where TWrapper : IJSWrapper 8 | { 9 | public override TWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | throw new NotImplementedException(); 12 | } 13 | 14 | public override void Write(Utf8JsonWriter writer, TWrapper value, JsonSerializerOptions options) 15 | { 16 | writer.WriteRawValue(JsonSerializer.Serialize(value.JSReference, options)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/Credential.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace KristofferStrube.Blazor.CredentialManagement; 4 | 5 | public class Credential : BaseJSWrapper 6 | { 7 | /// 8 | /// Constructs a wrapper instance for a given JS Instance of a . 9 | /// 10 | /// An instance. 11 | /// A JS reference to an existing . 12 | protected internal Credential(IJSRuntime jSRuntime, IJSObjectReference jSReference) : base(jSRuntime, jSReference) 13 | { 14 | } 15 | 16 | public async Task GetIdAsync() 17 | { 18 | IJSObjectReference helper = await helperTask.Value; 19 | return await helper.InvokeAsync("getAttribute", JSReference, "id"); 20 | } 21 | 22 | public async Task GetTypeAsync() 23 | { 24 | IJSObjectReference helper = await helperTask.Value; 25 | return await helper.InvokeAsync("getAttribute", JSReference, "type"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/CredentialsContainer.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebIDL; 2 | using Microsoft.JSInterop; 3 | 4 | namespace KristofferStrube.Blazor.CredentialManagement; 5 | 6 | public class CredentialsContainer : BaseJSWrapper 7 | { 8 | protected IJSObjectReference ErrorHandlingJSReference { get; set; } 9 | 10 | /// 11 | /// Constructs a wrapper instance for a given JS Instance of a . 12 | /// 13 | /// An instance. 14 | /// A JS reference to an existing . 15 | protected internal CredentialsContainer(IJSRuntime jSRuntime, IJSObjectReference jSReference) : base(jSRuntime, jSReference) 16 | { 17 | ErrorHandlingJSReference = new ErrorHandlingJSObjectReference(jSRuntime, jSReference); 18 | } 19 | 20 | public async Task GetAsync(CredentialRequestOptions? options = null) 21 | { 22 | IJSObjectReference? jSInstance = await ErrorHandlingJSReference.InvokeAsync("get", options); 23 | return jSInstance is null ? null : new Credential(JSRuntime, jSInstance); 24 | } 25 | 26 | public async Task StoreAsync(Credential credential) 27 | { 28 | IJSObjectReference jSInstance = await ErrorHandlingJSReference.InvokeAsync("store", credential); 29 | return new Credential(JSRuntime, jSInstance); 30 | } 31 | 32 | public async Task CreateAsync(CredentialCreationOptions? options = null) 33 | { 34 | IJSObjectReference? jSInstance = await ErrorHandlingJSReference.InvokeAsync("create", options); 35 | return jSInstance is null ? null : new Credential(JSRuntime, jSInstance); 36 | } 37 | 38 | public async Task PreventSilentAccessAsync() 39 | { 40 | await JSReference.InvokeVoidAsync("preventSilentAccess"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/CredentialsService.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.CredentialManagement.Extensions; 2 | using Microsoft.JSInterop; 3 | 4 | namespace KristofferStrube.Blazor.CredentialManagement; 5 | 6 | public class CredentialsService(IJSRuntime jSRuntime) 7 | { 8 | public async Task GetCredentialsAsync() 9 | { 10 | IJSObjectReference jSInstance = await jSRuntime.InvokeAsync("navigator.credentials.valueOf"); 11 | return new CredentialsContainer(jSRuntime, jSInstance); 12 | } 13 | 14 | public async Task IsSupportedAsync() 15 | { 16 | IJSObjectReference helper = await jSRuntime.GetHelperAsync(); 17 | return await helper.InvokeAsync("isSupported"); 18 | } 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/Extensions/IJSRuntimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace KristofferStrube.Blazor.CredentialManagement.Extensions; 4 | 5 | internal static class IJSRuntimeExtensions 6 | { 7 | internal static async Task GetHelperAsync(this IJSRuntime jSRuntime) 8 | { 9 | return await jSRuntime.InvokeAsync( 10 | "import", "./_content/KristofferStrube.Blazor.CredentialManagement/KristofferStrube.Blazor.CredentialManagement.js"); 11 | } 12 | internal static async Task GetInProcessHelperAsync(this IJSRuntime jSRuntime) 13 | { 14 | return await jSRuntime.InvokeAsync( 15 | "import", "./_content/KristofferStrube.Blazor.CredentialManagement/KristofferStrube.Blazor.CredentialManagement.js"); 16 | } 17 | } -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/Extensions/IServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace KristofferStrube.Blazor.CredentialManagement; 4 | 5 | public static class IServiceCollectionExtensions 6 | { 7 | public static IServiceCollection AddCredentialsService(this IServiceCollection serviceCollection) 8 | { 9 | return serviceCollection.AddScoped(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/KristofferStrube.Blazor.CredentialManagement.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/Options/CredentialCreationOptions.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.CredentialManagement.Converters; 2 | using KristofferStrube.Blazor.DOM; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace KristofferStrube.Blazor.CredentialManagement; 6 | 7 | public class CredentialCreationOptions 8 | { 9 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 10 | [JsonConverter(typeof(IJSWrapperConverter>))] 11 | [JsonPropertyName("signal")] 12 | public AbortSignal? Signal { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/Options/CredentialMediationRequirement.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.CredentialManagement.Converters; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.CredentialManagement; 5 | 6 | [JsonConverter(typeof(CredentialMediationRequirementConverter))] 7 | public enum CredentialMediationRequirement 8 | { 9 | Silent, 10 | Optional, 11 | Required 12 | } 13 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/Options/CredentialRequestOptions.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.CredentialManagement.Converters; 2 | using KristofferStrube.Blazor.DOM; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace KristofferStrube.Blazor.CredentialManagement; 6 | 7 | public class CredentialRequestOptions 8 | { 9 | [JsonPropertyName("mediation")] 10 | public CredentialMediationRequirement Mediation { get; set; } = CredentialMediationRequirement.Optional; 11 | 12 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 13 | [JsonConverter(typeof(IJSWrapperConverter>))] 14 | [JsonPropertyName("signal")] 15 | public AbortSignal? Signal { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.CredentialManagement/wwwroot/KristofferStrube.Blazor.CredentialManagement.js: -------------------------------------------------------------------------------- 1 | export function getAttribute(object, attribute) { return object[attribute]; } 2 | 3 | export function isSupported() { return !(!window.PublicKeyCredential); } -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/AttestationStatements/AndroidSafetyNetAttestationStatement.cs: -------------------------------------------------------------------------------- 1 | using System.Formats.Cbor; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication; 5 | 6 | /// 7 | /// When the authenticator is a platform authenticator on certain Android platforms, the attestation statement may be based on the SafetyNet API. 8 | /// In this case the authenticator data is completely controlled by the caller of the SafetyNet API (typically an application running on the Android platform) 9 | /// and the attestation statement provides some statements about the health of the platform and the identity of the calling application 10 | /// 11 | /// See the API definition here. 12 | public class AndroidSafetyNetAttestationStatement : AttestationStatement 13 | { 14 | /// 15 | /// The algorithm used to generate the attestation signature. 16 | /// 17 | public required string Version { get; set; } 18 | 19 | /// 20 | /// The UTF-8 encoded result of the getJwsResult() call of the SafetyNet API. 21 | /// This value is a JWS object in Compact Serialization. 22 | /// 23 | public required byte[] Response { get; set; } 24 | 25 | internal static AndroidSafetyNetAttestationStatement ReadAttestationStatement(CborReader cborReader) 26 | { 27 | CborReaderState state = cborReader.PeekState(); 28 | if (state is not CborReaderState.TextString) 29 | { 30 | throw new FormatException($"Attestation Statement's second key was of type '{state}' but '{CborReaderState.TextString}' was expected."); 31 | } 32 | 33 | string label = cborReader.ReadTextString(); 34 | if (label is not "attStmt") 35 | { 36 | throw new FormatException($"Attestation Statement's second key was '{label}' but 'attStmt' was expected."); 37 | } 38 | 39 | state = cborReader.PeekState(); 40 | if (state is not CborReaderState.StartMap) 41 | { 42 | throw new FormatException($"Attestation Statement's 'attStmt' was of type '{state}' but '{CborReaderState.StartMap}' was expected."); 43 | } 44 | 45 | int? mapSize = cborReader.ReadStartMap(); 46 | if (mapSize is not 2) 47 | { 48 | throw new FormatException($"Attestation Statement's safety net format had '{mapSize}' entries but '2' was expected."); 49 | } 50 | 51 | state = cborReader.PeekState(); 52 | if (state is not CborReaderState.TextString) 53 | { 54 | throw new FormatException($"Attestation Statement's safety net format's first key was of type '{state}' but '{CborReaderState.TextString}' was expected."); 55 | } 56 | 57 | label = cborReader.ReadTextString(); 58 | if (label is not "ver") 59 | { 60 | throw new FormatException($"Attestation Statement's safety net format's first key was '{label}' but 'ver' was expected."); 61 | } 62 | 63 | state = cborReader.PeekState(); 64 | if (state is not CborReaderState.TextString) 65 | { 66 | throw new FormatException($"Attestation Statement's safety net format's 'ver' was of type '{state}' but '{CborReaderState.TextString}' was expected."); 67 | } 68 | 69 | string version = cborReader.ReadTextString(); 70 | 71 | state = cborReader.PeekState(); 72 | if (state is not CborReaderState.TextString) 73 | { 74 | throw new FormatException($"Attestation Statement's safety net format's second key was of type '{state}' but '{CborReaderState.TextString}' was expected."); 75 | } 76 | 77 | label = cborReader.ReadTextString(); 78 | if (label is not "response") 79 | { 80 | throw new FormatException($"Attestation Statement's safety net format's second key was '{label}' but 'response' was expected."); 81 | } 82 | 83 | state = cborReader.PeekState(); 84 | if (state is not CborReaderState.ByteString) 85 | { 86 | throw new FormatException($"Attestation Statement's safety net format's 'response' was of type '{state}' but '{CborReaderState.ByteString}' was expected."); 87 | } 88 | 89 | byte[] response = cborReader.ReadByteString(); 90 | 91 | return new() 92 | { 93 | Version = version, 94 | Response = response 95 | }; 96 | } 97 | 98 | public class AttestationStatementValiditiy 99 | { 100 | [JsonPropertyName("nonce")] 101 | public string Nonce { get; set; } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/AttestationStatements/AttestationStatement.cs: -------------------------------------------------------------------------------- 1 | using System.Formats.Cbor; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | /// 6 | /// WebAuthn supports pluggable attestation statement formats. 7 | /// Attestation statement formats are identified by a string, called an attestation statement format identifier, chosen by the author of the attestation statement format. 8 | /// 9 | /// See the API definition here. 10 | public abstract class AttestationStatement 11 | { 12 | public static AttestationStatement ReadFromBase64EncodedAttestationStatement(string input) 13 | { 14 | CborReader cborReader = new(Convert.FromBase64String(input)); 15 | 16 | CborReaderState state = cborReader.PeekState(); 17 | 18 | if (state is not CborReaderState.StartMap) 19 | { 20 | throw new FormatException("Attestation Statement did not start with a map."); 21 | } 22 | 23 | int? mapSize = cborReader.ReadStartMap(); 24 | if (mapSize is not 3) 25 | { 26 | throw new FormatException($"Attestation Statement had '{mapSize}' entries in its first map but '3' was expected."); 27 | } 28 | 29 | state = cborReader.PeekState(); 30 | if (state is not CborReaderState.TextString) 31 | { 32 | throw new FormatException($"Attestation Statement's first key was of type '{state}' but '{CborReaderState.TextString}' was expected."); 33 | } 34 | 35 | string label = cborReader.ReadTextString(); 36 | if (label is not "fmt") 37 | { 38 | throw new FormatException($"Attestation Statement's first key was '{label}' but 'fmt' was expected."); 39 | } 40 | 41 | state = cborReader.PeekState(); 42 | if (state is not CborReaderState.TextString) 43 | { 44 | throw new FormatException($"Attestation Statement's first value was of type '{state}' but '{CborReaderState.TextString}' was expected."); 45 | } 46 | 47 | string fmt = cborReader.ReadTextString(); 48 | 49 | return fmt switch 50 | { 51 | "packed" => PackedAttestationStatement.ReadAttestationStatement(cborReader), 52 | "tpm" => TPMAttestationStatement.ReadAttestationStatement(cborReader), 53 | "android-safetynet" => AndroidSafetyNetAttestationStatement.ReadAttestationStatement(cborReader), 54 | "none" => new NoneAttestationStatement(), 55 | _ => throw new FormatException($"Attestation Statement had format '{fmt}' which was not supported.") 56 | }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/AttestationStatements/NoneAttestationStatement.cs: -------------------------------------------------------------------------------- 1 | namespace KristofferStrube.Blazor.WebAuthentication; 2 | 3 | /// 4 | /// The none attestation statement format is used to replace any authenticator-provided attestation statement when a WebAuthn Relying Party indicates it does not wish to receive attestation information. 5 | /// 6 | /// See the API definition here. 7 | public class NoneAttestationStatement : AttestationStatement 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/AttestationStatements/PackedAttestationStatement.cs: -------------------------------------------------------------------------------- 1 | using System.Formats.Cbor; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | /// 6 | /// This is a WebAuthn optimized attestation statement format. 7 | /// It uses a very compact but still extensible encoding method. 8 | /// It is implementable by authenticators with limited resources (e.g., secure elements). 9 | /// 10 | /// See the API definition here. 11 | public class PackedAttestationStatement : AttestationStatement 12 | { 13 | /// 14 | /// The algorithm used to generate the attestation signature. 15 | /// 16 | public required COSEAlgorithm Algorithm { get; set; } 17 | 18 | /// 19 | /// A byte string containing the attestation signature. 20 | /// 21 | public required byte[] Signature { get; set; } 22 | 23 | /// 24 | /// The elements of this array contain attestation certificate and its certificate chain (if any), each encoded in X.509 format. 25 | /// The attestation certificate will be the first element in the array. 26 | /// 27 | public byte[][]? X5c { get; set; } 28 | 29 | internal static PackedAttestationStatement ReadAttestationStatement(CborReader cborReader) 30 | { 31 | CborReaderState state = cborReader.PeekState(); 32 | if (state is not CborReaderState.TextString) 33 | { 34 | throw new FormatException($"Attestation Statement's second key was of type '{state}' but '{CborReaderState.TextString}' was expected."); 35 | } 36 | 37 | string label = cborReader.ReadTextString(); 38 | if (label is not "attStmt") 39 | { 40 | throw new FormatException($"Attestation Statement's second key was '{label}' but 'attStmt' was expected."); 41 | } 42 | 43 | state = cborReader.PeekState(); 44 | if (state is not CborReaderState.StartMap) 45 | { 46 | throw new FormatException($"Attestation Statement's 'attStmt' was of type '{state}' but '{CborReaderState.StartMap}' was expected."); 47 | } 48 | 49 | int? mapSize = cborReader.ReadStartMap(); 50 | if (mapSize is not 2 or 3) 51 | { 52 | throw new FormatException($"Attestation Statement's packed format had '{mapSize}' entries but '2' or '3' was expected."); 53 | } 54 | 55 | state = cborReader.PeekState(); 56 | if (state is not CborReaderState.TextString) 57 | { 58 | throw new FormatException($"Attestation Statement's packed format's first key was of type '{state}' but '{CborReaderState.TextString}' was expected."); 59 | } 60 | 61 | label = cborReader.ReadTextString(); 62 | if (label is not "alg") 63 | { 64 | throw new FormatException($"Attestation Statement's packed format's first key was '{label}' but 'alg' was expected."); 65 | } 66 | 67 | state = cborReader.PeekState(); 68 | if (state is not CborReaderState.NegativeInteger) 69 | { 70 | throw new FormatException($"Attestation Statement's packed format's 'alg' was of type '{state}' but '{CborReaderState.NegativeInteger}' was expected."); 71 | } 72 | 73 | ulong negativeAlg = cborReader.ReadCborNegativeIntegerRepresentation(); 74 | 75 | state = cborReader.PeekState(); 76 | if (state is not CborReaderState.TextString) 77 | { 78 | throw new FormatException($"Attestation Statement's packed format's second key was of type '{state}' but '{CborReaderState.TextString}' was expected."); 79 | } 80 | 81 | label = cborReader.ReadTextString(); 82 | if (label is not "sig") 83 | { 84 | throw new FormatException($"Attestation Statement's packed format's second key was '{label}' but 'sig' was expected."); 85 | } 86 | 87 | state = cborReader.PeekState(); 88 | if (state is not CborReaderState.ByteString) 89 | { 90 | throw new FormatException($"Attestation Statement's packed format's 'sig' was of type '{state}' but '{CborReaderState.ByteString}' was expected."); 91 | } 92 | 93 | byte[] signature = cborReader.ReadByteString(); 94 | 95 | if (mapSize is 2) 96 | { 97 | return new() 98 | { 99 | Algorithm = (COSEAlgorithm)(-(long)negativeAlg - 1), 100 | Signature = signature, 101 | }; 102 | } 103 | 104 | throw new NotSupportedException("Reading x5c is not yet supported."); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/AttestationStatements/TPMAttestationStatement.cs: -------------------------------------------------------------------------------- 1 | using System.Formats.Cbor; 2 | using System.Runtime.Intrinsics.Arm; 3 | using System.Security.Cryptography; 4 | 5 | namespace KristofferStrube.Blazor.WebAuthentication; 6 | 7 | /// 8 | /// This attestation statement format is generally used by authenticators that use a Trusted Platform Module as their cryptographic engine. 9 | /// 10 | /// See the API definition here. 11 | public class TPMAttestationStatement : AttestationStatement 12 | { 13 | /// 14 | /// The version of the TPM specification to which the signature conforms. 15 | /// 16 | public required string Version { get; set; } 17 | 18 | /// 19 | /// The algorithm used to generate the attestation signature. 20 | /// 21 | public required COSEAlgorithm Algorithm { get; set; } 22 | 23 | /// 24 | /// The AIK certificate used for the attestation, in X.509 encoding. Followed by its certificate chain, in X.509 encoding. 25 | /// 26 | public byte[][]? X5c { get; set; } 27 | 28 | /// 29 | /// The attestation signature, in the form of a TPMT_SIGNATURE structure as specified in TPMv2-Part2 section 11.3.4. 30 | /// 31 | public required byte[] Signature { get; set; } 32 | 33 | /// 34 | /// The TPMS_ATTEST structure over which the above signature was computed, as specified in TPMv2-Part2 section 10.12.8. 35 | /// 36 | public required byte[] CertificateInformation { get; set; } 37 | 38 | /// 39 | /// The TPMT_PUBLIC structure (see TPMv2-Part2 section 12.2.4) used by the TPM to represent the credential public key. 40 | /// 41 | public required byte[] PublicArea { get; set; } 42 | 43 | internal static TPMAttestationStatement ReadAttestationStatement(CborReader cborReader) 44 | { 45 | CborReaderState state = cborReader.PeekState(); 46 | if (state is not CborReaderState.TextString) 47 | { 48 | throw new FormatException($"Attestation Statement's second key was of type '{state}' but '{CborReaderState.TextString}' was expected."); 49 | } 50 | 51 | string label = cborReader.ReadTextString(); 52 | if (label is not "attStmt") 53 | { 54 | throw new FormatException($"Attestation Statement's second key was '{label}' but 'attStmt' was expected."); 55 | } 56 | 57 | state = cborReader.PeekState(); 58 | if (state is not CborReaderState.StartMap) 59 | { 60 | throw new FormatException($"Attestation Statement's 'attStmt' was of type '{state}' but '{CborReaderState.StartMap}' was expected."); 61 | } 62 | 63 | int? mapSize = cborReader.ReadStartMap(); 64 | if (mapSize is null) 65 | { 66 | throw new FormatException($"Attestation Statement's format had no keys."); 67 | } 68 | 69 | string? version = null; 70 | ulong? alg = null; 71 | byte[][]? x5c = null; 72 | byte[]? sig = null; 73 | byte[]? certInfo = null; 74 | byte[]? pubArea = null; 75 | 76 | for (int i = 0; i < mapSize; i++) 77 | { 78 | state = cborReader.PeekState(); 79 | if (state is not CborReaderState.TextString) 80 | { 81 | throw new FormatException($"Attestation Statement's format's key number '{i + 1}' was of type '{state}' but '{CborReaderState.TextString}' was expected."); 82 | } 83 | 84 | label = cborReader.ReadTextString(); 85 | switch (label) 86 | { 87 | case "ver": 88 | state = cborReader.PeekState(); 89 | if (state is not CborReaderState.TextString) 90 | { 91 | throw new FormatException($"Attestation Statement's format's 'ver' was of type '{state}' but '{CborReaderState.TextString}' was expected."); 92 | } 93 | version = cborReader.ReadTextString(); 94 | break; 95 | case "alg": 96 | state = cborReader.PeekState(); 97 | if (state is not CborReaderState.NegativeInteger) 98 | { 99 | throw new FormatException($"Attestation Statement's format's 'alg' was of type '{state}' but '{CborReaderState.NegativeInteger}' was expected."); 100 | } 101 | alg = cborReader.ReadCborNegativeIntegerRepresentation(); 102 | break; 103 | case "x5c": 104 | state = cborReader.PeekState(); 105 | if (state is not CborReaderState.StartArray) 106 | { 107 | throw new FormatException($"Attestation Statement's format's 'x5c' started with a '{state}' but '{CborReaderState.StartArray}' was expected."); 108 | } 109 | int? x5cLength = cborReader.ReadStartArray(); 110 | if (x5cLength is null) 111 | { 112 | throw new FormatException($"Attestation Statement's format's 'x5c' was empty, but a length of atleast 2 was expected."); 113 | } 114 | 115 | x5c = new byte[x5cLength.Value][]; 116 | for (int j = 0; j < x5cLength; j++) 117 | { 118 | x5c[j] = cborReader.ReadByteString(); 119 | } 120 | cborReader.ReadEndArray(); 121 | break; 122 | case "sig": 123 | state = cborReader.PeekState(); 124 | if (state is not CborReaderState.ByteString) 125 | { 126 | throw new FormatException($"Attestation Statement's format's 'sig' was of type '{state}' but '{CborReaderState.ByteString}' was expected."); 127 | } 128 | sig = cborReader.ReadByteString(); 129 | break; 130 | case "certInfo": 131 | state = cborReader.PeekState(); 132 | if (state is not CborReaderState.ByteString) 133 | { 134 | throw new FormatException($"Attestation Statement's format's 'certInfo' was of type '{state}' but '{CborReaderState.ByteString}' was expected."); 135 | } 136 | certInfo = cborReader.ReadByteString(); 137 | break; 138 | case "pubArea": 139 | state = cborReader.PeekState(); 140 | if (state is not CborReaderState.ByteString) 141 | { 142 | throw new FormatException($"Attestation Statement's format's 'pubArea' was of type '{state}' but '{CborReaderState.ByteString}' was expected."); 143 | } 144 | pubArea = cborReader.ReadByteString(); 145 | break; 146 | default: 147 | throw new FormatException($"Unsupported key '{label}' found in Attestation Statement's format."); 148 | } 149 | } 150 | 151 | if (version is null) 152 | { 153 | throw new FormatException("Expected a 'ver' in Attestation Statement's format, but key was not present."); 154 | } 155 | if (alg is null) 156 | { 157 | throw new FormatException("Expected a 'alg' in Attestation Statement's format, but key was not present."); 158 | } 159 | if (sig is null) 160 | { 161 | throw new FormatException("Expected a 'sig' in Attestation Statement's format, but key was not present."); 162 | } 163 | if (certInfo is null) 164 | { 165 | throw new FormatException("Expected a 'certInfo' in Attestation Statement's format, but key was not present."); 166 | } 167 | if (pubArea is null) 168 | { 169 | throw new FormatException("Expected a 'pubArea' in Attestation Statement's format, but key was not present."); 170 | } 171 | 172 | return new() 173 | { 174 | Version = version, 175 | Algorithm = (COSEAlgorithm)(-(long)alg - 1), 176 | X5c = x5c, 177 | Signature = sig, 178 | CertificateInformation = certInfo, 179 | PublicArea = pubArea, 180 | }; 181 | } 182 | 183 | public bool Verify(byte[] authenticatorData, byte[] clientDataHash) 184 | { 185 | byte[] attToBeSigned = authenticatorData.Concat(clientDataHash).ToArray(); 186 | 187 | byte[] magic = CertificateInformation[..4]; 188 | if (magic is not [0xff, 0x54, 0x43, 0x47]) // TPM_GENERATED_VALUE 189 | { 190 | return false; 191 | } 192 | 193 | byte[] type = CertificateInformation[4..6]; 194 | if (type is not [0x80, 0x17]) // TPM_ST_ATTEST_CERTIFY 195 | { 196 | return false; 197 | } 198 | 199 | byte[] qualifiedSignerSizeBuffer = CertificateInformation[6..8]; 200 | UInt16 qualifiedSignerSize = (UInt16)(qualifiedSignerSizeBuffer[0] * 256 + qualifiedSignerSizeBuffer[1]); 201 | 202 | byte[] extraDataSizeBuffer = CertificateInformation[(8 + qualifiedSignerSize)..(8 + qualifiedSignerSize + 2)]; 203 | UInt16 extraDataSize = (UInt16)(extraDataSizeBuffer[0] * 256 + extraDataSizeBuffer[1]); 204 | byte[] extraData = CertificateInformation[(8 + qualifiedSignerSize + 2)..(8 + qualifiedSignerSize + 2 + extraDataSize)]; 205 | 206 | var hasher = SHA1.Create(); 207 | byte[] hash = hasher.ComputeHash(attToBeSigned); 208 | 209 | if (!extraData.SequenceEqual(hash)) 210 | { 211 | return false; 212 | } 213 | 214 | int clockInfoSize = 8 + 4 + 4 + 1; 215 | 216 | int firmwareVersionSize = 8; 217 | 218 | byte[] attestedBuffer = CertificateInformation[(8 + qualifiedSignerSize + 2 + extraDataSize + clockInfoSize + firmwareVersionSize)..]; 219 | 220 | byte[] tpmsCertifyInfoNameSizeBuffer = attestedBuffer[..2]; 221 | UInt16 tpmsCertifyInfoNameSize = (UInt16)(tpmsCertifyInfoNameSizeBuffer[0] * 256 + tpmsCertifyInfoNameSizeBuffer[1]); 222 | byte[] tpmsCertifyInfoName = attestedBuffer[2..(2 + tpmsCertifyInfoNameSize)]; 223 | 224 | byte[] tpmsCertifyInfoQualifiedNameSizeBuffer = attestedBuffer[(2 + tpmsCertifyInfoNameSize)..(2 + tpmsCertifyInfoNameSize + 2)]; 225 | UInt16 tpmsCertifyInfoQualifiedNameSize = (UInt16)(tpmsCertifyInfoQualifiedNameSizeBuffer[0] * 256 + tpmsCertifyInfoQualifiedNameSizeBuffer[1]); 226 | 227 | byte[] pubAreaNameAlg = PublicArea[2..4]; 228 | 229 | byte[]? name; 230 | switch (pubAreaNameAlg) 231 | { 232 | case [0x0, 0xB]: 233 | name = pubAreaNameAlg.Concat(SHA256.HashData(PublicArea)).ToArray(); 234 | break; 235 | default: 236 | return false; 237 | } 238 | 239 | return name.SequenceEqual(tpmsCertifyInfoName); 240 | } 241 | 242 | //TPMI_ST_ATTEST type = typeBytes switch 243 | //{ 244 | //[0x80, 0x14] => TPMI_ST_ATTEST.TPM_ST_ATTEST_NV, 245 | //[0x80, 0x15] => TPMI_ST_ATTEST.TPM_ST_ATTEST_COMMAND_AUDIT, 246 | //[0x80, 0x16] => TPMI_ST_ATTEST.TPM_ST_ATTEST_SESSION_AUDIT, 247 | //[0x80, 0x17] => TPMI_ST_ATTEST.TPM_ST_ATTEST_CERTIFY, 248 | //[0x80, 0x18] => TPMI_ST_ATTEST.TPM_ST_ATTEST_QUOTE, 249 | //[0x80, 0x19] => TPMI_ST_ATTEST.TPM_ST_ATTEST_TIME, 250 | //[0x80, 0x1a] => TPMI_ST_ATTEST.TPM_ST_ATTEST_CREATION, 251 | // _ => TPMI_ST_ATTEST.Invalid, 252 | //}; 253 | /// 254 | /// From [TPMv2-Part2] section 10.12.6. 255 | /// 256 | //internal enum TPMI_ST_ATTEST 257 | //{ 258 | // /// 259 | // /// Generated by TPM2_Certify() 260 | // /// 261 | // TPM_ST_ATTEST_CERTIFY, 262 | 263 | // /// 264 | // /// Generated by TPM2_Quote() 265 | // /// 266 | // TPM_ST_ATTEST_QUOTE, 267 | 268 | // /// 269 | // /// Generated by TPM2_GetSessionAuditDigest() 270 | // /// 271 | // TPM_ST_ATTEST_SESSION_AUDIT, 272 | 273 | // /// 274 | // /// Generated by TPM2_GetCommandAuditDigest() 275 | // /// 276 | // TPM_ST_ATTEST_COMMAND_AUDIT, 277 | 278 | // /// 279 | // /// Generated by TPM2_GetTime() 280 | // /// 281 | // TPM_ST_ATTEST_TIME, 282 | 283 | // /// 284 | // /// Generated by TPM2_CertifyCreation() 285 | // /// 286 | // TPM_ST_ATTEST_CREATION, 287 | 288 | // /// 289 | // /// Generated by TPM2_NV_Certify() 290 | // /// 291 | // TPM_ST_ATTEST_NV, 292 | 293 | // /// 294 | // /// Invalid 295 | // /// 296 | // Invalid, 297 | //} 298 | } 299 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/AuthenticatorAssertionResponse.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebIDL; 2 | using Microsoft.JSInterop; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication; 5 | 6 | public class AuthenticatorAssertionResponse : AuthenticatorResponse, IJSCreatable 7 | { 8 | public static Task CreateAsync(IJSRuntime jSRuntime, IJSObjectReference jSReference) 9 | { 10 | return Task.FromResult(new(jSRuntime, jSReference)); 11 | } 12 | 13 | public AuthenticatorAssertionResponse(IJSRuntime jSRuntime, IJSObjectReference jSReference) : base(jSRuntime, jSReference) { } 14 | 15 | public async Task GetAuthenticatorDataAsync() 16 | { 17 | IJSObjectReference helper = await webAuthenticationHelperTask.Value; 18 | return await helper.InvokeAsync("getAttribute", JSReference, "authenticatorData"); 19 | } 20 | 21 | public async Task GetAuthenticatorDataAsArrayAsync() 22 | { 23 | IJSObjectReference helper = await webAuthenticationHelperTask.Value; 24 | IJSObjectReference arrayBuffer = await helper.InvokeAsync("getAttribute", JSReference, "authenticatorData"); 25 | 26 | IJSObjectReference webIDLHelper = await JSRuntime.InvokeAsync("import", "./_content/KristofferStrube.Blazor.WebIDL/KristofferStrube.Blazor.WebIDL.js"); 27 | IJSObjectReference uint8ArrayFromBuffer = await webIDLHelper.InvokeAsync("constructUint8Array", arrayBuffer); 28 | Uint8Array uint8Array = await Uint8Array.CreateAsync(JSRuntime, uint8ArrayFromBuffer); 29 | return await uint8Array.GetByteArrayAsync(); 30 | } 31 | 32 | public async Task GetSignatureAsync() 33 | { 34 | IJSObjectReference helper = await webAuthenticationHelperTask.Value; 35 | return await helper.InvokeAsync("getAttribute", JSReference, "signature"); 36 | } 37 | 38 | public async Task GetSignatureAsArrayAsync() 39 | { 40 | IJSObjectReference helper = await webAuthenticationHelperTask.Value; 41 | IJSObjectReference arrayBuffer = await helper.InvokeAsync("getAttribute", JSReference, "signature"); 42 | 43 | IJSObjectReference webIDLHelper = await JSRuntime.InvokeAsync("import", "./_content/KristofferStrube.Blazor.WebIDL/KristofferStrube.Blazor.WebIDL.js"); 44 | IJSObjectReference uint8ArrayFromBuffer = await webIDLHelper.InvokeAsync("constructUint8Array", arrayBuffer); 45 | Uint8Array uint8Array = await Uint8Array.CreateAsync(JSRuntime, uint8ArrayFromBuffer); 46 | return await uint8Array.GetByteArrayAsync(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/AuthenticatorAttestationResponse.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebIDL; 2 | using Microsoft.JSInterop; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication; 5 | 6 | public class AuthenticatorAttestationResponse : AuthenticatorResponse, IJSCreatable 7 | { 8 | public static Task CreateAsync(IJSRuntime jSRuntime, IJSObjectReference jSReference) 9 | { 10 | return Task.FromResult(new(jSRuntime, jSReference)); 11 | } 12 | 13 | public AuthenticatorAttestationResponse(IJSRuntime jSRuntime, IJSObjectReference jSReference) : base(jSRuntime, jSReference) { } 14 | 15 | public async Task GetAttestationObjectAsync() 16 | { 17 | IJSObjectReference helper = await webAuthenticationHelperTask.Value; 18 | IJSObjectReference arrayBuffer = await helper.InvokeAsync("getAttribute", JSReference, "attestationObject"); 19 | 20 | IJSObjectReference webIDLHelper = await JSRuntime.InvokeAsync("import", "./_content/KristofferStrube.Blazor.WebIDL/KristofferStrube.Blazor.WebIDL.js"); 21 | IJSObjectReference uint8ArrayFromBuffer = await webIDLHelper.InvokeAsync("constructUint8Array", arrayBuffer); 22 | Uint8Array uint8Array = await Uint8Array.CreateAsync(JSRuntime, uint8ArrayFromBuffer); 23 | return await uint8Array.GetByteArrayAsync(); 24 | } 25 | 26 | /// 27 | /// These values are the transports that the authenticator is believed to support, or an empty sequence if the information is unavailable. 28 | /// 29 | public async Task GetTransportsAsync() 30 | { 31 | return await JSReference.InvokeAsync("getTransports"); 32 | } 33 | 34 | public async Task GetAuthenticatorDataAsync() 35 | { 36 | return await JSReference.InvokeAsync("getAuthenticatorData"); 37 | } 38 | 39 | public async Task GetAuthenticatorDataAsArrayAsync() 40 | { 41 | IJSObjectReference arrayBuffer = await JSReference.InvokeAsync("getAuthenticatorData"); 42 | 43 | IJSObjectReference webIDLHelper = await JSRuntime.InvokeAsync("import", "./_content/KristofferStrube.Blazor.WebIDL/KristofferStrube.Blazor.WebIDL.js"); 44 | IJSObjectReference uint8ArrayFromBuffer = await webIDLHelper.InvokeAsync("constructUint8Array", arrayBuffer); 45 | Uint8Array uint8Array = await Uint8Array.CreateAsync(JSRuntime, uint8ArrayFromBuffer); 46 | return await uint8Array.GetByteArrayAsync(); 47 | } 48 | 49 | public async Task GetPublicKeyAsync() 50 | { 51 | return await JSReference.InvokeAsync("getPublicKey"); 52 | } 53 | 54 | public async Task GetPublicKeyAsArrayAsync() 55 | { 56 | IJSObjectReference arrayBuffer = await JSReference.InvokeAsync("getPublicKey"); 57 | 58 | IJSObjectReference webIDLHelper = await JSRuntime.InvokeAsync("import", "./_content/KristofferStrube.Blazor.WebIDL/KristofferStrube.Blazor.WebIDL.js"); 59 | IJSObjectReference uint8ArrayFromBuffer = await webIDLHelper.InvokeAsync("constructUint8Array", arrayBuffer); 60 | Uint8Array uint8Array = await Uint8Array.CreateAsync(JSRuntime, uint8ArrayFromBuffer); 61 | return await uint8Array.GetByteArrayAsync(); 62 | } 63 | 64 | public async Task GetPublicKeyAlgorithmAsync() 65 | { 66 | return await JSReference.InvokeAsync("getPublicKeyAlgorithm"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/AuthenticatorResponse.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebAuthentication.Extensions; 2 | using KristofferStrube.Blazor.WebIDL; 3 | using Microsoft.JSInterop; 4 | 5 | namespace KristofferStrube.Blazor.WebAuthentication; 6 | 7 | /// 8 | /// Authenticators respond to Relying Party requests by returning an object derived from the interface. 9 | /// 10 | /// See the API definition here. 11 | public abstract class AuthenticatorResponse : IJSWrapper 12 | { 13 | protected readonly Lazy> webAuthenticationHelperTask; 14 | 15 | public IJSRuntime JSRuntime { get; set; } 16 | public IJSObjectReference JSReference { get; set; } 17 | 18 | public static async Task GetConcreteInstanceAsync(ValueReference authenticatorResponse) 19 | { 20 | authenticatorResponse.ValueMapper = new() 21 | { 22 | { "authenticatorattestationresponse", async () => await AuthenticatorAttestationResponse.CreateAsync(authenticatorResponse.JSRuntime, await authenticatorResponse.GetValueAsync()) }, 23 | { "authenticatorassertionresponse", async () => await AuthenticatorAssertionResponse.CreateAsync(authenticatorResponse.JSRuntime, await authenticatorResponse.GetValueAsync()) } 24 | }; 25 | 26 | object? result = await authenticatorResponse.GetValueAsync(); 27 | return result is IJSObjectReference or null ? null! : (AuthenticatorResponse)result; 28 | } 29 | 30 | protected AuthenticatorResponse(IJSRuntime jSRuntime, IJSObjectReference jSReference) 31 | { 32 | JSRuntime = jSRuntime; 33 | JSReference = jSReference; 34 | webAuthenticationHelperTask = new(jSRuntime.GetHelperAsync); 35 | } 36 | public async Task GetClientDataJSONAsync() 37 | { 38 | IJSObjectReference helper = await webAuthenticationHelperTask.Value; 39 | return await helper.InvokeAsync("getAttribute", JSReference, "clientDataJSON"); 40 | } 41 | public async Task GetClientDataJSONAsArrayAsync() 42 | { 43 | IJSObjectReference helper = await webAuthenticationHelperTask.Value; 44 | IJSObjectReference arrayBuffer = await helper.InvokeAsync("getAttribute", JSReference, "clientDataJSON"); 45 | 46 | IJSObjectReference webIDLHelper = await JSRuntime.InvokeAsync("import", "./_content/KristofferStrube.Blazor.WebIDL/KristofferStrube.Blazor.WebIDL.js"); 47 | IJSObjectReference uint8ArrayFromBuffer = await webIDLHelper.InvokeAsync("constructUint8Array", arrayBuffer); 48 | Uint8Array uint8Array = await Uint8Array.CreateAsync(JSRuntime, uint8ArrayFromBuffer); 49 | return await uint8Array.GetByteArrayAsync(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/COSEAlgorithms.cs: -------------------------------------------------------------------------------- 1 | namespace KristofferStrube.Blazor.WebAuthentication; 2 | 3 | /// 4 | /// A COSEAlgorithmIdentifier's value is a number identifying a cryptographic algorithm. 5 | /// 6 | public enum COSEAlgorithm : long 7 | { 8 | /// 9 | /// RSASSA-PKCS1-v1_5 using SHA-1
10 | /// This is not recommended to use. 11 | ///
12 | /// 13 | /// See the reference for RFC8812.
14 | /// See the reference for RFC9053.
15 | ///
16 | RS1 = -65535, 17 | 18 | /// 19 | /// AES-CTR w/ 128-bit key
20 | /// This is not recommended to use. 21 | ///
22 | /// 23 | /// See the reference for RFC9459.
24 | ///
25 | A128CTR = -65534, 26 | 27 | /// 28 | /// AES-CTR w/ 192-bit key
29 | /// This is not recommended to use. 30 | ///
31 | /// 32 | /// See the reference for RFC9459.
33 | ///
34 | A192CTR = -65533, 35 | 36 | /// 37 | /// AES-CTR w/ 256-bit key
38 | /// This is not recommended to use. 39 | ///
40 | /// 41 | /// See the reference for RFC9459.
42 | ///
43 | A256CTR = -65532, 44 | 45 | /// 46 | /// AES-CBC w/ 128-bit key
47 | /// This is not recommended to use. 48 | ///
49 | /// 50 | /// See the reference for RFC9459.
51 | ///
52 | A128CBC = -65531, 53 | 54 | /// 55 | /// AES-CBC w/ 192-bit key
56 | /// This is not recommended to use. 57 | ///
58 | /// 59 | /// See the reference for RFC9459.
60 | ///
61 | A192CBC = -65530, 62 | 63 | /// 64 | /// AES-CBC w/ 256-bit key
65 | /// This is not recommended to use. 66 | ///
67 | /// 68 | /// See the reference for RFC9459.
69 | ///
70 | A256CBC = -65529, 71 | 72 | /// 73 | /// WalnutDSA signature
74 | /// This is not recommended to use. 75 | ///
76 | /// 77 | /// See the reference for RFC9021.
78 | /// See the reference for RFC9053.
79 | ///
80 | WalnutDSA = -260, 81 | 82 | /// 83 | /// RSASSA-PKCS1-v1_5 using SHA-512
84 | /// This is not recommended to use. 85 | ///
86 | /// 87 | /// See the reference for RFC8812.
88 | /// See the reference for RFC9053.
89 | ///
90 | RS512 = -259, 91 | 92 | /// 93 | /// RSASSA-PKCS1-v1_5 using SHA-384
94 | /// This is not recommended to use. 95 | ///
96 | /// 97 | /// See the reference for RFC8812.
98 | /// See the reference for RFC9053.
99 | ///
100 | RS384 = -258, 101 | 102 | /// 103 | /// RSASSA-PKCS1-v1_5 using SHA-256
104 | /// This is not recommended to use. 105 | ///
106 | /// 107 | /// See the reference for RFC8812.
108 | /// See the reference for RFC9053.
109 | ///
110 | RS256 = -257, 111 | 112 | /// 113 | /// ECDSA using secp256k1 curve and SHA-256
114 | /// This is not recommended to use. 115 | ///
116 | /// 117 | /// See the reference for RFC8812.
118 | /// See the reference for RFC9053.
119 | ///
120 | ES256K = -47, 121 | 122 | /// 123 | /// HSS/LMS hash-based digital signature
124 | /// This is recommended to use. 125 | ///
126 | /// 127 | /// See the reference for RFC8778.
128 | /// See the reference for RFC9053.
129 | ///
130 | HSS_LMS = -46, 131 | 132 | /// 133 | /// SHAKE-256 512-bit Hash Value
134 | /// This is recommended to use. 135 | ///
136 | /// 137 | /// See the reference for RFC9054.
138 | /// See the reference for RFC9053.
139 | ///
140 | SHAKE256 = -45, 141 | 142 | /// 143 | /// SHA-2 512-bit Hash
144 | /// This is recommended to use. 145 | ///
146 | /// 147 | /// See the reference for RFC9054.
148 | /// See the reference for RFC9053.
149 | ///
150 | SHA_512 = -44, 151 | 152 | /// 153 | /// SHA-2 384-bit Hash
154 | /// This is recommended to use. 155 | ///
156 | /// 157 | /// See the reference for RFC9054.
158 | /// See the reference for RFC9053.
159 | ///
160 | SHA_384 = -43, 161 | 162 | /// 163 | /// RSAES-OAEP w/ SHA-512
164 | /// This is recommended to use. 165 | ///
166 | /// 167 | /// See the reference for RFC8230.
168 | /// See the reference for RFC9053.
169 | ///
170 | RSAES_OAEP_SHA_512 = -42, 171 | 172 | /// 173 | /// RSAES-OAEP w/ SHA-256
174 | /// This is recommended to use. 175 | ///
176 | /// 177 | /// See the reference for RFC8230.
178 | /// See the reference for RFC9053.
179 | ///
180 | RSAES_OAEP_SHA_256 = -41, 181 | 182 | /// 183 | /// RSAES-OAEP w/ SHA-1
184 | /// This is recommended to use. 185 | ///
186 | /// 187 | /// See the reference for RFC8230.
188 | /// See the reference for RFC9053.
189 | ///
190 | RSAES_OAEP_RFC_8017_default_parameters = -40, 191 | 192 | /// 193 | /// RSASSA-PSS w/ SHA-512
194 | /// This is recommended to use. 195 | ///
196 | /// 197 | /// See the reference for RFC8230.
198 | /// See the reference for RFC9053.
199 | ///
200 | PS512 = -39, 201 | 202 | /// 203 | /// RSASSA-PSS w/ SHA-384
204 | /// This is recommended to use. 205 | ///
206 | /// 207 | /// See the reference for RFC8230.
208 | /// See the reference for RFC9053.
209 | ///
210 | PS384 = -38, 211 | 212 | /// 213 | /// RSASSA-PSS w/ SHA-256
214 | /// This is recommended to use. 215 | ///
216 | /// 217 | /// See the reference for RFC8230.
218 | /// See the reference for RFC9053.
219 | ///
220 | PS256 = -37, 221 | 222 | /// 223 | /// ECDSA w/ SHA-512
224 | /// This is recommended to use. 225 | ///
226 | /// 227 | /// See the reference for RFC9053.
228 | ///
229 | ES512 = -36, 230 | 231 | /// 232 | /// ECDSA w/ SHA-384
233 | /// This is recommended to use. 234 | ///
235 | /// 236 | /// See the reference for RFC9053.
237 | ///
238 | ES384 = -35, 239 | 240 | /// 241 | /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 256-bit key
242 | /// This is recommended to use. 243 | ///
244 | /// 245 | /// See the reference for RFC9053.
246 | ///
247 | ECDH_SS_and_A256KW = -34, 248 | 249 | /// 250 | /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 192-bit key
251 | /// This is recommended to use. 252 | ///
253 | /// 254 | /// See the reference for RFC9053.
255 | ///
256 | ECDH_SS_and_A192KW = -33, 257 | 258 | /// 259 | /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 128-bit key
260 | /// This is recommended to use. 261 | ///
262 | /// 263 | /// See the reference for RFC9053.
264 | ///
265 | ECDH_SS_and_A128KW = -32, 266 | 267 | /// 268 | /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 256-bit key
269 | /// This is recommended to use. 270 | ///
271 | /// 272 | /// See the reference for RFC9053.
273 | ///
274 | ECDH_ES_and_A256KW = -31, 275 | 276 | /// 277 | /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 192-bit key
278 | /// This is recommended to use. 279 | ///
280 | /// 281 | /// See the reference for RFC9053.
282 | ///
283 | ECDH_ES_and_A192KW = -30, 284 | 285 | /// 286 | /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 128-bit key
287 | /// This is recommended to use. 288 | ///
289 | /// 290 | /// See the reference for RFC9053.
291 | ///
292 | ECDH_ES_and_A128KW = -29, 293 | 294 | /// 295 | /// ECDH SS w/ HKDF - generate key directly
296 | /// This is recommended to use. 297 | ///
298 | /// 299 | /// See the reference for RFC9053.
300 | ///
301 | ECDH_SS_and_HKDF_512 = -28, 302 | 303 | /// 304 | /// ECDH SS w/ HKDF - generate key directly
305 | /// This is recommended to use. 306 | ///
307 | /// 308 | /// See the reference for RFC9053.
309 | ///
310 | ECDH_SS_and_HKDF_256 = -27, 311 | 312 | /// 313 | /// ECDH ES w/ HKDF - generate key directly
314 | /// This is recommended to use. 315 | ///
316 | /// 317 | /// See the reference for RFC9053.
318 | ///
319 | ECDH_ES_and_HKDF_512 = -26, 320 | 321 | /// 322 | /// ECDH ES w/ HKDF - generate key directly
323 | /// This is recommended to use. 324 | ///
325 | /// 326 | /// See the reference for RFC9053.
327 | ///
328 | ECDH_ES_and_HKDF_256 = -25, 329 | 330 | /// 331 | /// SHAKE-128 256-bit Hash Value
332 | /// This is recommended to use. 333 | ///
334 | /// 335 | /// See the reference for RFC9054.
336 | /// See the reference for RFC9053.
337 | ///
338 | SHAKE128 = -18, 339 | 340 | /// 341 | /// SHA-2 512-bit Hash truncated to 256-bits
342 | /// This is recommended to use. 343 | ///
344 | /// 345 | /// See the reference for RFC9054.
346 | /// See the reference for RFC9053.
347 | ///
348 | SHA_512_truncated_to_256 = -17, 349 | 350 | /// 351 | /// SHA-2 256-bit Hash
352 | /// This is recommended to use. 353 | ///
354 | /// 355 | /// See the reference for RFC9054.
356 | /// See the reference for RFC9053.
357 | ///
358 | SHA_256 = -16, 359 | 360 | /// 361 | /// SHA-2 256-bit Hash truncated to 64-bits
362 | /// This is not recommended to use. 363 | ///
364 | /// 365 | /// See the reference for RFC9054.
366 | /// See the reference for RFC9053.
367 | ///
368 | SHA_256_truncated_to_64 = -15, 369 | 370 | /// 371 | /// SHA-1 Hash
372 | /// This is not recommended to use. 373 | ///
374 | /// 375 | /// See the reference for RFC9054.
376 | /// See the reference for RFC9053.
377 | ///
378 | SHA_1 = -14, 379 | 380 | /// 381 | /// Shared secret w/ AES-MAC 256-bit key
382 | /// This is recommended to use. 383 | ///
384 | /// 385 | /// See the reference for RFC9053.
386 | ///
387 | directandHKDF_AES_256 = -13, 388 | 389 | /// 390 | /// Shared secret w/ AES-MAC 128-bit key
391 | /// This is recommended to use. 392 | ///
393 | /// 394 | /// See the reference for RFC9053.
395 | ///
396 | directandHKDF_AES_128 = -12, 397 | 398 | /// 399 | /// Shared secret w/ HKDF and SHA-512
400 | /// This is recommended to use. 401 | ///
402 | /// 403 | /// See the reference for RFC9053.
404 | ///
405 | directandHKDF_SHA_512 = -11, 406 | 407 | /// 408 | /// Shared secret w/ HKDF and SHA-256
409 | /// This is recommended to use. 410 | ///
411 | /// 412 | /// See the reference for RFC9053.
413 | ///
414 | directandHKDF_SHA_256 = -10, 415 | 416 | /// 417 | /// EdDSA
418 | /// This is recommended to use. 419 | ///
420 | /// 421 | /// See the reference for RFC9053.
422 | ///
423 | EdDSA = -8, 424 | 425 | /// 426 | /// ECDSA w/ SHA-256
427 | /// This is recommended to use. 428 | ///
429 | /// 430 | /// See the reference for RFC9053.
431 | ///
432 | ES256 = -7, 433 | 434 | /// 435 | /// Direct use of CEK
436 | /// This is recommended to use. 437 | ///
438 | /// 439 | /// See the reference for RFC9053.
440 | ///
441 | direct = -6, 442 | 443 | /// 444 | /// AES Key Wrap w/ 256-bit key
445 | /// This is recommended to use. 446 | ///
447 | /// 448 | /// See the reference for RFC9053.
449 | ///
450 | A256KW = -5, 451 | 452 | /// 453 | /// AES Key Wrap w/ 192-bit key
454 | /// This is recommended to use. 455 | ///
456 | /// 457 | /// See the reference for RFC9053.
458 | ///
459 | A192KW = -4, 460 | 461 | /// 462 | /// AES Key Wrap w/ 128-bit key
463 | /// This is recommended to use. 464 | ///
465 | /// 466 | /// See the reference for RFC9053.
467 | ///
468 | A128KW = -3, 469 | 470 | /// 471 | ///
472 | /// This is not recommended to use. 473 | ///
474 | /// 475 | /// See the reference for RFC9053.
476 | ///
477 | Reserved = 0, 478 | 479 | /// 480 | /// "AES-GCM mode w/ 128-bit key
481 | /// This is not recommended to use. 482 | ///
483 | A128GCM = 1, 484 | 485 | /// 486 | /// "AES-GCM mode w/ 192-bit key
487 | /// This is not recommended to use. 488 | ///
489 | A192GCM = 2, 490 | 491 | /// 492 | /// "AES-GCM mode w/ 256-bit key
493 | /// This is not recommended to use. 494 | ///
495 | A256GCM = 3, 496 | 497 | /// 498 | /// HMAC w/ SHA-256 truncated to 64 bits
499 | /// This is recommended to use. 500 | ///
501 | /// 502 | /// See the reference for RFC9053.
503 | ///
504 | HMAC_256_truncated_to_64 = 4, 505 | 506 | /// 507 | /// HMAC w/ SHA-256
508 | /// This is recommended to use. 509 | ///
510 | /// 511 | /// See the reference for RFC9053.
512 | ///
513 | HMAC_256_truncated_to_256 = 5, 514 | 515 | /// 516 | /// HMAC w/ SHA-384
517 | /// This is recommended to use. 518 | ///
519 | /// 520 | /// See the reference for RFC9053.
521 | ///
522 | HMAC_384_truncated_to_384 = 6, 523 | 524 | /// 525 | /// HMAC w/ SHA-512
526 | /// This is recommended to use. 527 | ///
528 | /// 529 | /// See the reference for RFC9053.
530 | ///
531 | HMAC_512_truncated_to_512 = 7, 532 | 533 | /// 534 | /// "AES-CCM mode 128-bit key
535 | /// This is not recommended to use. 536 | ///
537 | /// 538 | /// See the reference for kty.
539 | ///
540 | AES_CCM_16_64_128 = 10, 541 | 542 | /// 543 | /// "AES-CCM mode 256-bit key
544 | /// This is not recommended to use. 545 | ///
546 | /// 547 | /// See the reference for kty.
548 | ///
549 | AES_CCM_16_64_256 = 11, 550 | 551 | /// 552 | /// "AES-CCM mode 128-bit key
553 | /// This is not recommended to use. 554 | ///
555 | /// 556 | /// See the reference for kty.
557 | ///
558 | AES_CCM_64_64_128 = 12, 559 | 560 | /// 561 | /// "AES-CCM mode 256-bit key
562 | /// This is not recommended to use. 563 | ///
564 | /// 565 | /// See the reference for kty.
566 | ///
567 | AES_CCM_64_64_256 = 13, 568 | 569 | /// 570 | /// "AES-MAC 128-bit key
571 | /// This is not recommended to use. 572 | ///
573 | AES_MAC_128_truncated_to_64 = 14, 574 | 575 | /// 576 | /// "AES-MAC 256-bit key
577 | /// This is not recommended to use. 578 | ///
579 | AES_MAC_256_truncated_to_64 = 15, 580 | 581 | /// 582 | /// "ChaCha20/Poly1305 w/ 256-bit key
583 | /// This is not recommended to use. 584 | ///
585 | ChaCha20_truncated_to_Poly1305 = 24, 586 | 587 | /// 588 | /// "AES-MAC 128-bit key
589 | /// This is not recommended to use. 590 | ///
591 | AES_MAC_128_truncated_to_128 = 25, 592 | 593 | /// 594 | /// "AES-MAC 256-bit key
595 | /// This is not recommended to use. 596 | ///
597 | AES_MAC_256_truncated_to_128 = 26, 598 | 599 | /// 600 | /// "AES-CCM mode 128-bit key
601 | /// This is not recommended to use. 602 | ///
603 | /// 604 | /// See the reference for kty.
605 | ///
606 | AES_CCM_16_128_128 = 30, 607 | 608 | /// 609 | /// "AES-CCM mode 256-bit key
610 | /// This is not recommended to use. 611 | ///
612 | /// 613 | /// See the reference for kty.
614 | ///
615 | AES_CCM_16_128_256 = 31, 616 | 617 | /// 618 | /// "AES-CCM mode 128-bit key
619 | /// This is not recommended to use. 620 | ///
621 | /// 622 | /// See the reference for kty.
623 | ///
624 | AES_CCM_64_128_128 = 32, 625 | 626 | /// 627 | /// "AES-CCM mode 256-bit key
628 | /// This is not recommended to use. 629 | ///
630 | /// 631 | /// See the reference for kty.
632 | ///
633 | AES_CCM_64_128_256 = 33, 634 | 635 | /// 636 | /// For doing IV generation for symmetric algorithms.
637 | /// This is not recommended to use. 638 | ///
639 | /// 640 | /// See the reference for RFC9053.
641 | ///
642 | IV_GENERATION = 34, 643 | 644 | } 645 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/CollectedClientData.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | public class CollectedClientData 6 | { 7 | [JsonPropertyName("type")] 8 | public required string Type { get; set; } 9 | 10 | [JsonPropertyName("challenge")] 11 | public required string Challenge { get; set; } 12 | 13 | [JsonPropertyName("origin")] 14 | public required string Origin { get; set; } 15 | 16 | [JsonPropertyName("topOrigin")] 17 | public string? TopOrigin { get; set; } 18 | 19 | [JsonPropertyName("crossOrigin")] 20 | public bool CrossOrigin { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Converters/AttestationConveyancePreferenceConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication.Converters; 5 | 6 | public class AttestationConveyancePreferenceConverter : JsonConverter 7 | { 8 | public override AttestationConveyancePreference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | return reader.GetString() switch 11 | { 12 | "none" => AttestationConveyancePreference.None, 13 | "indirect" => AttestationConveyancePreference.Indirect, 14 | "direct" => AttestationConveyancePreference.Direct, 15 | "enterprise" => AttestationConveyancePreference.Enterprise, 16 | var value => throw new ArgumentException($"Value '{value}' was not a valid {nameof(PublicKeyCredentialType)}.") 17 | }; 18 | } 19 | 20 | public override void Write(Utf8JsonWriter writer, AttestationConveyancePreference value, JsonSerializerOptions options) 21 | { 22 | writer.WriteStringValue(value switch 23 | { 24 | AttestationConveyancePreference.None => "none", 25 | AttestationConveyancePreference.Indirect => "indirect", 26 | AttestationConveyancePreference.Direct => "direct", 27 | AttestationConveyancePreference.Enterprise => "enterprise", 28 | _ => throw new ArgumentException($"Value '{value}' was not a valid {nameof(PublicKeyCredentialType)}.") 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Converters/AttestationFormatConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication.Converters; 5 | 6 | public class AttestationFormatConverter : JsonConverter 7 | { 8 | public override AttestationFormat Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | return reader.GetString() switch 11 | { 12 | "packed" => AttestationFormat.Packed, 13 | "tpm" => AttestationFormat.TPM, 14 | "android-key" => AttestationFormat.AndroidKey, 15 | "android-safetynet" => AttestationFormat.AndroidSafetyNet, 16 | "fido-u2f" => AttestationFormat.FidoU2F, 17 | "apple" => AttestationFormat.Apple, 18 | "none" => AttestationFormat.None, 19 | var value => throw new ArgumentException($"Value '{value}' was not a valid {nameof(AttestationFormat)}.") 20 | }; 21 | } 22 | 23 | public override void Write(Utf8JsonWriter writer, AttestationFormat value, JsonSerializerOptions options) 24 | { 25 | writer.WriteStringValue(value switch 26 | { 27 | AttestationFormat.Packed => "packed", 28 | AttestationFormat.TPM => "tpm", 29 | AttestationFormat.AndroidKey => "android-key", 30 | AttestationFormat.AndroidSafetyNet => "android-safetynet", 31 | AttestationFormat.FidoU2F => "fido-u2f", 32 | AttestationFormat.Apple => "apple", 33 | AttestationFormat.None => "none", 34 | _ => throw new ArgumentException($"Value '{value}' was not a valid {nameof(AttestationFormat)}.") 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Converters/AuthenticatorTransportConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication.Converters; 5 | 6 | public class AuthenticatorTransportConverter : JsonConverter 7 | { 8 | public override AuthenticatorTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | return reader.GetString() switch 11 | { 12 | "usb" => AuthenticatorTransport.Usb, 13 | "nfc" => AuthenticatorTransport.Nfc, 14 | "ble" => AuthenticatorTransport.Ble, 15 | "smard-card" => AuthenticatorTransport.SmartCard, 16 | "hybrid" => AuthenticatorTransport.Hybrid, 17 | "internal" => AuthenticatorTransport.Internal, 18 | var value => throw new ArgumentException($"Value '{value}' was not a valid {nameof(AuthenticatorTransport)}.") 19 | }; 20 | } 21 | 22 | public override void Write(Utf8JsonWriter writer, AuthenticatorTransport value, JsonSerializerOptions options) 23 | { 24 | writer.WriteStringValue(value switch 25 | { 26 | AuthenticatorTransport.Usb => "usb", 27 | AuthenticatorTransport.Nfc => "nfc", 28 | AuthenticatorTransport.Ble => "ble", 29 | AuthenticatorTransport.SmartCard => "smard-card", 30 | AuthenticatorTransport.Hybrid => "hybrid", 31 | AuthenticatorTransport.Internal => "internal", 32 | _ => throw new ArgumentException($"Value '{value}' was not a valid {nameof(AuthenticatorTransport)}.") 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Converters/PublicKeyCredentialTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication.Converters; 5 | 6 | public class PublicKeyCredentialTypeConverter : JsonConverter 7 | { 8 | public override PublicKeyCredentialType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | throw new NotImplementedException(); 11 | } 12 | 13 | public override void Write(Utf8JsonWriter writer, PublicKeyCredentialType value, JsonSerializerOptions options) 14 | { 15 | writer.WriteStringValue(value switch 16 | { 17 | PublicKeyCredentialType.PublicKey => "public-key", 18 | _ => throw new ArgumentException($"Value '{value}' was not a valid {nameof(PublicKeyCredentialType)}.") 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Converters/UserVerificationRequirementConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication.Converters; 5 | 6 | public class UserVerificationRequirementConverter : JsonConverter 7 | { 8 | public override UserVerificationRequirement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | throw new NotImplementedException(); 11 | } 12 | 13 | public override void Write(Utf8JsonWriter writer, UserVerificationRequirement value, JsonSerializerOptions options) 14 | { 15 | writer.WriteStringValue(value switch 16 | { 17 | UserVerificationRequirement.Required => "required", 18 | UserVerificationRequirement.Preferred => "preferred", 19 | UserVerificationRequirement.Discouraged => "discouraged", 20 | _ => throw new ArgumentException($"Value '{value}' was not a valid {nameof(UserVerificationRequirement)}.") 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Extensions/IJSRuntimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication.Extensions; 4 | 5 | internal static class IJSRuntimeExtensions 6 | { 7 | internal static async Task GetHelperAsync(this IJSRuntime jSRuntime) 8 | { 9 | return await jSRuntime.InvokeAsync( 10 | "import", "./_content/KristofferStrube.Blazor.WebAuthentication/KristofferStrube.Blazor.WebAuthentication.js"); 11 | } 12 | internal static async Task GetInProcessHelperAsync(this IJSRuntime jSRuntime) 13 | { 14 | return await jSRuntime.InvokeAsync( 15 | "import", "./_content/KristofferStrube.Blazor.WebAuthentication/KristofferStrube.Blazor.WebAuthentication.js"); 16 | } 17 | } -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/JSONRepresentations/AuthenticationExtensionsClientOutputsJSON.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication.JSONRepresentations; 5 | 6 | public class AuthenticationExtensionsClientOutputsJSON 7 | { 8 | [JsonExtensionData] 9 | public Dictionary? ExtensionData { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/JSONRepresentations/AuthenticationResponseJSON.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication.JSONRepresentations; 4 | 5 | public class AuthenticationResponseJSON : PublicKeyCredentialJSON 6 | { 7 | [JsonPropertyName("id")] 8 | public required string Id { get; set; } 9 | 10 | [JsonPropertyName("rawId")] 11 | public required string RawId { get; set; } 12 | 13 | [JsonPropertyName("response")] 14 | public required AuthenticatorAssertionResponseJSON Response { get; set; } 15 | 16 | [JsonPropertyName("authenticatorAttachment")] 17 | public string? AuthenticatorAttachment { get; set; } 18 | 19 | [JsonPropertyName("clientExtensionResults")] 20 | public required AuthenticationExtensionsClientOutputsJSON ClientExtensionResults { get; set; } 21 | 22 | [JsonPropertyName("type")] 23 | public required string Type { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/JSONRepresentations/AuthenticatorAssertionResponseJSON.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication.JSONRepresentations; 4 | 5 | public class AuthenticatorAssertionResponseJSON 6 | { 7 | [JsonPropertyName("clientDataJSON")] 8 | public required string ClientDataJSON { get; set; } 9 | 10 | [JsonPropertyName("authenticatorData")] 11 | public required string AuthenticatorData { get; set; } 12 | 13 | [JsonPropertyName("signature")] 14 | public required string Signature { get; set; } 15 | 16 | [JsonPropertyName("userHandle")] 17 | public string? UserHandle { get; set; } 18 | 19 | [JsonPropertyName("attestationObject")] 20 | public string? AttestationObject { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/JSONRepresentations/AuthenticatorAttestationResponseJSON.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication.JSONRepresentations; 4 | 5 | public class AuthenticatorAttestationResponseJSON 6 | { 7 | [JsonPropertyName("clientDataJSON")] 8 | public required string ClientDataJSON { get; set; } 9 | 10 | [JsonPropertyName("authenticatorData")] 11 | public required string AuthenticatorData { get; set; } 12 | 13 | [JsonPropertyName("transports")] 14 | public required AuthenticatorTransport[] Transports { get; set; } 15 | 16 | /// 17 | /// The publicKey field will be missing if pubKeyCredParams was used to negotiate a public-key algorithm that the user agent doesn’t understand. 18 | /// If using such an algorithm then the public key must be parsed directly from attestationObject or authenticatorData. 19 | /// 20 | [JsonPropertyName("publicKey")] 21 | public string? PublicKey { get; set; } 22 | 23 | [JsonPropertyName("publicKeyAlgorithm")] 24 | public required long PublicKeyAlgorithm { get; set; } 25 | 26 | [JsonPropertyName("attestationObject")] 27 | public required string AttestationObject { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/JSONRepresentations/PublicKeyCredentialJSON.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebIDL; 2 | using Microsoft.JSInterop; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication.JSONRepresentations; 5 | 6 | public class PublicKeyCredentialJSON 7 | { 8 | public static async Task GetConcreteInstanceAsync(ValueReference authenticatorResponse) 9 | { 10 | authenticatorResponse.ValueMapper = new() 11 | { 12 | { "registrationresponsejson", async () => await authenticatorResponse.GetValueAsync() }, 13 | { "authenticationresponsejson", async () => await authenticatorResponse.GetValueAsync() } 14 | }; 15 | 16 | object? result = await authenticatorResponse.GetValueAsync(); 17 | return result is IJSObjectReference or null ? null! : (PublicKeyCredentialJSON)result; 18 | } 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/JSONRepresentations/RegistrationResponseJSON.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication.JSONRepresentations; 4 | 5 | public class RegistrationResponseJSON : PublicKeyCredentialJSON 6 | { 7 | [JsonPropertyName("id")] 8 | public required string Id { get; set; } 9 | 10 | [JsonPropertyName("rawId")] 11 | public required string RawId { get; set; } 12 | 13 | [JsonPropertyName("response")] 14 | public required AuthenticatorAttestationResponseJSON Response { get; set; } 15 | 16 | [JsonPropertyName("authenticatorAttachment")] 17 | public string? AuthenticatorAttachment { get; set; } 18 | 19 | [JsonPropertyName("clientExtensionResults")] 20 | public required AuthenticationExtensionsClientOutputsJSON ClientExtensionResults { get; set; } 21 | 22 | [JsonPropertyName("type")] 23 | public required string Type { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/KristofferStrube.Blazor.WebAuthentication.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/AttestationConveyancePreference.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebAuthentication.Converters; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication; 5 | 6 | [JsonConverter(typeof(AttestationConveyancePreferenceConverter))] 7 | public enum AttestationConveyancePreference 8 | { 9 | /// 10 | /// The Relying Party is not interested in authenticator attestation. 11 | /// For example, in order to potentially avoid having to obtain user consent to relay identifying information to the Relying Party, 12 | /// or to save a roundtrip to an Attestation CA or Anonymization CA. 13 | /// If the authenticator generates an attestation statement that is not a self attestation, the client will replace it with a None attestation statement. 14 | /// 15 | None, 16 | /// 17 | /// The Relying Party wants to receive a verifiable attestation statement, 18 | /// but allows the client to decide how to obtain such an attestation statement. 19 | /// The client can replace an authenticator-generated attestation statement with one generated by an Anonymization CA, in order to protect the user’s privacy, 20 | /// or to assist Relying Parties with attestation verification in a heterogeneous ecosystem. 21 | /// 22 | Indirect, 23 | /// 24 | /// The Relying Party wants to receive the attestation statement as generated by the authenticator. 25 | /// 26 | Direct, 27 | /// 28 | /// The Relying Party wants to receive an attestation statement that may include uniquely identifying information. 29 | /// This is intended for controlled deployments within an enterprise where the organization wishes to tie registrations to specific authenticators. 30 | /// User agents will provide such an attestation unless the user agent or authenticator configuration permits it for the requested RP ID. 31 | /// 32 | Enterprise 33 | } 34 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/AttestationFormat.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebAuthentication.Converters; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication; 5 | 6 | /// 7 | /// Authenticators may implement various transports for communicating with clients. 8 | /// This enum defines hints as to how clients might communicate with a particular authenticator in order to obtain an assertion for a specific credential. 9 | /// 10 | /// See the API definition here. 11 | [JsonConverter(typeof(AttestationFormatConverter))] 12 | public enum AttestationFormat 13 | { 14 | /// 15 | /// The "packed" attestation statement format is a WebAuthn-optimized format for attestation. It uses a very compact but still extensible encoding method. 16 | /// This format is implementable by authenticators with limited resources (e.g., secure elements). 17 | /// 18 | Packed, 19 | /// 20 | /// The TPM attestation statement format returns an attestation statement in the same format as the packed attestation statement format, 21 | /// although the rawData and signature fields are computed differently. 22 | /// 23 | TPM, 24 | /// 25 | /// Platform authenticators on versions "N", and later, may provide this proprietary "hardware attestation" statement. 26 | /// 27 | AndroidKey, 28 | /// 29 | /// Android-based platform authenticators MAY produce an attestation statement based on the Android SafetyNet API. 30 | /// 31 | AndroidSafetyNet, 32 | /// 33 | /// Used with FIDO U2F authenticators 34 | /// 35 | FidoU2F, 36 | /// 37 | /// Used with Apple devices' platform authenticators 38 | /// 39 | Apple, 40 | /// 41 | /// Used to replace any authenticator-provided attestation statement when a WebAuthn Relying Party indicates it does not wish to receive attestation information. 42 | /// 43 | None, 44 | } 45 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/AuthenticatorTransport.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebAuthentication.Converters; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication; 5 | 6 | /// 7 | /// Authenticators may implement various transports for communicating with clients. 8 | /// This enum defines hints as to how clients might communicate with a particular authenticator in order to obtain an assertion for a specific credential. 9 | /// 10 | /// See the API definition here. 11 | [JsonConverter(typeof(AuthenticatorTransportConverter))] 12 | public enum AuthenticatorTransport 13 | { 14 | /// 15 | /// Indicates the respective authenticator can be contacted over removable USB. 16 | /// 17 | Usb, 18 | /// 19 | /// Indicates the respective authenticator can be contacted over Near Field Communication (NFC). 20 | /// 21 | Nfc, 22 | /// 23 | /// Indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE). 24 | /// 25 | Ble, 26 | /// 27 | /// Indicates the respective authenticator can be contacted over ISO/IEC 7816 smart card with contacts. 28 | /// 29 | SmartCard, 30 | /// 31 | /// Indicates the respective authenticator can be contacted using a combination of (often separate) data-transport and proximity mechanisms. 32 | /// This supports, for example, authentication on a desktop computer using a smartphone. 33 | /// 34 | Hybrid, 35 | /// 36 | /// Indicates the respective authenticator is contacted using a client device-specific transport, i.e., it is a platform authenticator. 37 | /// These authenticators are not removable from the client device. 38 | /// 39 | Internal, 40 | } 41 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/CredentialCreationOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | public class CredentialCreationOptions : CredentialManagement.CredentialCreationOptions 6 | { 7 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 8 | [JsonPropertyName("publicKey")] 9 | public PublicKeyCredentialCreationOptions? PublicKey { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/CredentialRequestOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | public class CredentialRequestOptions : CredentialManagement.CredentialRequestOptions 6 | { 7 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 8 | [JsonPropertyName("publicKey")] 9 | public PublicKeyCredentialRequestOptions? PublicKey { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/PublicKeyCredentialCreationOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | /// 6 | /// The options specific to the public key of . 7 | /// 8 | /// See the API definition here. 9 | public class PublicKeyCredentialCreationOptions 10 | { 11 | [JsonPropertyName("rp")] 12 | public required PublicKeyCredentialRpEntity Rp { get; set; } 13 | 14 | [JsonPropertyName("user")] 15 | public required PublicKeyCredentialUserEntity User { get; set; } 16 | 17 | [JsonPropertyName("challenge")] 18 | public required byte[] Challenge { get; set; } 19 | 20 | [JsonPropertyName("pubKeyCredParams")] 21 | public required PublicKeyCredentialParameters[] PubKeyCredParams { get; set; } 22 | 23 | [JsonPropertyName("timeout")] 24 | public ulong Timeout { get; set; } 25 | 26 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 27 | [JsonPropertyName("hints")] 28 | public string[]? Hints { get; set; } 29 | 30 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 31 | [JsonPropertyName("attestation")] 32 | public AttestationConveyancePreference Attestation { get; set; } 33 | 34 | /// 35 | /// The Relying Party can use this optional member to specify a preference regarding the attestation statement format used by the authenticator. 36 | /// Values are ordered from most preferable to least preferable. 37 | /// This parameter is advisory and the authenticator and could use an attestation statement not enumerated in this parameter. 38 | /// 39 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 40 | [JsonPropertyName("attestationFormats")] 41 | public AttestationFormat[]? AttestationFormats { get; set; } 42 | } 43 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/PublicKeyCredentialDescriptor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication; 5 | 6 | public class PublicKeyCredentialDescriptor 7 | { 8 | [JsonPropertyName("type")] 9 | public required PublicKeyCredentialType Type { get; set; } 10 | 11 | [JsonPropertyName("id")] 12 | public required IJSObjectReference Id { get; set; } 13 | 14 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 15 | [JsonPropertyName("transports")] 16 | public string[]? Transports { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/PublicKeyCredentialEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | public class PublicKeyCredentialEntity 6 | { 7 | [JsonPropertyName("name")] 8 | public required string Name { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/PublicKeyCredentialParameters.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | public class PublicKeyCredentialParameters 6 | { 7 | /// 8 | /// This member specifies the type of credential to be created. 9 | /// 10 | [JsonPropertyName("type")] 11 | public required PublicKeyCredentialType Type { get; set; } 12 | 13 | /// 14 | /// This member specifies the cryptographic signature algorithm with which the newly generated credential will be used, and thus also the type of asymmetric key pair to be generated, e.g., RSA or Elliptic Curve. 15 | /// 16 | [JsonPropertyName("alg")] 17 | public required COSEAlgorithm Alg { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/PublicKeyCredentialRequestOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | public class PublicKeyCredentialRequestOptions 6 | { 7 | [JsonPropertyName("challenge")] 8 | public required byte[] Challenge { get; set; } 9 | 10 | [JsonPropertyName("timeout")] 11 | public ulong Timeout { get; set; } 12 | 13 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 14 | [JsonPropertyName("rpId")] 15 | public string? RpId { get; set; } 16 | 17 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 18 | [JsonPropertyName("allowCredentials")] 19 | public PublicKeyCredentialDescriptor[]? AllowCredentials { get; set; } 20 | 21 | [JsonPropertyName("userVerfication")] 22 | public UserVerificationRequirement UserVerfication { get; set; } = UserVerificationRequirement.Preferred; 23 | } 24 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/PublicKeyCredentialRpEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | public class PublicKeyCredentialRpEntity : PublicKeyCredentialEntity 6 | { 7 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 8 | [JsonPropertyName("id")] 9 | public string? Id { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/PublicKeyCredentialType.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebAuthentication.Converters; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication; 5 | 6 | /// 7 | /// This enumeration defines the valid credential types. 8 | /// It is an extension point; values can be added to it in the future, as more credential types are defined. 9 | /// The values of this enumeration are used for versioning the Authentication Assertion and attestation structures according to the type of the authenticator.
10 | /// Currently one credential type is defined, namely . 11 | ///
12 | /// See the API definition here. 13 | [JsonConverter(typeof(PublicKeyCredentialTypeConverter))] 14 | public enum PublicKeyCredentialType 15 | { 16 | PublicKey 17 | } 18 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/PublicKeyCredentialUserEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KristofferStrube.Blazor.WebAuthentication; 4 | 5 | public class PublicKeyCredentialUserEntity : PublicKeyCredentialEntity 6 | { 7 | [JsonPropertyName("id")] 8 | public required byte[] Id { get; set; } 9 | 10 | [JsonPropertyName("displayName")] 11 | public required string DisplayName { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/Options/UserVerificationRequirement.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.WebAuthentication.Converters; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KristofferStrube.Blazor.WebAuthentication; 5 | 6 | [JsonConverter(typeof(UserVerificationRequirementConverter))] 7 | public enum UserVerificationRequirement 8 | { 9 | Required, 10 | Preferred, 11 | Discouraged, 12 | } 13 | -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/PublicKeyCredential.cs: -------------------------------------------------------------------------------- 1 | using KristofferStrube.Blazor.CredentialManagement; 2 | using KristofferStrube.Blazor.WebAuthentication.Extensions; 3 | using KristofferStrube.Blazor.WebAuthentication.JSONRepresentations; 4 | using KristofferStrube.Blazor.WebIDL; 5 | using Microsoft.JSInterop; 6 | 7 | namespace KristofferStrube.Blazor.WebAuthentication; 8 | 9 | /// 10 | /// The PublicKeyCredential interface inherits from , and contains the attributes that are returned to the caller when a new credential is created, or a new assertion is requested. 11 | /// 12 | /// See the API definition here. 13 | public class PublicKeyCredential : Credential 14 | { 15 | protected readonly Lazy> webAuthenticationHelperTask; 16 | 17 | protected internal PublicKeyCredential(IJSRuntime jSRuntime, IJSObjectReference jSReference) : base(jSRuntime, jSReference) 18 | { 19 | webAuthenticationHelperTask = new(jSRuntime.GetHelperAsync); 20 | } 21 | 22 | public PublicKeyCredential(Credential credential) : this(credential.JSRuntime, credential.JSReference) { } 23 | 24 | /// 25 | /// This attribute returns the ArrayBuffer for this credential. 26 | /// 27 | public async Task GetRawIdAsync() 28 | { 29 | IJSObjectReference helper = await webAuthenticationHelperTask.Value; 30 | return await helper.InvokeAsync("getAttribute", JSReference, "rawId"); 31 | } 32 | 33 | public async Task GetRawIdAsArrayAsync() 34 | { 35 | IJSObjectReference helper = await webAuthenticationHelperTask.Value; 36 | IJSObjectReference arrayBuffer = await helper.InvokeAsync("getAttribute", JSReference, "rawId"); 37 | 38 | IJSObjectReference webIDLHelper = await JSRuntime.InvokeAsync("import", "./_content/KristofferStrube.Blazor.WebIDL/KristofferStrube.Blazor.WebIDL.js"); 39 | IJSObjectReference uint8ArrayFromBuffer = await webIDLHelper.InvokeAsync("constructUint8Array", arrayBuffer); 40 | Uint8Array uint8Array = await Uint8Array.CreateAsync(JSRuntime, uint8ArrayFromBuffer); 41 | return await uint8Array.GetByteArrayAsync(); 42 | } 43 | 44 | public async Task GetResponseAsync() 45 | { 46 | ValueReference responseAttribute = new(JSRuntime, JSReference, "response"); 47 | return await AuthenticatorResponse.GetConcreteInstanceAsync(responseAttribute); 48 | } 49 | 50 | public async Task ToJSONAsync() 51 | { 52 | AuthenticatorResponse response = await GetResponseAsync(); 53 | if (response is AuthenticatorAssertionResponse authenticatorAssertion) 54 | { 55 | return new AuthenticationResponseJSON() 56 | { 57 | Id = Convert.ToBase64String(await GetRawIdAsArrayAsync()), 58 | RawId = Convert.ToBase64String(await GetRawIdAsArrayAsync()), 59 | Response = new() 60 | { 61 | ClientDataJSON = Convert.ToBase64String(await authenticatorAssertion.GetClientDataJSONAsArrayAsync()), 62 | AuthenticatorData = Convert.ToBase64String(await authenticatorAssertion.GetAuthenticatorDataAsArrayAsync()), 63 | Signature = Convert.ToBase64String(await authenticatorAssertion.GetSignatureAsArrayAsync()), 64 | }, 65 | ClientExtensionResults = new(), 66 | Type = "public-key" 67 | }; 68 | } 69 | else if (response is AuthenticatorAttestationResponse authenticatorAttestation) 70 | { 71 | return new RegistrationResponseJSON() 72 | { 73 | Id = Convert.ToBase64String(await GetRawIdAsArrayAsync()), 74 | RawId = Convert.ToBase64String(await GetRawIdAsArrayAsync()), 75 | Response = new() 76 | { 77 | ClientDataJSON = Convert.ToBase64String(await authenticatorAttestation.GetClientDataJSONAsArrayAsync()), 78 | Transports = await authenticatorAttestation.GetTransportsAsync(), 79 | AuthenticatorData = Convert.ToBase64String(await authenticatorAttestation.GetAuthenticatorDataAsArrayAsync()), 80 | PublicKey = Convert.ToBase64String(await authenticatorAttestation.GetPublicKeyAsArrayAsync()), 81 | PublicKeyAlgorithm = (long)await authenticatorAttestation.GetPublicKeyAlgorithmAsync(), 82 | AttestationObject = Convert.ToBase64String(await authenticatorAttestation.GetAttestationObjectAsync()), 83 | }, 84 | ClientExtensionResults = new(), 85 | Type = "public-key" 86 | }; 87 | } 88 | return default!; 89 | } 90 | } -------------------------------------------------------------------------------- /src/KristofferStrube.Blazor.WebAuthentication/wwwroot/KristofferStrube.Blazor.WebAuthentication.js: -------------------------------------------------------------------------------- 1 | export function getAttribute(object, attribute) { return object[attribute]; } -------------------------------------------------------------------------------- /tests/KristofferStrube.Blazor.WebAuthentication.Tests/AttestationStatementTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using FluentAssertions.Execution; 3 | using System.Security.Cryptography; 4 | 5 | namespace KristofferStrube.Blazor.WebAuthentication.Tests; 6 | 7 | public class AttestationStatementTests 8 | { 9 | [Fact] 10 | public void None_AttestationObject_CanBeParsed() 11 | { 12 | // Arrange 13 | string attestationObject = "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUfcfg03b1kxLHnF0mpy6nRsulHtqgscojQPUAQxyo9mBdAAAAAOqbjWZNAR0hPOS2tIy1ddQAEJmvgOPCmkesIY265qnCHCelAQIDJiABIVgg8V97cvCnVsupp29WapXqpis8L2\u002BGudY63q1jWBqhvF4iWCD0fuS8lSlnZ2OdyzW\u002BDIhlbKVVw16C/tyErEv0EiCAyg=="; 14 | 15 | // Act 16 | var result = AttestationStatement.ReadFromBase64EncodedAttestationStatement(attestationObject); 17 | 18 | // Assert 19 | _ = result.Should().BeOfType(); 20 | } 21 | 22 | [Fact] 23 | public void TPM_AttestationObject_CanBeParsed() 24 | { 25 | // Arrange 26 | string attestationObject = "o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn//mNzaWdZAQAmZOqDKka896vNDbGN9vP4rYch9kKFikscMhNoAxQ9epANhUcknLyCLk8kLsOG1XxUfdlF9gKaBLJ6tYYwq\u002BUzQA2EKj9rRg6I4aZmKcamQVfz39VWYKUwL/MfCd8M1758Z9iVzEe3/nTYUi9NOQ5yoNdzYYTMOo9\u002BUZR4vF6ZMXW2iwtY/SY0loVDREIUGSZbAxFauLimnZEbAp0XvDtneOWnrdZ3EhclvDmlKBRb46shBpawsA1rrEshxvkDrIctaEg20tGILluPewSyAzGtb2JtKc/EqAKv9o788CPzCG71gi6ZQ1qkqukUkIRGHHFqjod4i3QhQ1/boCTwyWBeY3ZlcmMyLjBjeDVjglkFxDCCBcAwggOooAMCAQICED7yGspsgUQ2oC5PoqMycLYwDQYJKoZIhvcNAQELBQAwQTE/MD0GA1UEAxM2RVVTLVNUTS1LRVlJRC1GQjE3RDcwRDczNDg3MEU5MTlDNEU4RTYwMzk3NUU2NjRFMEU0M0RFMB4XDTIzMTIwNDEzMDYyMVoXDTI3MDYwMzE5NDAyNFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMZB7aSfGaYByS/zXUXhPOv\u002BLwoKAjasTZ7hsE027y9hBe4H2zt9v/g/ZdCN0ItoHjmCm1GClbq7uhZUKnQLVZvANZj8mp8fPykkDQ7\u002BUtLZR128xwR13YuifPKoShDe9BlFnT2FKEAau/n2Kfe7Y1z/z/vju7T68DKSegdHkJIInBoHkl5jIfz/epQ7L0ZgTr9zZ\u002B0ZqIMOsD2npm3CILDSmep8my13Wb4EklS6UqDVbLtMjoZZdCvdglBAzSPMLzW2cYi7SRCIjFQ69jVf8aWIeFwEflHJ/U1PNXIsOEgQHU727YmUDEojHoBy/DbrYIm1rOFXebss\u002BrFJ/tj2KV8CAwEAAaOCAfMwggHvMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB/wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMFkGA1UdEQEB/wRPME2kSzBJMRYwFAYFZ4EFAgEMC2lkOjUzNTQ0RDIwMRcwFQYFZ4EFAgIMDFNUMzNIVFBIQUhFNDEWMBQGBWeBBQIDDAtpZDowMDAxMDMwMTAfBgNVHSMEGDAWgBQ4pJZObjM\u002BjRpJDnX8A7aFbZT8iDAdBgNVHQ4EFgQUBtuLgW9EjuajEOxLkmSejflmfh8wgbIGCCsGAQUFBwEBBIGlMIGiMIGfBggrBgEFBQcwAoaBkmh0dHA6Ly9hemNzcHJvZGV1c2Fpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L2V1cy1zdG0ta2V5aWQtZmIxN2Q3MGQ3MzQ4NzBlOTE5YzRlOGU2MDM5NzVlNjY0ZTBlNDNkZS84ZTJjZjI5NS1kMjIzLTQ5NmEtOTdlYy0yNTc2OTVjNmMyZmMuY2VyMA0GCSqGSIb3DQEBCwUAA4ICAQBo69fHfVEtul5n42xE/dapb7Jj7/eoCrSSUQKq3UjHbwv9Wr\u002B03G/VfaLp9HoXXxxDxPz8KuMrsAZs93FZ9QfQ2tdNQoay5hD5lJayeQ\u002BJB/fEgkwFH0QDTNX\u002BzaqZVxjOG4c79ATKLPy4V6tGxskO7NExVXwWniYALCeF\u002BwiwtDEeyvZsG0sdLSwwYuztHQNQ1dtqcuHkItNmmECgyivKTonO3qObOk5sn9JKbTRdGRVV/aATrHNXvFuem2q1LE/xL8U\u002B4ezKVyK6l0QyWS4oHj2cYSsqPOETzmZzPjdgQBsiw4A4GJcuhLELZ0tQZvyOfofeq6X\u002B383DRpdaUedKh64uzrvGJWVM1ZvBUE9LlpBznBQ3yz1YUgK32V9cnTSo\u002B0XSlXEPTOfmeBiS4a/T0qsHdaIeLYwVYmEkNwjImHR84HZzAZfife72PUxZoOimQRJdo6iEJ2eFQ5OVPRcMm9usXqlPQbDtCbpyPOZRI\u002B\u002B/XYt3zX8BM8z6A3vP6ldmS1MglyLZwAFs2GJHkohOFFNWbc3DORz59K\u002Bb4lnfExikhUqu97ydWYV1YO79g5ECdYv7Nqoa/xgRjvzXLjA7I\u002BwbYJjy74cwfyt/u4pO6dN/4tT7X6RycEBEQmpWPtbMv2lp56fXFhWMLYlvqr4KK3z/ftEFgRBd8NGBufnKLFkG7zCCBuswggTToAMCAQICEzMAAAUtM4db5/ICoa8AAAAABS0wDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDAeFw0yMTA2MDMxOTQwMjRaFw0yNzA2MDMxOTQwMjRaMEExPzA9BgNVBAMTNkVVUy1TVE0tS0VZSUQtRkIxN0Q3MEQ3MzQ4NzBFOTE5QzRFOEU2MDM5NzVFNjY0RTBFNDNERTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKvu5BtXmmeYhA9MHXP9BXKRWLoSn352DZWpcmSxtASbp2evJt5EiGuyHX637koanGphUUmKk26USKD19nyamCCQy6Wh4/U01DICR3gcaR7nsKE\u002B/uL3ratdR0xwpx/lO6WZw6bvuqsDSVFebZeOYBk310utv4kiMtDYC91\u002B0/JdjSGQtaYvNZJz7NfhNxtOvmLsNl9gjZOQOsF45SjNcRh/0S62qF4g4dM7q1/HuFlWLlDwNzAjn07nE2gNHjJ2zCxCZkh0PoLakKJZZRe1O0CfyQP9cCoPKk7nGfMpKn8wy\u002BRisMzaBopjl7NiyManoUT51qsFzbPNN3vUnqNeRPl9u/PteYMM7Agx73MVX5/76qA49mqrnP/XNpHUD/B6k9Ti2vtV5rnYNFtedbxDwEqdNcMNk068jBhecuFdPdKdatwWDz7oczxt5YyJTGMSuDPRZGHtAgl6Y5lHsFSaozpz/QmlRmlSni5MYyy0Kol7qrcLjgzAr\u002BwG1QHTlRRAyeaUAU0Op1c9yPVYSpwzmOsyI3swQKIZIAhHj8MVBUDsvBotf7GULUKJppfw5B43khwpNZVoUT6wKyYXSKekxPTsxdz7azBCPXWt9qJO4ZIzLzhOFq8eyXySFk92zuTb2gmwWXNLXsjMOm6hqk3sPm9DL/Rn9aSAi7SKJem4ibgNAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ4pJZObjM\u002BjRpJDnX8A7aFbZT8iDAfBgNVHSMEGDAWgBR6jArOL0hiF\u002BKU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBABjK2\u002B9pxH4S\u002B6fyCAKHHgROS5UvqzLkSZNd0F\u002B3bPJ9q1\u002BkAdUmk0\u002BF6lXpJcGTXG2tcX0VpOoxHYeuugPTYE2YsmVSd4l\u002B/sKPdDKPs5ZoJBYemEUsYjp0I2NUrSjQqSM7OTLUe/wdSEUaD1QIfQ1QmSSoGg8WqR23yswykOrkRomLRJqIQrI4Iyd1pSMhRVkizM/6asjyy/xCi3J29BnNAZFFUnH0fcfR9R6t2MSxo84aYvV8n0cdyFyM/L654kdUZcyqn4R0lfnemOxej4e9/pQLyP0qY1mfJ4TRiCTJ\u002BeG7VmC5tdH9Ol5QhiVsqWYBX6rF8hd7RSLDBr4HF8ve1IF1Nsg0qRtfPjAiax8q6TE/rpe0YMROHRcanBufX7U16idX/l/y6aOyvnezoCqEK1IM8YAE8/GF7RQJN6xNXB171vVudlet\u002B3gIoSp/flCgtIo81V6wRl\u002BCKtaNTNGX0frRaDp\u002BE7I3ullpJqhK5KtQE7AKGUeh8nc9LAKVW0FAnlrs96eDHbB/F53EBFc\u002BUtbYtSpXzx10RvqtctsWOtl5w5dEn6Pl1FugG\u002BKZ/fMWrDAk54WqyojOethgS1SbZb1dwzEixAZeUn7hjPqmI0IE0JJ13HJLPYLgpjWjf29n6NQ4rG7n134zNw1WjKseyCaUnN8AJ8aKDKwGBqTzZ3B1YkFyZWFYdgAjAAsABAQyACANlDe44jA/Lh9WlLkzGiFbLRtCkZsQcR2d/3fxFh9wiAAQABAAAwAQACB0s8Wqh9qsmgu0bkwMOT0zJcVZKRyo\u002BsPiLJIvTSC73gAgTvavLjnRvA6xFthlDlC5tA2dXB47D9/9wZLnULsVutZoY2VydEluZm9Yof9UQ0eAFwAiAAtowgBgfMtWkhgfWcncjqx1bT3ixhZn3bI3wB4Bo80lfwAUs0Bxm1z0X2zPSTLgW8WDMbWRPIMAAAAAObSrnyVm6fQt6eQWAQav\u002B508WuMSACIAC4zXXU60nKWkl0AQ\u002BkG07vqTyK/LhvehDx8iJJG4b\u002BGsACIAC5agueugLwosXuToSHXWGD6qRrs/swm8tVrK7FqALNzpaGF1dGhEYXRhWKR9x\u002BDTdvWTEsecXSanLqdGy6Ue2qCxyiNA9QBDHKj2YEUAAAAAnd0YF69aRnKiuT492VAAqQAgDFOCICwZ7LYEftbR3KVBx9mVeylbUS2zr8o7XBxE2BWlAQIDJiABIVggdLPFqofarJoLtG5MDDk9MyXFWSkcqPrD4iySL00gu94iWCBO9q8uOdG8DrEW2GUOULm0DZ1cHjsP3/3BkudQuxW61g=="; 27 | string authenticatorData = "fcfg03b1kxLHnF0mpy6nRsulHtqgscojQPUAQxyo9mBFAAAAAJ3dGBevWkZyork\u002BPdlQAKkAIAxTgiAsGey2BH7W0dylQcfZlXspW1Ets6/KO1wcRNgVpQECAyYgASFYIHSzxaqH2qyaC7RuTAw5PTMlxVkpHKj6w\u002BIski9NILveIlggTvavLjnRvA6xFthlDlC5tA2dXB47D9/9wZLnULsVutY="; 28 | string clientDataJSON = "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ3ZGc0FNWV80SmRUQ1d4MG9HVmNsdnd0eW95RV9ZZ05qUlRGcEFoTkxtayIsIm9yaWdpbiI6Imh0dHBzOi8va3Jpc3RvZmZlcnN0cnViZS5naXRodWIuaW8iLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ=="; 29 | 30 | byte[] clientDataJSONBytes = Convert.FromBase64String(clientDataJSON); 31 | var hasher = SHA256.Create(); 32 | byte[] clientDataHash = hasher.ComputeHash(clientDataJSONBytes); 33 | 34 | // Act 35 | var result = AttestationStatement.ReadFromBase64EncodedAttestationStatement(attestationObject); 36 | 37 | // Assert 38 | using (new AssertionScope()) 39 | { 40 | TPMAttestationStatement attestationStatement = result.Should().BeOfType().Subject; 41 | 42 | _ = attestationStatement.Verify(Convert.FromBase64String(authenticatorData), clientDataHash).Should().BeTrue(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/KristofferStrube.Blazor.WebAuthentication.Tests/KristofferStrube.Blazor.WebAuthentication.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tools/KristofferStrube.Blazor.COSEGenerator/KristofferStrube.Blazor.COSEGenerator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tools/KristofferStrube.Blazor.COSEGenerator/Program.cs: -------------------------------------------------------------------------------- 1 | using System.CodeDom.Compiler; 2 | 3 | if (args.Length != 3) 4 | { 5 | throw new ArgumentException("There should be parsed 3 arguments to this program. The first should be a CSV file containing COSE algorithm descriptions, the second should be the destination to generate the C# file, and the third should be the namespace to use for the class."); 6 | } 7 | 8 | using FileStream sourceStream = File.Open(args[0], FileMode.Open); 9 | using StreamWriter destinationStream = File.CreateText(args[1]); 10 | 11 | using StreamReader reader = new(sourceStream); 12 | using IndentedTextWriter writer = new(destinationStream); 13 | 14 | // We skip the first line which is just the headers. 15 | reader.ReadLine(); 16 | 17 | (string match, string replace)[] nameSanitationArguments = [ 18 | ("-", "_"), 19 | (" w/ ", "_"), 20 | (" ", "_"), 21 | ("+", "and"), 22 | ("/", "_truncated_to_"), 23 | ]; 24 | 25 | writer.WriteLine($"namespace {args[2]};"); 26 | writer.WriteLine(); 27 | writer.WriteLine("/// "); 28 | writer.WriteLine("/// A COSEAlgorithmIdentifier's value is a number identifying a cryptographic algorithm."); 29 | writer.WriteLine("/// "); 30 | writer.WriteLine("public enum COSEAlgorithm : long"); 31 | writer.WriteLine("{"); 32 | writer.Indent++; 33 | while (reader.ReadLine() is { } line) 34 | { 35 | string[] lineSegments = line.Split(","); 36 | string name = nameSanitationArguments 37 | .Aggregate(lineSegments[0], (accu, sanitizer) => accu.Replace(sanitizer.match, sanitizer.replace)); 38 | if (name is "Unassigned" || !int.TryParse(lineSegments[1], out int value)) 39 | { 40 | continue; 41 | } 42 | string description = lineSegments[2]; 43 | string capabilities = lineSegments[3]; 44 | string changeController = lineSegments[4]; 45 | string referenceList = lineSegments[5]; 46 | string recommended = lineSegments[6]; 47 | writer.WriteLine("/// "); 48 | writer.WriteLine($"/// {description}
"); 49 | writer.WriteLine($"/// {(recommended == "Yes" ? "This is recommended to use." : "This is not recommended to use.")}"); 50 | writer.WriteLine("///
"); 51 | if (referenceList.Length >= 3) 52 | { 53 | writer.WriteLine("/// "); 54 | string[] references = referenceList[1..^1].Split("]["); 55 | foreach (string reference in references) 56 | { 57 | writer.WriteLine($"/// See the reference for {reference}.
"); 58 | } 59 | writer.WriteLine("///
"); 60 | } 61 | writer.WriteLine($"{name} = {value},"); 62 | writer.WriteLine(); 63 | } 64 | writer.Indent--; 65 | writer.WriteLine("}"); 66 | --------------------------------------------------------------------------------