├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── BlazorServerOidc.sln ├── BlazorWebApp ├── BlazorWebApp.csproj ├── Components │ ├── App.razor │ ├── Layout │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ ├── NavMenu.razor.css │ │ └── RedirectToLogin.razor │ ├── Pages │ │ ├── Counter.razor │ │ ├── Error.razor │ │ ├── Home.razor │ │ └── Weather.razor │ ├── Routes.razor │ └── _Imports.razor ├── MapLoginLogoutEndpoints.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── SecurityHeadersDefinitions.cs ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── app.css │ ├── favicon.png │ └── lib │ └── bootstrap │ └── dist │ ├── css │ ├── bootstrap-grid.css │ ├── bootstrap-grid.css.map │ ├── bootstrap-grid.min.css │ ├── bootstrap-grid.min.css.map │ ├── bootstrap-grid.rtl.css │ ├── bootstrap-grid.rtl.css.map │ ├── bootstrap-grid.rtl.min.css │ ├── bootstrap-grid.rtl.min.css.map │ ├── bootstrap-reboot.css │ ├── bootstrap-reboot.css.map │ ├── bootstrap-reboot.min.css │ ├── bootstrap-reboot.min.css.map │ ├── bootstrap-reboot.rtl.css │ ├── bootstrap-reboot.rtl.css.map │ ├── bootstrap-reboot.rtl.min.css │ ├── bootstrap-reboot.rtl.min.css.map │ ├── bootstrap-utilities.css │ ├── bootstrap-utilities.css.map │ ├── bootstrap-utilities.min.css │ ├── bootstrap-utilities.min.css.map │ ├── bootstrap-utilities.rtl.css │ ├── bootstrap-utilities.rtl.css.map │ ├── bootstrap-utilities.rtl.min.css │ ├── bootstrap-utilities.rtl.min.css.map │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── bootstrap.rtl.css │ ├── bootstrap.rtl.css.map │ ├── bootstrap.rtl.min.css │ └── bootstrap.rtl.min.css.map │ └── js │ ├── bootstrap.bundle.js │ ├── bootstrap.bundle.js.map │ ├── bootstrap.bundle.min.js │ ├── bootstrap.bundle.min.js.map │ ├── bootstrap.esm.js │ ├── bootstrap.esm.js.map │ ├── bootstrap.esm.min.js │ ├── bootstrap.esm.min.js.map │ ├── bootstrap.js │ ├── bootstrap.js.map │ ├── bootstrap.min.js │ └── bootstrap.min.js.map ├── IdentityProvider ├── Areas │ └── Identity │ │ ├── IdentityHostingStartup.cs │ │ └── Pages │ │ ├── Account │ │ ├── Login.cshtml │ │ ├── Login.cshtml.cs │ │ ├── LoginFido2Mfa.cshtml │ │ ├── LoginFido2Mfa.cshtml.cs │ │ ├── Manage │ │ │ ├── Disable2fa.cshtml │ │ │ ├── Disable2fa.cshtml.cs │ │ │ ├── Fido2Mfa.cshtml │ │ │ ├── Fido2Mfa.cshtml.cs │ │ │ ├── ManageNavPages.cs │ │ │ ├── TwoFactorAuthentication.cshtml │ │ │ ├── TwoFactorAuthentication.cshtml.cs │ │ │ ├── _ManageNav.cshtml │ │ │ └── _ViewImports.cshtml │ │ └── _ViewImports.cshtml │ │ ├── _ValidationScriptsPartial.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml ├── Controllers │ ├── AuthorizationController.cs │ ├── ErrorController.cs │ ├── HomeController.cs │ ├── ResourceController.cs │ └── UserinfoController.cs ├── Data │ ├── ApplicationDbContext.cs │ └── ApplicationUser.cs ├── Fido2 │ ├── Fido2Store.cs │ ├── Fido2UserTwoFactorTokenProvider.cs │ ├── FidoStoredCredential.cs │ ├── MfaFido2RegisterController.cs │ ├── MfaFido2SignInFidoController.cs │ ├── PwFido2RegisterController.cs │ └── PwFido2SignInController.cs ├── Helpers │ ├── AsyncEnumerableExtensions.cs │ └── FormValueRequiredAttribute.cs ├── HostingExtensions.cs ├── IdentityProvider.csproj ├── Migrations │ ├── 20231222075731_init_sts.Designer.cs │ ├── 20231222075731_init_sts.cs │ ├── 20231222144924_5-0.0.Designer.cs │ ├── 20231222144924_5-0.0.cs │ └── ApplicationDbContextModelSnapshot.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── ViewModels │ ├── Authorization │ │ └── AuthorizeViewModel.cs │ └── Shared │ │ └── ErrorViewModel.cs ├── Views │ ├── Authorization │ │ ├── Authorize.cshtml │ │ └── Logout.cshtml │ ├── Home │ │ ├── Index.cshtml │ │ └── Privacy.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _Layout.cshtml │ │ └── _LoginPartial.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── Worker.cs ├── appsettings.json └── wwwroot │ ├── css │ └── site.css │ ├── favicon.ico │ ├── images │ ├── securitykey.min.svg │ └── securitykey.svg │ ├── js │ ├── helpers.js │ ├── instant.js │ ├── mfa.login.js │ ├── mfa.register.js │ └── site.js │ └── lib │ ├── bootstrap │ ├── LICENSE │ └── dist │ │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ │ └── js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map │ ├── jquery-validation-unobtrusive │ ├── LICENSE.txt │ ├── jquery.validate.unobtrusive.js │ └── jquery.validate.unobtrusive.min.js │ ├── jquery-validation │ ├── LICENSE.md │ └── dist │ │ ├── additional-methods.js │ │ ├── additional-methods.min.js │ │ ├── jquery.validate.js │ │ └── jquery.validate.min.js │ └── jquery │ ├── LICENSE.txt │ └── dist │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map ├── LICENSE ├── Old ├── BlazorServerOidc │ ├── App.razor │ ├── BlazorServerOidc.csproj │ ├── Controllers │ │ └── AccountController.cs │ ├── Data │ │ ├── WeatherForecast.cs │ │ └── WeatherForecastService.cs │ ├── Pages │ │ ├── Counter.razor │ │ ├── Error.cshtml │ │ ├── Error.cshtml.cs │ │ ├── FetchData.razor │ │ ├── Index.razor │ │ ├── SignedOut.cshtml │ │ ├── SignedOut.cshtml.cs │ │ ├── _Host.cshtml │ │ └── _Layout.cshtml │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── SecurityHeadersDefinitions.cs │ ├── Shared │ │ ├── LoginLogoutMenu.razor │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ ├── NavMenu.razor.css │ │ └── SurveyPrompt.razor │ ├── _Imports.razor │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ │ ├── .well-known │ │ └── security.txt │ │ ├── css │ │ ├── bootstrap │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ ├── open-iconic │ │ │ ├── FONT-LICENSE │ │ │ ├── ICON-LICENSE │ │ │ ├── README.md │ │ │ └── font │ │ │ │ ├── css │ │ │ │ └── open-iconic-bootstrap.min.css │ │ │ │ └── fonts │ │ │ │ ├── open-iconic.eot │ │ │ │ ├── open-iconic.otf │ │ │ │ ├── open-iconic.svg │ │ │ │ ├── open-iconic.ttf │ │ │ │ └── open-iconic.woff │ │ └── site.css │ │ ├── favicon.ico │ │ └── js │ │ ├── blazor.bootstrap.js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.esm.js │ │ ├── bootstrap.esm.js.map │ │ ├── bootstrap.esm.min.js │ │ ├── bootstrap.esm.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ ├── bootstrap.min.js.map │ │ └── popper.min.js └── BlazorWebFromBlazorServerOidc │ ├── App.razor │ ├── BlazorNonceService.cs │ ├── BlazorWebFromBlazorServerOidc.csproj │ ├── Controllers │ └── AccountController.cs │ ├── Data │ ├── WeatherForecast.cs │ └── WeatherForecastService.cs │ ├── Layout │ ├── LogInOrOut.razor │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ ├── NavMenu.razor │ └── NavMenu.razor.css │ ├── NonceMiddleware.cs │ ├── Pages │ ├── Counter.razor │ ├── Error.cshtml │ ├── Error.cshtml.cs │ ├── FetchData.razor │ ├── Index.razor │ ├── SignedOut.cshtml │ └── SignedOut.cshtml.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Routes.razor │ ├── SecurityHeadersDefinitions.cs │ ├── _Imports.razor │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ ├── .well-known │ └── security.txt │ ├── css │ ├── bootstrap │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ ├── open-iconic │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font │ │ │ ├── css │ │ │ └── open-iconic-bootstrap.min.css │ │ │ └── fonts │ │ │ ├── open-iconic.eot │ │ │ ├── open-iconic.otf │ │ │ ├── open-iconic.svg │ │ │ ├── open-iconic.ttf │ │ │ └── open-iconic.woff │ └── site.css │ ├── favicon.ico │ └── js │ ├── blazor.bootstrap.js │ ├── bootstrap.bundle.js │ ├── bootstrap.bundle.js.map │ ├── bootstrap.bundle.min.js │ ├── bootstrap.bundle.min.js.map │ ├── bootstrap.esm.js │ ├── bootstrap.esm.js.map │ ├── bootstrap.esm.min.js │ ├── bootstrap.esm.min.js.map │ ├── bootstrap.js │ ├── bootstrap.js.map │ ├── bootstrap.min.js │ ├── bootstrap.min.js.map │ └── popper.min.js └── README.md /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: 9.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | - name: Test 28 | run: dotnet test --no-build --verbosity normal 29 | -------------------------------------------------------------------------------- /BlazorServerOidc.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34330.188 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentityProvider", "IdentityProvider\IdentityProvider.csproj", "{763C2B8D-87E1-4289-9F19-FE2834FA62F7}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2826EED1-B486-46EF-9D22-771A0FABC003}" 9 | ProjectSection(SolutionItems) = preProject 10 | .github\workflows\dotnet.yml = .github\workflows\dotnet.yml 11 | README.md = README.md 12 | EndProjectSection 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Old", "Old", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorServerOidc", "Old\BlazorServerOidc\BlazorServerOidc.csproj", "{EE564E05-E433-7D2C-B719-4000E6B67CA7}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorWebFromBlazorServerOidc", "Old\BlazorWebFromBlazorServerOidc\BlazorWebFromBlazorServerOidc.csproj", "{FD991A49-4041-920D-D635-E5FEA13A87CE}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorWebApp", "BlazorWebApp\BlazorWebApp.csproj", "{2BA1FED1-8B11-44D8-A902-FF3EBF51DDEB}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {763C2B8D-87E1-4289-9F19-FE2834FA62F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {763C2B8D-87E1-4289-9F19-FE2834FA62F7}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {763C2B8D-87E1-4289-9F19-FE2834FA62F7}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {763C2B8D-87E1-4289-9F19-FE2834FA62F7}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {EE564E05-E433-7D2C-B719-4000E6B67CA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {EE564E05-E433-7D2C-B719-4000E6B67CA7}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {EE564E05-E433-7D2C-B719-4000E6B67CA7}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {EE564E05-E433-7D2C-B719-4000E6B67CA7}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {FD991A49-4041-920D-D635-E5FEA13A87CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {FD991A49-4041-920D-D635-E5FEA13A87CE}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {FD991A49-4041-920D-D635-E5FEA13A87CE}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {FD991A49-4041-920D-D635-E5FEA13A87CE}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {2BA1FED1-8B11-44D8-A902-FF3EBF51DDEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {2BA1FED1-8B11-44D8-A902-FF3EBF51DDEB}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {2BA1FED1-8B11-44D8-A902-FF3EBF51DDEB}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {2BA1FED1-8B11-44D8-A902-FF3EBF51DDEB}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {EE564E05-E433-7D2C-B719-4000E6B67CA7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} 50 | {FD991A49-4041-920D-D635-E5FEA13A87CE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {F612A4DE-7883-4FA5-AF96-8E18D8DFCE59} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /BlazorWebApp/BlazorWebApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | @code 24 | { 25 | public string? Nonce => HttpContextAccessor?.HttpContext?.GetNonce(); 26 | [Inject] private IHttpContextAccessor? HttpContextAccessor { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @inject NavigationManager NavigationManager 3 | 4 |
5 | 8 | 9 |
10 | 11 | 12 |
13 | @context.User.Identity?.Name 14 |
15 | 24 |
25 | 26 |
27 | 28 | Login 29 | 30 |
31 |
32 |
33 | 34 |
35 | @Body 36 |
37 |
38 |
39 | 40 |
41 | An unhandled error has occurred. 42 | Reload 43 | 🗙 44 |
45 | 46 | @code { 47 | private string? currentUrl; 48 | 49 | protected override void OnInitialized() 50 | { 51 | currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); 52 | NavigationManager.LocationChanged += OnLocationChanged; 53 | } 54 | 55 | private void OnLocationChanged(object? sender, LocationChangedEventArgs e) 56 | { 57 | currentUrl = NavigationManager.ToBaseRelativePath(e.Location); 58 | StateHasChanged(); 59 | } 60 | 61 | public void Dispose() 62 | { 63 | NavigationManager.LocationChanged -= OnLocationChanged; 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/Layout/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 { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | color-scheme: light only; 81 | background: lightyellow; 82 | bottom: 0; 83 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 84 | box-sizing: border-box; 85 | display: none; 86 | left: 0; 87 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 88 | position: fixed; 89 | width: 100%; 90 | z-index: 1000; 91 | } 92 | 93 | #blazor-error-ui .dismiss { 94 | cursor: pointer; 95 | position: absolute; 96 | right: 0.75rem; 97 | top: 0.5rem; 98 | } 99 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | 8 | 9 | 10 | 11 | 55 | 56 | @code { 57 | private string? currentUrl; 58 | 59 | protected override void OnInitialized() 60 | { 61 | currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); 62 | NavigationManager.LocationChanged += OnLocationChanged; 63 | } 64 | 65 | private void OnLocationChanged(object? sender, LocationChangedEventArgs e) 66 | { 67 | currentUrl = NavigationManager.ToBaseRelativePath(e.Location); 68 | StateHasChanged(); 69 | } 70 | 71 | public void Dispose() 72 | { 73 | NavigationManager.LocationChanged -= OnLocationChanged; 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/Layout/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | appearance: none; 3 | cursor: pointer; 4 | width: 3.5rem; 5 | height: 2.5rem; 6 | color: white; 7 | position: absolute; 8 | top: 0.5rem; 9 | right: 1rem; 10 | border: 1px solid rgba(255, 255, 255, 0.1); 11 | background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); 12 | } 13 | 14 | .navbar-toggler:checked { 15 | background-color: rgba(255, 255, 255, 0.5); 16 | } 17 | 18 | .top-row { 19 | min-height: 3.5rem; 20 | background-color: rgba(0,0,0,0.4); 21 | } 22 | 23 | .navbar-brand { 24 | font-size: 1.1rem; 25 | } 26 | 27 | .bi { 28 | display: inline-block; 29 | position: relative; 30 | width: 1.25rem; 31 | height: 1.25rem; 32 | margin-right: 0.75rem; 33 | top: -1px; 34 | background-size: cover; 35 | } 36 | 37 | .bi-house-door-fill-nav-menu { 38 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); 39 | } 40 | 41 | .bi-plus-square-fill-nav-menu { 42 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); 43 | } 44 | 45 | .bi-list-nested-nav-menu { 46 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); 47 | } 48 | 49 | .nav-item { 50 | font-size: 0.9rem; 51 | padding-bottom: 0.5rem; 52 | } 53 | 54 | .nav-item:first-of-type { 55 | padding-top: 1rem; 56 | } 57 | 58 | .nav-item:last-of-type { 59 | padding-bottom: 1rem; 60 | } 61 | 62 | .nav-item ::deep .nav-link { 63 | color: #d7d7d7; 64 | background: none; 65 | border: none; 66 | border-radius: 4px; 67 | height: 3rem; 68 | display: flex; 69 | align-items: center; 70 | line-height: 3rem; 71 | width: 100%; 72 | } 73 | 74 | .nav-item ::deep a.active { 75 | background-color: rgba(255,255,255,0.37); 76 | color: white; 77 | } 78 | 79 | .nav-item ::deep .nav-link:hover { 80 | background-color: rgba(255,255,255,0.1); 81 | color: white; 82 | } 83 | 84 | .nav-scrollable { 85 | display: none; 86 | } 87 | 88 | .navbar-toggler:checked ~ .nav-scrollable { 89 | display: block; 90 | } 91 | 92 | @media (min-width: 641px) { 93 | .navbar-toggler { 94 | display: none; 95 | } 96 | 97 | .nav-scrollable { 98 | /* Never collapse the sidebar for wide screens */ 99 | display: block; 100 | 101 | /* Allow sidebar to scroll for tall menus */ 102 | height: calc(100vh - 3.5rem); 103 | overflow-y: auto; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/Layout/RedirectToLogin.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager Navigation 2 | @code { 3 | protected override void OnInitialized() 4 | { 5 | Navigation.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}", true); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | @rendermode InteractiveServer 3 | 4 | Counter 5 | 6 |

Counter

7 | 8 |

Current count: @currentCount

9 | 10 | 11 | 12 | @code { 13 | private int currentCount = 0; 14 | 15 | private void IncrementCount() 16 | { 17 | currentCount++; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | 27 | @code{ 28 | [CascadingParameter] 29 | private HttpContext? HttpContext { get; set; } 30 | 31 | private string? RequestId { get; set; } 32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Home 4 | 5 |

Hello, world!

6 | 7 | Welcome to your new app. 8 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/Pages/Weather.razor: -------------------------------------------------------------------------------- 1 | @page "/weather" 2 | @attribute [StreamRendering] 3 | 4 | Weather 5 | 6 |

Weather

7 | 8 |

This component demonstrates showing data.

9 | 10 | @if (forecasts == null) 11 | { 12 |

Loading...

13 | } 14 | else 15 | { 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @foreach (var forecast in forecasts) 27 | { 28 | 29 | 30 | 31 | 32 | 33 | 34 | } 35 | 36 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
37 | } 38 | 39 | @code { 40 | private WeatherForecast[]? forecasts; 41 | 42 | protected override async Task OnInitializedAsync() 43 | { 44 | // Simulate asynchronous loading to demonstrate streaming rendering 45 | await Task.Delay(500); 46 | 47 | var startDate = DateOnly.FromDateTime(DateTime.Now); 48 | var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; 49 | forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast 50 | { 51 | Date = startDate.AddDays(index), 52 | TemperatureC = Random.Shared.Next(-20, 55), 53 | Summary = summaries[Random.Shared.Next(summaries.Length)] 54 | }).ToArray(); 55 | } 56 | 57 | private class WeatherForecast 58 | { 59 | public DateOnly Date { get; set; } 60 | public int TemperatureC { get; set; } 61 | public string? Summary { get; set; } 62 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/Routes.razor: -------------------------------------------------------------------------------- 1 | @using BlazorWebApp.Components.Layout 2 | 3 | 4 | 5 | 6 | Loading 7 | 8 | 9 | @if (!context.User.Identity!.IsAuthenticated) 10 | { 11 | 12 | } 13 | else 14 | { 15 |

You are not authorized to access this resource.

16 | } 17 |
18 |
19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /BlazorWebApp/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using BlazorWebApp 2 | @using BlazorWebApp.Components 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using Microsoft.AspNetCore.Authorization 10 | @using Microsoft.AspNetCore.Components; 11 | @using Microsoft.AspNetCore.Components.Authorization 12 | @using Microsoft.AspNetCore.OutputCaching 13 | @using Microsoft.AspNetCore.Components.Sections 14 | @using NetEscapades.AspNetCore.SecurityHeaders 15 | @using System.Net.Http 16 | @using System.Net.Http.Json 17 | -------------------------------------------------------------------------------- /BlazorWebApp/MapLoginLogoutEndpoints.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 4 | using Microsoft.Extensions.Primitives; 5 | 6 | namespace BlazorWebApp; 7 | 8 | public static class LoginLogoutEndpoints 9 | { 10 | public static WebApplication MapLoginLogoutEndpoints(this WebApplication app) 11 | { 12 | app.MapGet("/login", async context => 13 | { 14 | var returnUrl = context.Request.Query["returnUrl"]; 15 | 16 | await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties 17 | { 18 | RedirectUri = returnUrl == StringValues.Empty ? "/" : returnUrl.ToString() 19 | }); 20 | }).AllowAnonymous(); 21 | 22 | app.MapPost("/logout", async context => 23 | { 24 | if (context.User.Identity?.IsAuthenticated ?? false) 25 | { 26 | await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 27 | await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); 28 | } 29 | else 30 | { 31 | context.Response.Redirect("/"); 32 | } 33 | }); 34 | 35 | return app; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /BlazorWebApp/Program.cs: -------------------------------------------------------------------------------- 1 | using BlazorWebApp.Components; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 4 | using Microsoft.IdentityModel.Logging; 5 | using Microsoft.IdentityModel.Protocols.OpenIdConnect; 6 | using Microsoft.IdentityModel.Tokens; 7 | 8 | namespace BlazorWebApp; 9 | 10 | public class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | var builder = WebApplication.CreateBuilder(args); 15 | 16 | // Add services to the container. 17 | builder.Services.AddRazorComponents() 18 | .AddInteractiveServerComponents(); 19 | 20 | builder.Services.AddHttpContextAccessor(); 21 | 22 | var oidcConfig = builder.Configuration.GetSection("OpenIDConnectSettings"); 23 | 24 | builder.Services.AddAuthentication(options => 25 | { 26 | options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 27 | options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; 28 | options.DefaultSignOutScheme = OpenIdConnectDefaults.AuthenticationScheme; 29 | }) 30 | .AddCookie(options => 31 | { 32 | options.Cookie.Name = "__Host-blazorwebapp"; 33 | options.Cookie.SameSite = SameSiteMode.Lax; 34 | // can be strict if same-site 35 | //options.Cookie.SameSite = SameSiteMode.Strict; 36 | }) 37 | .AddOpenIdConnect(options => 38 | { 39 | builder.Configuration.GetSection("OpenIDConnectSettings").Bind(options); 40 | 41 | options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; 42 | options.ResponseType = OpenIdConnectResponseType.Code; 43 | 44 | options.SaveTokens = true; 45 | options.GetClaimsFromUserInfoEndpoint = true; 46 | options.TokenValidationParameters = new TokenValidationParameters 47 | { 48 | NameClaimType = "name" 49 | }; 50 | }); 51 | 52 | builder.Services.AddAntiforgery(options => 53 | { 54 | options.HeaderName = "X-XSRF-TOKEN"; 55 | options.Cookie.Name = "__Host-core-X-XSRF-TOKEN"; 56 | options.Cookie.SameSite = SameSiteMode.Strict; 57 | options.Cookie.SecurePolicy = CookieSecurePolicy.Always; 58 | }); 59 | 60 | builder.Services.AddSecurityHeaderPolicies() 61 | .SetDefaultPolicy(SecurityHeadersDefinitions 62 | .GetHeaderPolicyCollection(oidcConfig["Authority"], 63 | builder.Environment.IsDevelopment())); 64 | 65 | builder.Services.AddAuthenticationCore(); 66 | builder.Services.AddAuthorization(); 67 | builder.Services.AddCascadingAuthenticationState(); 68 | 69 | var app = builder.Build(); 70 | 71 | if (!app.Environment.IsDevelopment()) 72 | { 73 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 74 | app.UseHsts(); 75 | } 76 | else 77 | { 78 | IdentityModelEventSource.ShowPII = true; 79 | IdentityModelEventSource.LogCompleteSecurityArtifact = true; 80 | } 81 | 82 | app.UseSecurityHeaders(); 83 | 84 | app.UseHttpsRedirection(); 85 | app.UseAntiforgery(); 86 | app.UseAuthentication(); 87 | app.UseAuthorization(); 88 | 89 | app.MapStaticAssets(); 90 | app.MapRazorComponents() 91 | .AddInteractiveServerRenderMode(); 92 | 93 | app.MapLoginLogoutEndpoints(); 94 | 95 | app.Run(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /BlazorWebApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "BlazorWeb": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "https://localhost:5001", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BlazorWebApp/SecurityHeadersDefinitions.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorWebApp; 2 | 3 | using Microsoft.AspNetCore.Builder; 4 | 5 | public static class SecurityHeadersDefinitions 6 | { 7 | public static HeaderPolicyCollection GetHeaderPolicyCollection(string? idpHost, bool isDev) 8 | { 9 | ArgumentNullException.ThrowIfNull(idpHost); 10 | 11 | var policy = new HeaderPolicyCollection() 12 | .AddFrameOptionsDeny() 13 | .AddContentTypeOptionsNoSniff() 14 | .AddReferrerPolicyStrictOriginWhenCrossOrigin() 15 | .AddCrossOriginOpenerPolicy(builder => builder.SameOrigin()) 16 | .AddCrossOriginResourcePolicy(builder => builder.SameOrigin()) 17 | // #if !DEBUG // remove for dev if using Visual studio development hot reload 18 | .AddCrossOriginEmbedderPolicy(builder => builder.RequireCorp()) 19 | // #endif 20 | .AddContentSecurityPolicy(builder => 21 | { 22 | builder.AddObjectSrc().None(); 23 | builder.AddBlockAllMixedContent(); 24 | builder.AddImgSrc().Self().From("data:"); 25 | builder.AddFormAction().Self().From(idpHost); 26 | builder.AddFontSrc().Self(); 27 | builder.AddStyleSrc().Self().UnsafeInline(); 28 | builder.AddBaseUri().Self(); 29 | builder.AddFrameAncestors().None(); 30 | 31 | // #if !DEBUG // remove for Visual studio development 32 | builder.AddScriptSrc().WithNonce().UnsafeInline(); 33 | // #endif 34 | }) 35 | .RemoveServerHeader() 36 | .AddPermissionsPolicyWithDefaultSecureDirectives(); 37 | 38 | if (!isDev) 39 | { 40 | // maxage = one year in seconds 41 | policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(); 42 | } 43 | 44 | return policy; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /BlazorWebApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazorWebApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "OpenIDConnectSettings": { 9 | "Authority": "https://localhost:44318", 10 | "ClientId": "oidc-pkce-confidential", 11 | "ClientSecret": "oidc-pkce-confidential_secret" 12 | }, 13 | "AllowedHosts": "*" 14 | } 15 | -------------------------------------------------------------------------------- /BlazorWebApp/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 17 | } 18 | 19 | .content { 20 | padding-top: 1.1rem; 21 | } 22 | 23 | h1:focus { 24 | outline: none; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid #e50000; 33 | } 34 | 35 | .validation-message { 36 | color: #e50000; 37 | } 38 | 39 | .blazor-error-boundary { 40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 41 | padding: 1rem 1rem 1rem 3.7rem; 42 | color: white; 43 | } 44 | 45 | .blazor-error-boundary::after { 46 | content: "An error has occurred." 47 | } 48 | 49 | .darker-border-checkbox.form-check-input { 50 | border-color: #929292; 51 | } 52 | 53 | .form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { 54 | color: var(--bs-secondary-color); 55 | text-align: end; 56 | } 57 | 58 | .form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { 59 | text-align: start; 60 | } -------------------------------------------------------------------------------- /BlazorWebApp/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/BlazorWebApp/wwwroot/favicon.png -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/IdentityHostingStartup.cs: -------------------------------------------------------------------------------- 1 | [assembly: HostingStartup(typeof(OpeniddictServer.Areas.Identity.IdentityHostingStartup))] 2 | namespace OpeniddictServer.Areas.Identity 3 | { 4 | public class IdentityHostingStartup : IHostingStartup 5 | { 6 | public void Configure(IWebHostBuilder builder) 7 | { 8 | builder.ConfigureServices((context, services) => 9 | { 10 | }); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LoginModel 3 | 4 | @{ 5 | ViewData["Title"] = "Log in"; 6 | } 7 | 8 |

@ViewData["Title"]

9 |
10 |
11 |
12 |
13 |

Use a local account to log in.

14 |
15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 |
28 | 32 |
33 |
34 |
35 | 36 |
37 |
38 |

39 | Forgot your password? 40 |

41 |

42 | Register as a new user 43 |

44 |

45 | Resend email confirmation 46 |

47 |
48 |
49 |
50 |
51 |
52 |
53 |

Use another service to log in.

54 |
55 | @{ 56 | if ((Model.ExternalLogins?.Count ?? 0) == 0) 57 | { 58 |
59 |

60 | There are no external authentication services configured. See this article 61 | about setting up this ASP.NET application to support logging in via external services. 62 |

63 |
64 | } 65 | else 66 | { 67 |
68 |
69 |

70 | @foreach (var provider in Model.ExternalLogins) 71 | { 72 | 73 | } 74 |

75 |
76 |
77 | } 78 | } 79 |
80 |
81 |
82 | 83 | @section Scripts { 84 | 85 | 86 | } 87 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/LoginFido2Mfa.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @using Microsoft.AspNetCore.Identity 4 | @inject SignInManager SignInManager 5 | @inject UserManager UserManager 6 | @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf 7 | @functions{ 8 | public string? GetAntiXsrfRequestToken() 9 | { 10 | return Xsrf.GetAndStoreTokens(this.HttpContext).RequestToken; 11 | } 12 | } 13 | @model OpeniddictServer.Areas.Identity.Pages.Account.MfaModel 14 | @{ 15 | ViewData["Title"] = "Login with Fido2 MFA"; 16 | } 17 | 18 |

@ViewData["Title"]

19 |
20 |
21 |

2FA/MFA

22 |

This is scenario where we just want to use FIDO as the MFA. The user register and logins with their username and password. For demo purposes, we trigger the MFA registering on sign up.

23 | 26 | 27 |
28 |
29 | 30 |

Fido2 2FA

31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 |
41 | 42 |
43 | 44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/LoginFido2Mfa.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace OpeniddictServer.Areas.Identity.Pages.Account; 6 | 7 | [AllowAnonymous] 8 | public class MfaModel : PageModel 9 | { 10 | [BindProperty(SupportsGet = true)] 11 | public bool RememberMe { get; set; } 12 | 13 | [BindProperty(SupportsGet = true)] 14 | public string? ReturnUrl { get; set; } = string.Empty; 15 | 16 | public void OnGet() 17 | { 18 | } 19 | 20 | public void OnPost() 21 | { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Disable2faModel 3 | @{ 4 | ViewData["Title"] = "Disable two-factor authentication (2FA)"; 5 | ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; 6 | } 7 | 8 | 9 |

@ViewData["Title"]

10 | 11 | 20 | 21 |
22 |
23 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | #nullable disable 4 | 5 | using Fido2Identity; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using OpeniddictServer.Data; 10 | 11 | namespace OpeniddictServer.Areas.Identity.Pages.Account.Manage 12 | { 13 | public class Disable2faModel : PageModel 14 | { 15 | private readonly UserManager _userManager; 16 | private readonly ILogger _logger; 17 | private readonly Fido2Store _fido2Store; 18 | 19 | public Disable2faModel( 20 | UserManager userManager, 21 | Fido2Store fido2Store, 22 | ILogger logger) 23 | { 24 | _userManager = userManager; 25 | _fido2Store = fido2Store; 26 | _logger = logger; 27 | } 28 | 29 | /// 30 | /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used 31 | /// directly from your code. This API may change or be removed in future releases. 32 | /// 33 | [TempData] 34 | public string StatusMessage { get; set; } 35 | 36 | public async Task OnGet() 37 | { 38 | var user = await _userManager.GetUserAsync(User); 39 | if (user == null) 40 | { 41 | return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 42 | } 43 | 44 | if (!await _userManager.GetTwoFactorEnabledAsync(user)) 45 | { 46 | throw new InvalidOperationException($"Cannot disable 2FA for user as it's not currently enabled."); 47 | } 48 | 49 | return Page(); 50 | } 51 | 52 | public async Task OnPostAsync() 53 | { 54 | var user = await _userManager.GetUserAsync(User); 55 | if (user == null) 56 | { 57 | return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 58 | } 59 | 60 | // remove Fido2 MFA if it exists 61 | await _fido2Store.RemoveCredentialsByUserNameAsync(user.UserName); 62 | 63 | var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); 64 | if (!disable2faResult.Succeeded) 65 | { 66 | throw new InvalidOperationException($"Unexpected error occurred disabling 2FA."); 67 | } 68 | 69 | _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User)); 70 | StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app"; 71 | return RedirectToPage("./TwoFactorAuthentication"); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/Manage/Fido2Mfa.cshtml: -------------------------------------------------------------------------------- 1 | @page "/Fido2Mfa/{handler?}" 2 | @using Microsoft.AspNetCore.Identity 3 | @inject SignInManager SignInManager 4 | @inject UserManager UserManager 5 | @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf 6 | @functions{ 7 | public string? GetAntiXsrfRequestToken() 8 | { 9 | return Xsrf.GetAndStoreTokens(this.HttpContext).RequestToken; 10 | } 11 | } 12 | @model OpeniddictServer.Areas.Identity.Pages.Account.Manage.MfaModel 13 | @{ 14 | Layout = "_Layout.cshtml"; 15 | ViewData["Title"] = "Two-factor authentication (2FA)"; 16 | ViewData["ActivePage"] = ManageNavPages.Fido2Mfa; 17 | } 18 | 19 |

@ViewData["Title"]

20 |
21 |
22 |

2FA/MFA

23 |

This is scenario where we just want to use FIDO as the MFA. The user register and logins with their username and password. For demo purposes, we trigger the MFA registering on sign up.

24 | 27 | 28 |
29 |
30 | 31 |

Add a Fido2 MFA

32 |
33 | 34 |
35 | 36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 | 50 | 51 |
52 | 53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/Manage/Fido2Mfa.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace OpeniddictServer.Areas.Identity.Pages.Account.Manage; 4 | 5 | public class MfaModel : PageModel 6 | { 7 | public void OnGet() 8 | { 9 | } 10 | 11 | public void OnPost() 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @using Microsoft.AspNetCore.Http.Features 3 | @model TwoFactorAuthenticationModel 4 | @{ 5 | ViewData["Title"] = "Two-factor authentication (2FA)"; 6 | ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; 7 | } 8 | 9 | 10 |

@ViewData["Title"]

11 | @{ 12 | var consentFeature = HttpContext.Features.Get(); 13 | @if (consentFeature?.CanTrack ?? true) 14 | { 15 | @if (Model.Is2faEnabled) 16 | { 17 | if (Model.RecoveryCodesLeft == 0) 18 | { 19 |
20 | You have no recovery codes left. 21 |

You must generate a new set of recovery codes before you can log in with a recovery code.

22 |
23 | } 24 | else if (Model.RecoveryCodesLeft == 1) 25 | { 26 |
27 | You have 1 recovery code left. 28 |

You can generate a new set of recovery codes.

29 |
30 | } 31 | else if (Model.RecoveryCodesLeft <= 3) 32 | { 33 |
34 | You have @Model.RecoveryCodesLeft recovery codes left. 35 |

You should generate a new set of recovery codes.

36 |
37 | } 38 | 39 | if (Model.IsMachineRemembered) 40 | { 41 |
42 | 43 |
44 | } 45 | Disable 2FA 46 | Reset recovery codes 47 | } 48 | 49 |

Authenticator app

50 | @if (!Model.HasAuthenticator) 51 | { 52 | Add authenticator app 53 | } 54 | else 55 | { 56 | Set up authenticator app 57 | Reset authenticator app 58 | } 59 | } 60 | else 61 | { 62 |
63 | Privacy and cookie policy have not been accepted. 64 |

You must accept the policy before you can enable two factor authentication.

65 |
66 | } 67 | } 68 | 69 | Add Fido2 MFA 70 | 71 | @section Scripts { 72 | 73 | 74 | } 75 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | #nullable disable 4 | 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using OpeniddictServer.Data; 9 | 10 | namespace OpeniddictServer.Areas.Identity.Pages.Account.Manage 11 | { 12 | public class TwoFactorAuthenticationModel : PageModel 13 | { 14 | private readonly UserManager _userManager; 15 | private readonly SignInManager _signInManager; 16 | 17 | public TwoFactorAuthenticationModel( 18 | UserManager userManager, SignInManager signInManager) 19 | { 20 | _userManager = userManager; 21 | _signInManager = signInManager; 22 | } 23 | 24 | /// 25 | /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used 26 | /// directly from your code. This API may change or be removed in future releases. 27 | /// 28 | public bool HasAuthenticator { get; set; } 29 | 30 | /// 31 | /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used 32 | /// directly from your code. This API may change or be removed in future releases. 33 | /// 34 | public int RecoveryCodesLeft { get; set; } 35 | 36 | /// 37 | /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used 38 | /// directly from your code. This API may change or be removed in future releases. 39 | /// 40 | [BindProperty] 41 | public bool Is2faEnabled { get; set; } 42 | 43 | /// 44 | /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used 45 | /// directly from your code. This API may change or be removed in future releases. 46 | /// 47 | public bool IsMachineRemembered { get; set; } 48 | 49 | /// 50 | /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used 51 | /// directly from your code. This API may change or be removed in future releases. 52 | /// 53 | [TempData] 54 | public string StatusMessage { get; set; } 55 | 56 | public async Task OnGetAsync() 57 | { 58 | var user = await _userManager.GetUserAsync(User); 59 | if (user == null) 60 | { 61 | return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 62 | } 63 | 64 | HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null; 65 | Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user); 66 | IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user); 67 | RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user); 68 | 69 | return Page(); 70 | } 71 | 72 | public async Task OnPostAsync() 73 | { 74 | var user = await _userManager.GetUserAsync(User); 75 | if (user == null) 76 | { 77 | return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 78 | } 79 | 80 | await _signInManager.ForgetTwoFactorClientAsync(); 81 | StatusMessage = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code."; 82 | return RedirectToPage(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml: -------------------------------------------------------------------------------- 1 | @inject SignInManager SignInManager 2 | @{ 3 | var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); 4 | } 5 | 16 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using OpeniddictServer.Areas.Identity.Pages.Account.Manage 2 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/Account/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using OpeniddictServer.Areas.Identity.Pages.Account -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using OpeniddictServer.Areas.Identity 3 | @using OpeniddictServer.Areas.Identity.Pages 4 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 5 | @using OpeniddictServer.Data 6 | -------------------------------------------------------------------------------- /IdentityProvider/Areas/Identity/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 |  2 | @{ 3 | Layout = "/Views/Shared/_Layout.cshtml"; 4 | } 5 | -------------------------------------------------------------------------------- /IdentityProvider/Controllers/ErrorController.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | * See https://github.com/openiddict/openiddict-core for more information concerning 4 | * the license and the contributors participating to this project. 5 | */ 6 | 7 | using Microsoft.AspNetCore; 8 | using Microsoft.AspNetCore.Mvc; 9 | using OpeniddictServer.ViewModels.Shared; 10 | 11 | namespace OpeniddictServer.Controllers; 12 | 13 | public class ErrorController : Controller 14 | { 15 | [Route("error")] 16 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 17 | public IActionResult Error() 18 | { 19 | // If the error was not caused by an invalid 20 | // OIDC request, display a generic error page. 21 | var response = HttpContext.GetOpenIddictServerResponse(); 22 | if (response == null) 23 | { 24 | return View(new ErrorViewModel()); 25 | } 26 | 27 | return View(new ErrorViewModel 28 | { 29 | Error = response.Error, 30 | ErrorDescription = response.ErrorDescription 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /IdentityProvider/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace OpeniddictServer.Controllers; 4 | 5 | public class HomeController : Controller 6 | { 7 | private readonly ILogger _logger; 8 | 9 | public HomeController(ILogger logger) 10 | { 11 | _logger = logger; 12 | } 13 | 14 | public IActionResult Index() 15 | { 16 | return View(); 17 | } 18 | 19 | public IActionResult Privacy() 20 | { 21 | return View(); 22 | } 23 | 24 | public IActionResult Error() 25 | { 26 | return View(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /IdentityProvider/Controllers/ResourceController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OpenIddict.Validation.AspNetCore; 5 | using OpeniddictServer.Data; 6 | 7 | namespace OpeniddictServer.Controllers; 8 | 9 | [Route("api")] 10 | public class ResourceController : Controller 11 | { 12 | private readonly UserManager _userManager; 13 | 14 | public ResourceController(UserManager userManager) 15 | => _userManager = userManager; 16 | 17 | [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] 18 | [HttpGet("message")] 19 | public async Task GetMessage() 20 | { 21 | var user = await _userManager.GetUserAsync(User); 22 | if (user == null) 23 | { 24 | return BadRequest(); 25 | } 26 | 27 | return Content($"{user.UserName} has been successfully authenticated."); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /IdentityProvider/Controllers/UserinfoController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Mvc; 5 | using OpenIddict.Abstractions; 6 | using OpenIddict.Server.AspNetCore; 7 | using OpeniddictServer.Data; 8 | using static OpenIddict.Abstractions.OpenIddictConstants; 9 | 10 | namespace OpeniddictServer.Controllers; 11 | 12 | public class UserinfoController : Controller 13 | { 14 | private readonly UserManager _userManager; 15 | 16 | public UserinfoController(UserManager userManager) 17 | => _userManager = userManager; 18 | 19 | // 20 | // GET: /api/userinfo 21 | [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] 22 | [HttpGet("~/connect/userinfo"), HttpPost("~/connect/userinfo"), Produces("application/json")] 23 | public async Task Userinfo() 24 | { 25 | var user = await _userManager.GetUserAsync(User); 26 | if (user == null) 27 | { 28 | return Challenge( 29 | authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, 30 | properties: new AuthenticationProperties(new Dictionary 31 | { 32 | [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken, 33 | [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = 34 | "The specified access token is bound to an account that no longer exists." 35 | })); 36 | } 37 | 38 | var claims = new Dictionary(StringComparer.Ordinal) 39 | { 40 | // Note: the "sub" claim is a mandatory claim and must be included in the JSON response. 41 | [Claims.Subject] = await _userManager.GetUserIdAsync(user) 42 | }; 43 | 44 | if (User.HasScope(Scopes.Email)) 45 | { 46 | claims[Claims.Email] = await _userManager.GetEmailAsync(user); 47 | claims[Claims.EmailVerified] = await _userManager.IsEmailConfirmedAsync(user); 48 | } 49 | 50 | if (User.HasScope(Scopes.Phone)) 51 | { 52 | claims[Claims.PhoneNumber] = await _userManager.GetPhoneNumberAsync(user); 53 | claims[Claims.PhoneNumberVerified] = await _userManager.IsPhoneNumberConfirmedAsync(user); 54 | } 55 | 56 | if (User.HasScope(Scopes.Roles)) 57 | { 58 | claims[Claims.Role] = await _userManager.GetRolesAsync(user); 59 | } 60 | 61 | // Note: the complete list of standard claims supported by the OpenID Connect specification 62 | // can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims 63 | 64 | return Ok(claims); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /IdentityProvider/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Fido2Identity; 2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace OpeniddictServer.Data; 6 | 7 | public class ApplicationDbContext : IdentityDbContext 8 | { 9 | public ApplicationDbContext(DbContextOptions options) 10 | : base(options) 11 | { 12 | } 13 | 14 | public DbSet FidoStoredCredential => Set(); 15 | 16 | protected override void OnModelCreating(ModelBuilder builder) 17 | { 18 | builder.Entity().HasKey(m => m.Id); 19 | 20 | base.OnModelCreating(builder); 21 | } 22 | } -------------------------------------------------------------------------------- /IdentityProvider/Data/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace OpeniddictServer.Data; 4 | 5 | // Add profile data for application users by adding properties to the ApplicationUser class 6 | public class ApplicationUser : IdentityUser { } 7 | -------------------------------------------------------------------------------- /IdentityProvider/Fido2/Fido2Store.cs: -------------------------------------------------------------------------------- 1 | using Fido2NetLib; 2 | using Microsoft.EntityFrameworkCore; 3 | using OpeniddictServer.Data; 4 | using System.Text; 5 | 6 | namespace Fido2Identity; 7 | 8 | public class Fido2Store 9 | { 10 | private readonly ApplicationDbContext _applicationDbContext; 11 | 12 | public Fido2Store(ApplicationDbContext applicationDbContext) 13 | { 14 | _applicationDbContext = applicationDbContext; 15 | } 16 | 17 | public async Task> GetCredentialsByUserNameAsync(string username) 18 | { 19 | return await _applicationDbContext.FidoStoredCredential.Where(c => c.UserName == username).ToListAsync(); 20 | } 21 | 22 | public async Task RemoveCredentialsByUserNameAsync(string username) 23 | { 24 | var items = await _applicationDbContext.FidoStoredCredential.Where(c => c.UserName == username).ToListAsync(); 25 | if (items != null) 26 | { 27 | foreach (var fido2Key in items) 28 | { 29 | _applicationDbContext.FidoStoredCredential.Remove(fido2Key); 30 | } 31 | ; 32 | 33 | await _applicationDbContext.SaveChangesAsync(); 34 | } 35 | } 36 | 37 | public async Task GetCredentialByIdAsync(byte[] id) 38 | { 39 | var credentialIdString = Base64Url.Encode(id); 40 | //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString); 41 | 42 | var cred = await _applicationDbContext.FidoStoredCredential 43 | .Where(c => c.DescriptorJson != null && c.DescriptorJson.Contains(credentialIdString)) 44 | .FirstOrDefaultAsync(); 45 | 46 | return cred; 47 | } 48 | 49 | public Task> GetCredentialsByUserHandleAsync(byte[] userHandle) 50 | { 51 | return Task.FromResult>( 52 | _applicationDbContext 53 | .FidoStoredCredential.Where(c => c.UserHandle != null && c.UserHandle.SequenceEqual(userHandle)) 54 | .ToList()); 55 | } 56 | 57 | public async Task UpdateCounterAsync(byte[] credentialId, uint counter) 58 | { 59 | var credentialIdString = Base64Url.Encode(credentialId); 60 | //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString); 61 | 62 | var cred = await _applicationDbContext.FidoStoredCredential 63 | .Where(c => c.DescriptorJson != null && c.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync(); 64 | 65 | if (cred != null) 66 | { 67 | cred.SignatureCounter = counter; 68 | await _applicationDbContext.SaveChangesAsync(); 69 | } 70 | } 71 | 72 | public async Task AddCredentialToUserAsync(Fido2User user, FidoStoredCredential credential) 73 | { 74 | credential.UserId = user.Id; 75 | _applicationDbContext.FidoStoredCredential.Add(credential); 76 | await _applicationDbContext.SaveChangesAsync(); 77 | } 78 | 79 | public async Task> GetUsersByCredentialIdAsync(byte[] credentialId) 80 | { 81 | var credentialIdString = Base64Url.Encode(credentialId); 82 | //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString); 83 | 84 | var cred = await _applicationDbContext.FidoStoredCredential 85 | .Where(c => c.DescriptorJson != null && c.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync(); 86 | 87 | if (cred == null || cred.UserId == null) 88 | { 89 | return new List(); 90 | } 91 | 92 | return await _applicationDbContext.Users 93 | .Where(u => Encoding.UTF8.GetBytes(u.UserName) 94 | .SequenceEqual(cred.UserId)) 95 | .Select(u => new Fido2User 96 | { 97 | DisplayName = u.UserName, 98 | Name = u.UserName, 99 | Id = Encoding.UTF8.GetBytes(u.UserName) // byte representation of userID is required 100 | }).ToListAsync(); 101 | } 102 | } 103 | 104 | public static class Fido2Extenstions 105 | { 106 | public static IEnumerable NotNull(this IEnumerable enumerable) where T : class 107 | { 108 | return enumerable.Where(e => e != null).Select(e => e!); 109 | } 110 | } -------------------------------------------------------------------------------- /IdentityProvider/Fido2/Fido2UserTwoFactorTokenProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using OpeniddictServer.Data; 3 | 4 | namespace Fido2Identity; 5 | 6 | public class Fido2UserTwoFactorTokenProvider : IUserTwoFactorTokenProvider 7 | { 8 | public Task CanGenerateTwoFactorTokenAsync(UserManager manager, ApplicationUser user) 9 | { 10 | return Task.FromResult(true); 11 | } 12 | 13 | public Task GenerateAsync(string purpose, UserManager manager, ApplicationUser user) 14 | { 15 | return Task.FromResult("fido2"); 16 | } 17 | 18 | public Task ValidateAsync(string purpose, string token, UserManager manager, ApplicationUser user) 19 | { 20 | return Task.FromResult(true); 21 | } 22 | } -------------------------------------------------------------------------------- /IdentityProvider/Fido2/FidoStoredCredential.cs: -------------------------------------------------------------------------------- 1 | using Fido2NetLib.Objects; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Text.Json; 4 | 5 | namespace Fido2Identity; 6 | 7 | /// 8 | /// Represents a WebAuthn credential. 9 | /// 10 | public class FidoStoredCredential 11 | { 12 | /// 13 | /// Gets or sets the primary key for this user. 14 | /// 15 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 16 | public virtual int Id { get; set; } 17 | 18 | /// 19 | /// Gets or sets the user name for this user. 20 | /// 21 | public virtual string? UserName { get; set; } 22 | 23 | public virtual byte[]? UserId { get; set; } 24 | 25 | /// 26 | /// Gets or sets the public key for this user. 27 | /// 28 | public virtual byte[]? PublicKey { get; set; } 29 | 30 | /// 31 | /// Gets or sets the user handle for this user. 32 | /// 33 | public virtual byte[]? UserHandle { get; set; } 34 | 35 | public virtual uint SignatureCounter { get; set; } 36 | 37 | public virtual string? CredType { get; set; } 38 | 39 | /// 40 | /// Gets or sets the registration date for this user. 41 | /// 42 | public virtual DateTime RegDate { get; set; } 43 | 44 | /// 45 | /// Gets or sets the Authenticator Attestation GUID (AAGUID) for this user. 46 | /// 47 | /// 48 | /// An AAGUID is a 128-bit identifier indicating the type of the authenticator. 49 | /// 50 | public virtual Guid AaGuid { get; set; } 51 | 52 | [NotMapped] 53 | public PublicKeyCredentialDescriptor? Descriptor 54 | { 55 | get { return string.IsNullOrWhiteSpace(DescriptorJson) ? null : JsonSerializer.Deserialize(DescriptorJson); } 56 | set { DescriptorJson = JsonSerializer.Serialize(value); } 57 | } 58 | 59 | public virtual string? DescriptorJson { get; set; } 60 | } 61 | -------------------------------------------------------------------------------- /IdentityProvider/Helpers/AsyncEnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace OpeniddictServer.Helpers; 2 | 3 | public static class AsyncEnumerableExtensions 4 | { 5 | public static Task> ToListAsync(this IAsyncEnumerable source) 6 | { 7 | if (source == null) 8 | { 9 | throw new ArgumentNullException(nameof(source)); 10 | } 11 | 12 | return ExecuteAsync(); 13 | 14 | async Task> ExecuteAsync() 15 | { 16 | var list = new List(); 17 | 18 | await foreach (var element in source) 19 | { 20 | list.Add(element); 21 | } 22 | 23 | return list; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /IdentityProvider/Helpers/FormValueRequiredAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Abstractions; 2 | using Microsoft.AspNetCore.Mvc.ActionConstraints; 3 | 4 | namespace OpeniddictServer.Helpers; 5 | 6 | public sealed class FormValueRequiredAttribute : ActionMethodSelectorAttribute 7 | { 8 | private readonly string _name; 9 | 10 | public FormValueRequiredAttribute(string name) 11 | { 12 | _name = name; 13 | } 14 | 15 | public override bool IsValidForRequest(RouteContext context, ActionDescriptor action) 16 | { 17 | if (string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) || 18 | string.Equals(context.HttpContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) || 19 | string.Equals(context.HttpContext.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase) || 20 | string.Equals(context.HttpContext.Request.Method, "TRACE", StringComparison.OrdinalIgnoreCase)) 21 | { 22 | return false; 23 | } 24 | 25 | if (string.IsNullOrEmpty(context.HttpContext.Request.ContentType)) 26 | { 27 | return false; 28 | } 29 | 30 | if (!context.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) 31 | { 32 | return false; 33 | } 34 | 35 | return !string.IsNullOrEmpty(context.HttpContext.Request.Form[_name]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /IdentityProvider/IdentityProvider.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | false 6 | 728e892b-c8e1-40df-a44c-805a4a138e98 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /IdentityProvider/Migrations/20231222144924_5-0.0.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace IdentityProvider.Migrations 6 | { 7 | /// 8 | public partial class _500 : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.RenameColumn( 14 | name: "Type", 15 | table: "OpenIddictApplications", 16 | newName: "ClientType"); 17 | 18 | migrationBuilder.AddColumn( 19 | name: "ApplicationType", 20 | table: "OpenIddictApplications", 21 | type: "nvarchar(50)", 22 | maxLength: 50, 23 | nullable: true); 24 | 25 | migrationBuilder.AddColumn( 26 | name: "JsonWebKeySet", 27 | table: "OpenIddictApplications", 28 | type: "nvarchar(max)", 29 | nullable: true); 30 | 31 | migrationBuilder.AddColumn( 32 | name: "Settings", 33 | table: "OpenIddictApplications", 34 | type: "nvarchar(max)", 35 | nullable: true); 36 | } 37 | 38 | /// 39 | protected override void Down(MigrationBuilder migrationBuilder) 40 | { 41 | migrationBuilder.DropColumn( 42 | name: "ApplicationType", 43 | table: "OpenIddictApplications"); 44 | 45 | migrationBuilder.DropColumn( 46 | name: "JsonWebKeySet", 47 | table: "OpenIddictApplications"); 48 | 49 | migrationBuilder.DropColumn( 50 | name: "Settings", 51 | table: "OpenIddictApplications"); 52 | 53 | migrationBuilder.RenameColumn( 54 | name: "ClientType", 55 | table: "OpenIddictApplications", 56 | newName: "Type"); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /IdentityProvider/Program.cs: -------------------------------------------------------------------------------- 1 | using OpeniddictServer; 2 | using Serilog; 3 | 4 | Log.Logger = new LoggerConfiguration() 5 | .WriteTo.Console() 6 | .CreateBootstrapLogger(); 7 | 8 | Log.Information("Starting up OpeniddictServer"); 9 | 10 | try 11 | { 12 | var builder = WebApplication.CreateBuilder(args); 13 | 14 | builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration 15 | .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}") 16 | .WriteTo.File("../_logs-IdentityProvider.txt") 17 | .Enrich.FromLogContext() 18 | .ReadFrom.Configuration(context.Configuration)); 19 | 20 | var app = builder 21 | .ConfigureServices() 22 | .ConfigurePipeline(); 23 | 24 | app.Run(); 25 | } 26 | catch (Exception ex) when (ex.GetType().Name is not "StopTheHostException" && ex.GetType().Name is not "HostAbortedException") 27 | { 28 | Log.Fatal(ex, "Unhandled exception"); 29 | } 30 | finally 31 | { 32 | Log.Information("Shut down complete"); 33 | Log.CloseAndFlush(); 34 | } -------------------------------------------------------------------------------- /IdentityProvider/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "OpeniddictServer": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development", 8 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" 9 | }, 10 | "applicationUrl": "https://localhost:44318/" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /IdentityProvider/ViewModels/Authorization/AuthorizeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OpeniddictServer.ViewModels.Authorization; 4 | 5 | public class AuthorizeViewModel 6 | { 7 | [Display(Name = "Application")] 8 | public string ApplicationName { get; set; } 9 | 10 | [Display(Name = "Scope")] 11 | public string Scope { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /IdentityProvider/ViewModels/Shared/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OpeniddictServer.ViewModels.Shared; 4 | 5 | public class ErrorViewModel 6 | { 7 | [Display(Name = "Error")] 8 | public string Error { get; set; } 9 | 10 | [Display(Name = "Description")] 11 | public string ErrorDescription { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /IdentityProvider/Views/Authorization/Authorize.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.Extensions.Primitives 2 | @model AuthorizeViewModel 3 | 4 |
5 |

Authorization

6 | 7 |

Do you want to grant @Model.ApplicationName access to your data? (scopes requested: @Model.Scope)

8 | 9 |
10 | @* Flow the request parameters so they can be received by the Accept/Reject actions: *@ 11 | @foreach (var parameter in Context.Request.HasFormContentType ? 12 | (IEnumerable>) Context.Request.Form : Context.Request.Query) 13 | { 14 | 15 | } 16 | 17 | 18 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /IdentityProvider/Views/Authorization/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.Extensions.Primitives 2 | 3 |
4 |

Log out

5 |

Are you sure you want to sign out?

6 | 7 |
8 | @* Flow the request parameters so they can be received by the LogoutPost action: *@ 9 | @foreach (var parameter in Context.Request.HasFormContentType ? 10 | (IEnumerable>) Context.Request.Form : Context.Request.Query) 11 | { 12 | 13 | } 14 | 15 | 16 | 17 |
-------------------------------------------------------------------------------- /IdentityProvider/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 |
6 |

Welcome

7 |

Learn about building Web apps with ASP.NET Core.

8 |
9 | -------------------------------------------------------------------------------- /IdentityProvider/Views/Home/Privacy.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Privacy Policy"; 3 | } 4 |

@ViewData["Title"]

5 | 6 |

Use this page to detail your site's privacy policy.

7 | -------------------------------------------------------------------------------- /IdentityProvider/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | 3 |
4 |

Ooooops, something went really bad! :(

5 |

6 | @if (!string.IsNullOrEmpty(Model.Error)) { 7 | @Model.Error 8 | } 9 | 10 | @if (!string.IsNullOrEmpty(Model.ErrorDescription)) { 11 | @Model.ErrorDescription 12 | } 13 |

14 |
-------------------------------------------------------------------------------- /IdentityProvider/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - OpenIddict Server 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 35 |
36 |
37 |
38 | @RenderBody() 39 |
40 |
41 | 42 |
43 |
44 | © 2025 - OpenIddict Server - Privacy 45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | @await RenderSectionAsync("Scripts", required: false) 53 | 54 | 55 | -------------------------------------------------------------------------------- /IdentityProvider/Views/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @inject SignInManager SignInManager 3 | @inject UserManager UserManager 4 | 5 | 27 | -------------------------------------------------------------------------------- /IdentityProvider/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using OpeniddictServer 2 | @using OpeniddictServer.Data 3 | @using OpeniddictServer.ViewModels.Authorization 4 | @using OpeniddictServer.ViewModels.Shared 5 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 6 | -------------------------------------------------------------------------------- /IdentityProvider/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /IdentityProvider/Worker.cs: -------------------------------------------------------------------------------- 1 | using OpenIddict.Abstractions; 2 | using OpeniddictServer.Data; 3 | using System.Globalization; 4 | using static OpenIddict.Abstractions.OpenIddictConstants; 5 | 6 | namespace OpeniddictServer; 7 | 8 | public class Worker : IHostedService 9 | { 10 | private readonly IServiceProvider _serviceProvider; 11 | 12 | public Worker(IServiceProvider serviceProvider) 13 | => _serviceProvider = serviceProvider; 14 | 15 | public async Task StartAsync(CancellationToken cancellationToken) 16 | { 17 | using var scope = _serviceProvider.CreateScope(); 18 | 19 | var context = scope.ServiceProvider.GetRequiredService(); 20 | await context.Database.EnsureCreatedAsync(cancellationToken); 21 | 22 | await RegisterApplicationsAsync(scope.ServiceProvider); 23 | await RegisterScopesAsync(scope.ServiceProvider); 24 | 25 | static async Task RegisterApplicationsAsync(IServiceProvider provider) 26 | { 27 | var manager = provider.GetRequiredService(); 28 | 29 | //var dd = await manager.FindByClientIdAsync("oidc-pkce-confidential"); 30 | 31 | //await manager.DeleteAsync(dd); 32 | 33 | // OIDC Code flow confidential client 34 | if (await manager.FindByClientIdAsync("oidc-pkce-confidential") is null) 35 | { 36 | await manager.CreateAsync(new OpenIddictApplicationDescriptor 37 | { 38 | ClientId = "oidc-pkce-confidential", 39 | ConsentType = ConsentTypes.Explicit, 40 | DisplayName = "OIDC confidential Code Flow PKCE", 41 | DisplayNames = 42 | { 43 | [CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente MVC" 44 | }, 45 | PostLogoutRedirectUris = 46 | { 47 | new Uri("https://localhost:5001/signout-callback-oidc"), 48 | new Uri("https://localhost:64265/signout-callback-oidc") 49 | }, 50 | RedirectUris = 51 | { 52 | new Uri("https://localhost:5001/signin-oidc"), 53 | new Uri("https://localhost:64265/signin-oidc"), 54 | }, 55 | ClientSecret = "oidc-pkce-confidential_secret", 56 | Permissions = 57 | { 58 | Permissions.Endpoints.Authorization, 59 | Permissions.Endpoints.EndSession, 60 | Permissions.Endpoints.Token, 61 | Permissions.Endpoints.Revocation, 62 | Permissions.GrantTypes.AuthorizationCode, 63 | Permissions.GrantTypes.RefreshToken, 64 | Permissions.ResponseTypes.Code, 65 | Permissions.Scopes.Email, 66 | Permissions.Scopes.Profile, 67 | Permissions.Scopes.Roles, 68 | Permissions.Prefixes.Scope + "dataEventRecords" 69 | }, 70 | Requirements = 71 | { 72 | Requirements.Features.ProofKeyForCodeExchange 73 | } 74 | }); 75 | } 76 | } 77 | 78 | static async Task RegisterScopesAsync(IServiceProvider provider) 79 | { 80 | var manager = provider.GetRequiredService(); 81 | 82 | if (await manager.FindByNameAsync("dataEventRecords") is null) 83 | { 84 | await manager.CreateAsync(new OpenIddictScopeDescriptor 85 | { 86 | DisplayName = "dataEventRecords API access", 87 | DisplayNames = 88 | { 89 | [CultureInfo.GetCultureInfo("fr-FR")] = "Accès à l'API de démo" 90 | }, 91 | Name = "dataEventRecords", 92 | Resources = 93 | { 94 | "rs_dataEventRecordsApi" 95 | } 96 | }); 97 | } 98 | } 99 | } 100 | 101 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 102 | } 103 | -------------------------------------------------------------------------------- /IdentityProvider/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=openiddict-BlazorServer-Oidc;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Fido2": { 6 | "ServerDomain": "localhost", 7 | "ServerName": "Fido2OpenIddict", 8 | "Origins": [ "https://localhost:44318" ], 9 | "TimestampDriftTolerance": 300000, 10 | "MDSAccessKey": null 11 | }, 12 | "Serilog": { 13 | "MinimumLevel": { 14 | "Default": "Debug", 15 | "Override": { 16 | "Microsoft": "Warning", 17 | "Microsoft.Hosting.Lifetime": "Information", 18 | "Microsoft.AspNetCore.Authentication": "Debug", 19 | "System": "Warning" 20 | } 21 | } 22 | }, 23 | "AllowedHosts": "*" 24 | } -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | 4 | a.navbar-brand { 5 | white-space: normal; 6 | text-align: center; 7 | word-break: break-all; 8 | } 9 | 10 | /* Provide sufficient contrast against white background */ 11 | a { 12 | color: #0366d6; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .nav-pills .nav-link.active, .nav-pills .show > .nav-link { 22 | color: #fff; 23 | background-color: #1b6ec2; 24 | border-color: #1861ac; 25 | } 26 | 27 | /* Sticky footer styles 28 | -------------------------------------------------- */ 29 | html { 30 | font-size: 14px; 31 | } 32 | @media (min-width: 768px) { 33 | html { 34 | font-size: 16px; 35 | } 36 | } 37 | 38 | .border-top { 39 | border-top: 1px solid #e5e5e5; 40 | } 41 | .border-bottom { 42 | border-bottom: 1px solid #e5e5e5; 43 | } 44 | 45 | .box-shadow { 46 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 47 | } 48 | 49 | button.accept-policy { 50 | font-size: 1rem; 51 | line-height: inherit; 52 | } 53 | 54 | /* Sticky footer styles 55 | -------------------------------------------------- */ 56 | html { 57 | position: relative; 58 | min-height: 100%; 59 | } 60 | 61 | body { 62 | /* Margin bottom by footer height */ 63 | margin-bottom: 60px; 64 | } 65 | .footer { 66 | position: absolute; 67 | bottom: 0; 68 | width: 100%; 69 | white-space: nowrap; 70 | line-height: 60px; /* Vertically center the text there */ 71 | } 72 | -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/IdentityProvider/wwwroot/favicon.ico -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/images/securitykey.min.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/js/helpers.js: -------------------------------------------------------------------------------- 1 | coerceToArrayBuffer = function (thing, name) { 2 | if (typeof thing === "string") { 3 | // base64url to base64 4 | thing = thing.replace(/-/g, "+").replace(/_/g, "/"); 5 | 6 | // base64 to Uint8Array 7 | var str = window.atob(thing); 8 | var bytes = new Uint8Array(str.length); 9 | for (var i = 0; i < str.length; i++) { 10 | bytes[i] = str.charCodeAt(i); 11 | } 12 | thing = bytes; 13 | } 14 | 15 | // Array to Uint8Array 16 | if (Array.isArray(thing)) { 17 | thing = new Uint8Array(thing); 18 | } 19 | 20 | // Uint8Array to ArrayBuffer 21 | if (thing instanceof Uint8Array) { 22 | thing = thing.buffer; 23 | } 24 | 25 | // error if none of the above worked 26 | if (!(thing instanceof ArrayBuffer)) { 27 | throw new TypeError("could not coerce '" + name + "' to ArrayBuffer"); 28 | } 29 | 30 | return thing; 31 | }; 32 | 33 | 34 | coerceToBase64Url = function (thing) { 35 | // Array or ArrayBuffer to Uint8Array 36 | if (Array.isArray(thing)) { 37 | thing = Uint8Array.from(thing); 38 | } 39 | 40 | if (thing instanceof ArrayBuffer) { 41 | thing = new Uint8Array(thing); 42 | } 43 | 44 | // Uint8Array to base64 45 | if (thing instanceof Uint8Array) { 46 | var str = ""; 47 | var len = thing.byteLength; 48 | 49 | for (var i = 0; i < len; i++) { 50 | str += String.fromCharCode(thing[i]); 51 | } 52 | thing = window.btoa(str); 53 | } 54 | 55 | if (typeof thing !== "string") { 56 | throw new Error("could not coerce to string"); 57 | } 58 | 59 | // base64 to base64url 60 | // NOTE: "=" at the end of challenge is optional, strip it off here 61 | thing = thing.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, ""); 62 | 63 | return thing; 64 | }; 65 | 66 | 67 | 68 | // HELPERS 69 | 70 | function showErrorAlert(message, error) { 71 | let footermsg = ''; 72 | if (error) { 73 | footermsg = 'exception:' + error.toString(); 74 | } 75 | Swal.fire({ 76 | //type: 'error', 77 | title: 'Error', 78 | text: message, 79 | footer: footermsg 80 | //footer: 'Why do I have this issue?' 81 | }) 82 | } 83 | 84 | function detectFIDOSupport() { 85 | if (window.PublicKeyCredential === undefined || 86 | typeof window.PublicKeyCredential !== "function") { 87 | //$('#register-button').attr("disabled", true); 88 | //$('#login-button').attr("disabled", true); 89 | var el = document.getElementById("notSupportedWarning"); 90 | if (el) { 91 | el.style.display = 'block'; 92 | } 93 | return; 94 | } 95 | } 96 | 97 | /** 98 | * 99 | * Get a form value 100 | * @param {any} selector 101 | */ 102 | function value(selector) { 103 | var el = document.querySelector(selector); 104 | if (el.type === "checkbox") { 105 | return el.checked; 106 | } 107 | return el.value; 108 | } -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/js/instant.js: -------------------------------------------------------------------------------- 1 | /*! instant.page v1.1.0 - (C) 2019 Alexandre Dieulot - https://instant.page/license */ 2 | 3 | let urlToPreload 4 | let mouseoverTimer 5 | let lastTouchTimestamp 6 | 7 | const prefetcher = document.createElement('link') 8 | const isSupported = prefetcher.relList && prefetcher.relList.supports && prefetcher.relList.supports('prefetch') 9 | const allowQueryString = 'instantAllowQueryString' in document.body.dataset 10 | 11 | if (isSupported) { 12 | prefetcher.rel = 'prefetch' 13 | document.head.appendChild(prefetcher) 14 | 15 | const eventListenersOptions = { 16 | capture: true, 17 | passive: true, 18 | } 19 | document.addEventListener('touchstart', touchstartListener, eventListenersOptions) 20 | document.addEventListener('mouseover', mouseoverListener, eventListenersOptions) 21 | } 22 | 23 | function touchstartListener(event) { 24 | /* Chrome on Android calls mouseover before touchcancel so `lastTouchTimestamp` 25 | * must be assigned on touchstart to be measured on mouseover. */ 26 | lastTouchTimestamp = performance.now() 27 | 28 | const linkElement = event.target.closest('a') 29 | 30 | if (!linkElement) { 31 | return 32 | } 33 | 34 | if (!isPreloadable(linkElement)) { 35 | return 36 | } 37 | 38 | linkElement.addEventListener('touchcancel', touchendAndTouchcancelListener, { passive: true }) 39 | linkElement.addEventListener('touchend', touchendAndTouchcancelListener, { passive: true }) 40 | 41 | urlToPreload = linkElement.href 42 | preload(linkElement.href) 43 | } 44 | 45 | function touchendAndTouchcancelListener() { 46 | urlToPreload = undefined 47 | stopPreloading() 48 | } 49 | 50 | function mouseoverListener(event) { 51 | if (performance.now() - lastTouchTimestamp < 1100) { 52 | return 53 | } 54 | 55 | const linkElement = event.target.closest('a') 56 | 57 | if (!linkElement) { 58 | return 59 | } 60 | 61 | if (!isPreloadable(linkElement)) { 62 | return 63 | } 64 | 65 | linkElement.addEventListener('mouseout', mouseoutListener, { passive: true }) 66 | 67 | urlToPreload = linkElement.href 68 | 69 | mouseoverTimer = setTimeout(() => { 70 | preload(linkElement.href) 71 | mouseoverTimer = undefined 72 | }, 65) 73 | } 74 | 75 | function mouseoutListener(event) { 76 | if (event.relatedTarget && event.target.closest('a') == event.relatedTarget.closest('a')) { 77 | return 78 | } 79 | 80 | if (mouseoverTimer) { 81 | clearTimeout(mouseoverTimer) 82 | mouseoverTimer = undefined 83 | } 84 | else { 85 | urlToPreload = undefined 86 | stopPreloading() 87 | } 88 | } 89 | 90 | function isPreloadable(linkElement) { 91 | if (urlToPreload == linkElement.href) { 92 | return 93 | } 94 | 95 | const urlObject = new URL(linkElement.href) 96 | 97 | if (urlObject.origin != location.origin) { 98 | return 99 | } 100 | 101 | if (!allowQueryString && urlObject.search && !('instant' in linkElement.dataset)) { 102 | return 103 | } 104 | 105 | if (urlObject.hash && urlObject.pathname + urlObject.search == location.pathname + location.search) { 106 | return 107 | } 108 | 109 | if ('noInstant' in linkElement.dataset) { 110 | return 111 | } 112 | 113 | return true 114 | } 115 | 116 | function preload(url) { 117 | prefetcher.href = url 118 | } 119 | 120 | function stopPreloading() { 121 | /* The spec says an empty string should abort the prefetching 122 | * but Firefox 64 interprets it as a relative URL to prefetch. */ 123 | prefetcher.removeAttribute('href') 124 | } 125 | -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your JavaScript code. 5 | -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2018 Twitter, Inc. 4 | Copyright (c) 2011-2018 The Bootstrap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | // Unobtrusive validation support library for jQuery and jQuery Validate 2 | // Copyright (c) .NET Foundation. All rights reserved. 3 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 4 | // @version v3.2.11 5 | !function(a){"function"==typeof define&&define.amd?define("jquery.validate.unobtrusive",["jquery-validation"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery-validation")):jQuery.validator.unobtrusive=a(jQuery)}(function(a){function e(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function n(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function t(a){return a.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function r(a){return a.substr(0,a.lastIndexOf(".")+1)}function i(a,e){return 0===a.indexOf("*.")&&(a=a.replace("*.",e)),a}function o(e,n){var r=a(this).find("[data-valmsg-for='"+t(n[0].name)+"']"),i=r.attr("data-valmsg-replace"),o=i?a.parseJSON(i)!==!1:null;r.removeClass("field-validation-valid").addClass("field-validation-error"),e.data("unobtrusiveContainer",r),o?(r.empty(),e.removeClass("input-validation-error").appendTo(r)):e.hide()}function d(e,n){var t=a(this).find("[data-valmsg-summary=true]"),r=t.find("ul");r&&r.length&&n.errorList.length&&(r.empty(),t.addClass("validation-summary-errors").removeClass("validation-summary-valid"),a.each(n.errorList,function(){a("
  • ").html(this.message).appendTo(r)}))}function s(e){var n=e.data("unobtrusiveContainer");if(n){var t=n.attr("data-valmsg-replace"),r=t?a.parseJSON(t):null;n.addClass("field-validation-valid").removeClass("field-validation-error"),e.removeData("unobtrusiveContainer"),r&&n.empty()}}function l(e){var n=a(this),t="__jquery_unobtrusive_validation_form_reset";if(!n.data(t)){n.data(t,!0);try{n.data("validator").resetForm()}finally{n.removeData(t)}n.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),n.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function u(e){var n=a(e),t=n.data(v),r=a.proxy(l,e),i=f.unobtrusive.options||{},u=function(n,t){var r=i[n];r&&a.isFunction(r)&&r.apply(e,t)};return t||(t={options:{errorClass:i.errorClass||"input-validation-error",errorElement:i.errorElement||"span",errorPlacement:function(){o.apply(e,arguments),u("errorPlacement",arguments)},invalidHandler:function(){d.apply(e,arguments),u("invalidHandler",arguments)},messages:{},rules:{},success:function(){s.apply(e,arguments),u("success",arguments)}},attachValidation:function(){n.off("reset."+v,r).on("reset."+v,r).validate(this.options)},validate:function(){return n.validate(),n.valid()}},n.data(v,t)),t}var m,f=a.validator,v="unobtrusiveValidation";return f.unobtrusive={adapters:[],parseElement:function(e,n){var t,r,i,o=a(e),d=o.parents("form")[0];d&&(t=u(d),t.options.rules[e.name]=r={},t.options.messages[e.name]=i={},a.each(this.adapters,function(){var n="data-val-"+this.name,t=o.attr(n),s={};void 0!==t&&(n+="-",a.each(this.params,function(){s[this]=o.attr(n+this)}),this.adapt({element:e,form:d,message:t,params:s,rules:r,messages:i}))}),a.extend(r,{__dummy__:!0}),n||t.attachValidation())},parse:function(e){var n=a(e),t=n.parents().addBack().filter("form").add(n.find("form")).has("[data-val=true]");n.find("[data-val=true]").each(function(){f.unobtrusive.parseElement(this,!0)}),t.each(function(){var a=u(this);a&&a.attachValidation()})}},m=f.unobtrusive.adapters,m.add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},m.addBool=function(a,n){return this.add(a,function(t){e(t,n||a,!0)})},m.addMinMax=function(a,n,t,r,i,o){return this.add(a,[i||"min",o||"max"],function(a){var i=a.params.min,o=a.params.max;i&&o?e(a,r,[i,o]):i?e(a,n,i):o&&e(a,t,o)})},m.addSingleVal=function(a,n,t){return this.add(a,[n||"val"],function(r){e(r,t||a,r.params[n])})},f.addMethod("__dummy__",function(a,e,n){return!0}),f.addMethod("regex",function(a,e,n){var t;return!!this.optional(e)||(t=new RegExp(n).exec(a),t&&0===t.index&&t[0].length===a.length)}),f.addMethod("nonalphamin",function(a,e,n){var t;return n&&(t=a.match(/\W/g),t=t&&t.length>=n),t}),f.methods.extension?(m.addSingleVal("accept","mimtype"),m.addSingleVal("extension","extension")):m.addSingleVal("extension","extension","accept"),m.addSingleVal("regex","pattern"),m.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),m.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),m.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),m.add("equalto",["other"],function(n){var o=r(n.element.name),d=n.params.other,s=i(d,o),l=a(n.form).find(":input").filter("[name='"+t(s)+"']")[0];e(n,"equalTo",l)}),m.add("required",function(a){"INPUT"===a.element.tagName.toUpperCase()&&"CHECKBOX"===a.element.type.toUpperCase()||e(a,"required",!0)}),m.add("remote",["url","type","additionalfields"],function(o){var d={url:o.params.url,type:o.params.type||"GET",data:{}},s=r(o.element.name);a.each(n(o.params.additionalfields||o.element.name),function(e,n){var r=i(n,s);d.data[r]=function(){var e=a(o.form).find(":input").filter("[name='"+t(r)+"']");return e.is(":checkbox")?e.filter(":checked").val()||e.filter(":hidden").val()||"":e.is(":radio")?e.filter(":checked").val()||"":e.val()}}),e(o,"remote",d)}),m.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&e(a,"minlength",a.params.min),a.params.nonalphamin&&e(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&e(a,"regex",a.params.regex)}),m.add("fileextensions",["extensions"],function(a){e(a,"extension",a.params.extensions)}),a(function(){f.unobtrusive.parse(document)}),f.unobtrusive}); -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 damienbod 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 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/App.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | 4 | 5 | 6 | 7 | 8 | @{ 9 | var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); 10 | NavigationManager.NavigateTo($"api/account/login?redirectUri={returnUrl}", forceLoad: true); 11 | } 12 | 13 | 14 | Wait... 15 | 16 | 17 | 18 | 19 | 20 |

    Sorry, there's nothing at this address.

    21 |
    22 |
    23 |
    24 |
    -------------------------------------------------------------------------------- /Old/BlazorServerOidc/BlazorServerOidc.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace BlazorServerOidc.Controllers; 8 | 9 | [Route("api/[controller]")] 10 | public class AccountController : ControllerBase 11 | { 12 | [HttpGet("Login")] 13 | public ActionResult Login(string? returnUrl, string? claimsChallenge) 14 | { 15 | var properties = GetAuthProperties(returnUrl); 16 | 17 | if (claimsChallenge != null) 18 | { 19 | string jsonString = claimsChallenge.Replace("\\", "") 20 | .Trim(new char[1] { '"' }); 21 | 22 | properties.Items["claims"] = jsonString; 23 | } 24 | 25 | return Challenge(properties); 26 | } 27 | 28 | // [ValidateAntiForgeryToken] // not needed explicitly due the the Auto global definition. 29 | [IgnoreAntiforgeryToken] // need to apply this to the form post request 30 | [Authorize] 31 | [HttpPost("Logout")] 32 | public IActionResult Logout() 33 | { 34 | return SignOut( 35 | new AuthenticationProperties { RedirectUri = "/SignedOut" }, 36 | CookieAuthenticationDefaults.AuthenticationScheme, 37 | OpenIdConnectDefaults.AuthenticationScheme); 38 | } 39 | 40 | /// 41 | /// Original src: 42 | /// https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebOidc/BlazorWebOidc/LoginLogoutEndpointRouteBuilderExtensions.cs 43 | /// 44 | private static AuthenticationProperties GetAuthProperties(string? returnUrl) 45 | { 46 | const string pathBase = "/"; 47 | 48 | // Prevent open redirects. 49 | if (string.IsNullOrEmpty(returnUrl)) 50 | { 51 | returnUrl = pathBase; 52 | } 53 | else if (!Uri.IsWellFormedUriString(returnUrl, UriKind.Relative)) 54 | { 55 | returnUrl = new Uri(returnUrl, UriKind.Absolute).PathAndQuery; 56 | } 57 | else if (returnUrl[0] != '/') 58 | { 59 | returnUrl = $"{pathBase}{returnUrl}"; 60 | } 61 | 62 | return new AuthenticationProperties { RedirectUri = returnUrl }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Data/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorServerOidc.Data 2 | { 3 | public class WeatherForecast 4 | { 5 | public DateTime Date { get; set; } 6 | 7 | public int TemperatureC { get; set; } 8 | 9 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 10 | 11 | public string? Summary { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Data/WeatherForecastService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorServerOidc.Data 2 | { 3 | public class WeatherForecastService 4 | { 5 | private static readonly string[] Summaries = new[] 6 | { 7 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 8 | }; 9 | 10 | public Task GetForecastAsync(DateTime startDate) 11 | { 12 | return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast 13 | { 14 | Date = startDate.AddDays(index), 15 | TemperatureC = Random.Shared.Next(-20, 55), 16 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 17 | }).ToArray()); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | 3 | Counter 4 | 5 |

    Counter

    6 | 7 |

    Current count: @currentCount

    8 | 9 | 10 | 11 | @code { 12 | private int currentCount = 0; 13 | 14 | private void IncrementCount() 15 | { 16 | currentCount++; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model BlazorServerOidc.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
    17 |
    18 |

    Error.

    19 |

    An error occurred while processing your request.

    20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

    24 | Request ID: @Model.RequestId 25 |

    26 | } 27 | 28 |

    Development Mode

    29 |

    30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

    32 |

    33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

    38 |
    39 |
    40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.Diagnostics; 4 | 5 | namespace BlazorServerOidc.Pages 6 | { 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | public class ErrorModel : PageModel 10 | { 11 | public string? RequestId { get; set; } 12 | 13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 14 | 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public void OnGet() 23 | { 24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Pages/FetchData.razor: -------------------------------------------------------------------------------- 1 | @page "/fetchdata" 2 | 3 | Weather forecast 4 | 5 | @using BlazorServerOidc.Data 6 | @inject WeatherForecastService ForecastService 7 | 8 |

    Weather forecast

    9 | 10 |

    This component demonstrates fetching data from a service.

    11 | 12 | @if (forecasts == null) 13 | { 14 |

    Loading...

    15 | } 16 | else 17 | { 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | @foreach (var forecast in forecasts) 29 | { 30 | 31 | 32 | 33 | 34 | 35 | 36 | } 37 | 38 |
    DateTemp. (C)Temp. (F)Summary
    @forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
    39 | } 40 | 41 | @code { 42 | private WeatherForecast[]? forecasts; 43 | 44 | protected override async Task OnInitializedAsync() 45 | { 46 | forecasts = await ForecastService.GetForecastAsync(DateTime.Now); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Index 4 | 5 |

    Hello, world!

    6 | 7 | Welcome to your new app. 8 | 9 | 10 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Pages/SignedOut.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @addTagHelper *, NetEscapades.AspNetCore.SecurityHeaders.TagHelpers 3 | @model BlazorServerOidc.Pages.SignedOutModel 4 | @{ 5 | ViewData["Title"] = "SignedOut"; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 35 |
    36 |
    37 |
    38 |

    You have successfully signed out

    39 |
    40 |
    41 | 42 |
    43 |
    44 | Signed out 45 |
    46 |
    47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Pages/SignedOut.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace BlazorServerOidc.Pages; 5 | 6 | [AllowAnonymous] 7 | public class SignedOutModel : PageModel 8 | { 9 | public void OnGet() 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace BlazorServerOidc.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = "_Layout"; 6 | } 7 | 8 | 9 | 10 | @Html.AntiForgeryToken() 11 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Pages/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | @namespace BlazorServerOidc.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @addTagHelper *, NetEscapades.AspNetCore.SecurityHeaders.TagHelpers 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @RenderBody() 19 | 20 |
    21 | 22 | An error has occurred. This application may no longer respond until reloaded. 23 | 24 | 25 | An unhandled exception has occurred. See browser dev tools for details. 26 | 27 | Reload 28 | 🗙 29 |
    30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Program.cs: -------------------------------------------------------------------------------- 1 | using BlazorServerOidc.Data; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Authorization; 7 | using Microsoft.IdentityModel.JsonWebTokens; 8 | using Microsoft.IdentityModel.Protocols.OpenIdConnect; 9 | using Microsoft.IdentityModel.Tokens; 10 | using NetEscapades.AspNetCore.SecurityHeaders.Infrastructure; 11 | 12 | namespace BlazorServerOidc; 13 | 14 | public class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | var builder = WebApplication.CreateBuilder(args); 19 | 20 | builder.Services.AddSecurityHeaderPolicies() 21 | .SetPolicySelector((PolicySelectorContext ctx) => 22 | { 23 | return SecurityHeadersDefinitions.GetHeaderPolicyCollection(builder.Environment.IsDevelopment(), 24 | builder.Configuration["OpenIDConnectSettings:Authority"]); 25 | }); 26 | 27 | builder.Services.AddAuthentication(options => 28 | { 29 | options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 30 | options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; 31 | }) 32 | .AddCookie() 33 | .AddOpenIdConnect(options => 34 | { 35 | builder.Configuration.GetSection("OpenIDConnectSettings").Bind(options); 36 | 37 | options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; 38 | options.ResponseType = OpenIdConnectResponseType.Code; 39 | 40 | options.SaveTokens = true; 41 | options.GetClaimsFromUserInfoEndpoint = true; 42 | options.TokenValidationParameters = new TokenValidationParameters 43 | { 44 | NameClaimType = "name" 45 | }; 46 | }); 47 | 48 | builder.Services.AddRazorPages().AddMvcOptions(options => 49 | { 50 | var policy = new AuthorizationPolicyBuilder() 51 | .RequireAuthenticatedUser() 52 | .Build(); 53 | options.Filters.Add(new AuthorizeFilter(policy)); 54 | }); 55 | 56 | builder.Services.AddServerSideBlazor(); 57 | builder.Services.AddSingleton(); 58 | 59 | builder.Services.AddControllersWithViews(options => 60 | options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())); 61 | 62 | var app = builder.Build(); 63 | 64 | JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear(); 65 | 66 | // < .NET 8 67 | // JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 68 | 69 | if (!app.Environment.IsDevelopment()) 70 | { 71 | app.UseExceptionHandler("/Error"); 72 | app.UseHsts(); 73 | } 74 | 75 | app.UseSecurityHeaders(); 76 | 77 | app.UseHttpsRedirection(); 78 | 79 | app.UseStaticFiles(); 80 | 81 | app.UseRouting(); 82 | 83 | app.UseAuthentication(); 84 | app.UseAuthorization(); 85 | 86 | app.MapRazorPages(); 87 | app.MapControllers(); 88 | 89 | app.MapBlazorHub().RequireAuthorization(); 90 | app.MapFallbackToPage("/_Host"); 91 | 92 | app.Run(); 93 | } 94 | } -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "https://localhost:5001", 7 | "sslPort": 5001 8 | } 9 | }, 10 | "profiles": { 11 | "BlazorServerOidc": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "https://localhost:5001", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/SecurityHeadersDefinitions.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorServerOidc; 2 | 3 | public static class SecurityHeadersDefinitions 4 | { 5 | private static HeaderPolicyCollection? policy; 6 | 7 | public static HeaderPolicyCollection GetHeaderPolicyCollection(bool isDev, string? idpHost) 8 | { 9 | ArgumentNullException.ThrowIfNull(idpHost); 10 | 11 | // Avoid building a new HeaderPolicyCollection on every request for performance reasons. 12 | // Where possible, cache and reuse HeaderPolicyCollection instances. 13 | if (policy != null) return policy; 14 | 15 | policy = new HeaderPolicyCollection() 16 | .AddFrameOptionsDeny() 17 | .AddContentTypeOptionsNoSniff() 18 | .AddReferrerPolicyStrictOriginWhenCrossOrigin() 19 | .AddCrossOriginOpenerPolicy(builder => builder.SameOrigin()) 20 | .AddCrossOriginResourcePolicy(builder => builder.SameOrigin()) 21 | .AddCrossOriginEmbedderPolicy(builder => builder.RequireCorp()) // remove for dev if using hot reload 22 | .AddContentSecurityPolicy(builder => 23 | { 24 | builder.AddObjectSrc().None(); 25 | builder.AddBlockAllMixedContent(); 26 | builder.AddImgSrc().Self().From("data:"); 27 | builder.AddFormAction().Self().From(idpHost); 28 | builder.AddFontSrc().Self(); 29 | builder.AddBaseUri().Self(); 30 | builder.AddFrameAncestors().None(); 31 | 32 | builder.AddStyleSrc() 33 | .UnsafeInline() 34 | .Self(); 35 | 36 | builder.AddScriptSrc() 37 | .WithNonce() 38 | .UnsafeInline(); // only a fallback for older browsers when the nonce is used 39 | 40 | // disable script and style CSP protection if using Blazor hot reload 41 | // if using hot reload, DO NOT deploy with an insecure CSP 42 | }) 43 | .RemoveServerHeader() 44 | .AddPermissionsPolicyWithDefaultSecureDirectives(); 45 | 46 | if (!isDev) 47 | { 48 | // maxage = one year in seconds 49 | policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(); 50 | } 51 | 52 | return policy; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Shared/LoginLogoutMenu.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | @context.User.Identity?.Name 4 |
    5 | 8 |
    9 |
    10 | 11 | Log in 12 | 13 |
    14 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | BlazorServerOidc 4 | 5 |
    6 | 9 | 10 |
    11 |
    12 | 13 |
    14 | 15 |
    16 | @Body 17 |
    18 |
    19 |
    20 | 21 | 22 |
    23 | An unhandled error has occurred. 24 | Reload 25 | 🗙 26 |
    27 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/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 .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .top-row, article { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  9 | 10 |
    11 | 28 |
    29 | 30 | @code { 31 | private bool collapseNavMenu = true; 32 | 33 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 34 | 35 | private void ToggleNavMenu() 36 | { 37 | collapseNavMenu = !collapseNavMenu; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/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 | .oi { 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 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/Shared/SurveyPrompt.razor: -------------------------------------------------------------------------------- 1 | 
    2 | 3 | @Title 4 | 5 | 6 | Please take our 7 | brief survey 8 | 9 | and tell us what you think. 10 |
    11 | 12 | @code { 13 | // Demonstrates how a parent component can supply parameters 14 | [Parameter] 15 | public string? Title { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using BlazorServerOidc 10 | @using BlazorServerOidc.Shared 11 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "OpenIDConnectSettings": { 3 | "Authority": "https://localhost:44318", 4 | "ClientId": "oidc-pkce-confidential", 5 | "ClientSecret": "oidc-pkce-confidential_secret" 6 | }, 7 | "Logging": { 8 | "LogLevel": { 9 | "Default": "Information", 10 | "Microsoft.AspNetCore": "Warning" 11 | } 12 | }, 13 | "AllowedHosts": "*" 14 | } 15 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/wwwroot/.well-known/security.txt: -------------------------------------------------------------------------------- 1 | Security email 2 | 3 | contact: mailto: security-dev-officer@secure.ch 4 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Old/BlazorServerOidc/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](https://github.com/iconic/open-iconic) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](https://github.com/iconic/open-iconic). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](https://github.com/iconic/open-iconic) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](https://github.com/iconic/open-iconic) and [Reference](https://github.com/iconic/open-iconic) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/Old/BlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /Old/BlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/Old/BlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /Old/BlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/Old/BlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /Old/BlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/Old/BlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /Old/BlazorServerOidc/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | h1:focus { 8 | outline: none; 9 | } 10 | 11 | a, .btn-link { 12 | color: #0071c1; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .content { 22 | padding-top: 1.1rem; 23 | } 24 | 25 | .valid.modified:not([type=checkbox]) { 26 | outline: 1px solid #26b050; 27 | } 28 | 29 | .invalid { 30 | outline: 1px solid red; 31 | } 32 | 33 | .validation-message { 34 | color: red; 35 | } 36 | 37 | #blazor-error-ui { 38 | background: lightyellow; 39 | bottom: 0; 40 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 41 | display: none; 42 | left: 0; 43 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 44 | position: fixed; 45 | width: 100%; 46 | z-index: 1000; 47 | } 48 | 49 | #blazor-error-ui .dismiss { 50 | cursor: pointer; 51 | position: absolute; 52 | right: 0.75rem; 53 | top: 0.5rem; 54 | } 55 | 56 | .blazor-error-boundary { 57 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 58 | padding: 1rem 1rem 1rem 3.7rem; 59 | color: white; 60 | } 61 | 62 | .blazor-error-boundary::after { 63 | content: "An error has occurred." 64 | } 65 | -------------------------------------------------------------------------------- /Old/BlazorServerOidc/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/Old/BlazorServerOidc/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/App.razor: -------------------------------------------------------------------------------- 1 | @inject IHostEnvironment Env 2 | @inject BlazorNonceService BlazorNonceService 3 | @using System.Security.Cryptography; 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | @code 24 | { 25 | /// 26 | /// Original src: https://github.com/javiercn/BlazorWebNonceService 27 | /// 28 | [CascadingParameter] HttpContext Context { get; set; } = default!; 29 | 30 | protected override void OnInitialized() 31 | { 32 | var nonce = GetNonce(); 33 | if (nonce != null) 34 | { 35 | BlazorNonceService.SetNonce(nonce); 36 | } 37 | } 38 | 39 | public string? GetNonce() 40 | { 41 | if (Context.Items.TryGetValue("nonce", out var item) && item is string nonce and not null) 42 | { 43 | return nonce; 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/BlazorNonceService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.Server.Circuits; 3 | 4 | namespace BlazorWebFromBlazorServerOidc; 5 | 6 | /// 7 | /// Original src: https://github.com/javiercn/BlazorWebNonceService 8 | /// 9 | public class BlazorNonceService : CircuitHandler, IDisposable 10 | { 11 | private readonly PersistentComponentState _state; 12 | private readonly PersistingComponentStateSubscription _subscription; 13 | 14 | public BlazorNonceService(PersistentComponentState state) 15 | { 16 | if (state.TryTakeFromJson("nonce", out string? nonce)) 17 | { 18 | if (nonce is not null) 19 | { 20 | Nonce = nonce; 21 | } 22 | else 23 | { 24 | throw new InvalidOperationException("Nonce can't be null when provided"); 25 | } 26 | } 27 | else 28 | { 29 | _subscription = state.RegisterOnPersisting(PersistNonce); 30 | } 31 | 32 | _state = state; 33 | } 34 | 35 | public string? Nonce { get; set; } 36 | 37 | private Task PersistNonce() 38 | { 39 | _state.PersistAsJson("nonce", Nonce); 40 | return Task.CompletedTask; 41 | } 42 | 43 | public void SetNonce(string nonce) 44 | { 45 | ArgumentException.ThrowIfNullOrWhiteSpace(nonce); 46 | 47 | if (Nonce != null) 48 | { 49 | throw new InvalidOperationException("Nonce already defined"); 50 | } 51 | 52 | Nonce = nonce; 53 | } 54 | 55 | public void Dispose() => ((IDisposable)_subscription)?.Dispose(); 56 | } 57 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/BlazorWebFromBlazorServerOidc.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace BlazorWebFromBlazorServerOidc.Controllers; 8 | 9 | [Route("api/[controller]")] 10 | public class AccountController : ControllerBase 11 | { 12 | [HttpGet("Login")] 13 | public ActionResult Login(string? returnUrl, string? claimsChallenge) 14 | { 15 | var properties = GetAuthProperties(returnUrl); 16 | 17 | if (claimsChallenge != null) 18 | { 19 | string jsonString = claimsChallenge.Replace("\\", string.Empty) 20 | .Trim(['"']); 21 | 22 | properties.Items["claims"] = jsonString; 23 | } 24 | 25 | return Challenge(properties); 26 | } 27 | 28 | [ValidateAntiForgeryToken] // not needed explicitly due the the Auto global definition. 29 | [Authorize] 30 | [HttpPost("Logout")] 31 | public IActionResult Logout() 32 | { 33 | return SignOut( 34 | new AuthenticationProperties { RedirectUri = "/SignedOut" }, 35 | CookieAuthenticationDefaults.AuthenticationScheme, 36 | OpenIdConnectDefaults.AuthenticationScheme); 37 | } 38 | 39 | /// 40 | /// Original src: 41 | /// https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebOidc/BlazorWebOidc/LoginLogoutEndpointRouteBuilderExtensions.cs 42 | /// 43 | private static AuthenticationProperties GetAuthProperties(string? returnUrl) 44 | { 45 | const string pathBase = "/"; 46 | 47 | // Prevent open redirects. 48 | if (string.IsNullOrEmpty(returnUrl)) 49 | { 50 | returnUrl = pathBase; 51 | } 52 | else if (!Uri.IsWellFormedUriString(returnUrl, UriKind.Relative)) 53 | { 54 | returnUrl = new Uri(returnUrl, UriKind.Absolute).PathAndQuery; 55 | } 56 | else if (returnUrl[0] != '/') 57 | { 58 | returnUrl = $"{pathBase}{returnUrl}"; 59 | } 60 | 61 | return new AuthenticationProperties { RedirectUri = returnUrl }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Data/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorWebFromBlazorServerOidc.Data 2 | { 3 | public class WeatherForecast 4 | { 5 | public DateTime Date { get; set; } 6 | 7 | public int TemperatureC { get; set; } 8 | 9 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 10 | 11 | public string? Summary { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Data/WeatherForecastService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorWebFromBlazorServerOidc.Data 2 | { 3 | public class WeatherForecastService 4 | { 5 | private static readonly string[] Summaries = new[] 6 | { 7 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 8 | }; 9 | 10 | public Task GetForecastAsync(DateTime startDate) 11 | { 12 | return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast 13 | { 14 | Date = startDate.AddDays(index), 15 | TemperatureC = Random.Shared.Next(-20, 55), 16 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 17 | }).ToArray()); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Layout/LogInOrOut.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | 4 | 5 | 8 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
    4 | 7 | 8 |
    9 |
    10 | 11 |
    12 | 13 |
    14 | @Body 15 |
    16 |
    17 |
    18 | 19 |
    20 | An unhandled error has occurred. 21 | Reload 22 | 🗙 23 |
    24 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Layout/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 .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .top-row, article { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 | 9 | 10 |
    11 | 28 |
    29 | 30 | @code { 31 | private bool collapseNavMenu = true; 32 | 33 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 34 | 35 | private void ToggleNavMenu() 36 | { 37 | collapseNavMenu = !collapseNavMenu; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Layout/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 | .oi { 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 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/NonceMiddleware.cs: -------------------------------------------------------------------------------- 1 | using NetEscapades.AspNetCore.SecurityHeaders; 2 | 3 | namespace BlazorWebFromBlazorServerOidc; 4 | 5 | public class NonceMiddleware 6 | { 7 | private readonly RequestDelegate _next; 8 | 9 | public NonceMiddleware(RequestDelegate next) 10 | { 11 | _next = next; 12 | } 13 | 14 | public async Task Invoke(HttpContext context, BlazorNonceService blazorNonceService) 15 | { 16 | var nonce = context.GetNonce(); 17 | 18 | if (nonce != null) 19 | { 20 | blazorNonceService.SetNonce(nonce); 21 | } 22 | await _next.Invoke(context); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | 3 | Counter 4 | 5 |

    Counter

    6 | 7 |

    Current count: @currentCount

    8 | 9 | 10 | 11 | @code { 12 | private int currentCount = 0; 13 | 14 | private void IncrementCount() 15 | { 16 | currentCount++; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model BlazorWebFromBlazorServerOidc.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
    17 |
    18 |

    Error.

    19 |

    An error occurred while processing your request.

    20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

    24 | Request ID: @Model.RequestId 25 |

    26 | } 27 | 28 |

    Development Mode

    29 |

    30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

    32 |

    33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

    38 |
    39 |
    40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.Diagnostics; 4 | 5 | namespace BlazorWebFromBlazorServerOidc.Pages 6 | { 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | public class ErrorModel : PageModel 10 | { 11 | public string? RequestId { get; set; } 12 | 13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 14 | 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public void OnGet() 23 | { 24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Pages/FetchData.razor: -------------------------------------------------------------------------------- 1 | @page "/fetchdata" 2 | 3 | Weather forecast 4 | 5 | @using BlazorWebFromBlazorServerOidc.Data 6 | @inject WeatherForecastService ForecastService 7 | 8 |

    Weather forecast

    9 | 10 |

    This component demonstrates fetching data from a service.

    11 | 12 | @if (forecasts == null) 13 | { 14 |

    Loading...

    15 | } 16 | else 17 | { 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | @foreach (var forecast in forecasts) 29 | { 30 | 31 | 32 | 33 | 34 | 35 | 36 | } 37 | 38 |
    DateTemp. (C)Temp. (F)Summary
    @forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
    39 | } 40 | 41 | @code { 42 | private WeatherForecast[]? forecasts; 43 | 44 | protected override async Task OnInitializedAsync() 45 | { 46 | forecasts = await ForecastService.GetForecastAsync(DateTime.Now); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Index 4 | 5 |

    Hello, world!

    6 | 7 | Welcome to your new app. 8 | 9 | 10 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Pages/SignedOut.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @addTagHelper *, NetEscapades.AspNetCore.SecurityHeaders.TagHelpers 3 | @model BlazorWebFromBlazorServerOidc.Pages.SignedOutModel 4 | @{ 5 | ViewData["Title"] = "SignedOut"; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 35 |
    36 |
    37 |
    38 |

    You have successfully signed out

    39 |
    40 |
    41 | 42 |
    43 |
    44 | Signed out 45 |
    46 |
    47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Pages/SignedOut.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace BlazorWebFromBlazorServerOidc.Pages; 5 | 6 | [AllowAnonymous] 7 | public class SignedOutModel : PageModel 8 | { 9 | public void OnGet() 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Program.cs: -------------------------------------------------------------------------------- 1 | using BlazorWebFromBlazorServerOidc.Data; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Components.Server.Circuits; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.Authorization; 8 | using Microsoft.Extensions.DependencyInjection.Extensions; 9 | using Microsoft.IdentityModel.JsonWebTokens; 10 | using Microsoft.IdentityModel.Protocols.OpenIdConnect; 11 | using Microsoft.IdentityModel.Tokens; 12 | using NetEscapades.AspNetCore.SecurityHeaders.Infrastructure; 13 | 14 | namespace BlazorWebFromBlazorServerOidc; 15 | 16 | public class Program 17 | { 18 | public static void Main(string[] args) 19 | { 20 | var builder = WebApplication.CreateBuilder(args); 21 | 22 | builder.Services.AddSecurityHeaderPolicies() 23 | .SetPolicySelector((PolicySelectorContext ctx) => 24 | { 25 | return SecurityHeadersDefinitions.GetHeaderPolicyCollection(builder.Environment.IsDevelopment(), 26 | builder.Configuration["OpenIDConnectSettings:Authority"]); 27 | }); 28 | 29 | builder.Services.AddRazorComponents() 30 | .AddInteractiveServerComponents(); 31 | 32 | builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped(sp => 33 | sp.GetRequiredService())); 34 | 35 | builder.Services.AddScoped(); 36 | 37 | builder.Services.AddAuthentication(options => 38 | { 39 | options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 40 | options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; 41 | }) 42 | .AddCookie() 43 | .AddOpenIdConnect(options => 44 | { 45 | builder.Configuration.GetSection("OpenIDConnectSettings").Bind(options); 46 | 47 | options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; 48 | options.ResponseType = OpenIdConnectResponseType.Code; 49 | 50 | options.SaveTokens = true; 51 | options.GetClaimsFromUserInfoEndpoint = true; 52 | options.TokenValidationParameters = new TokenValidationParameters 53 | { 54 | NameClaimType = "name" 55 | }; 56 | }); 57 | 58 | builder.Services.AddRazorPages().AddMvcOptions(options => 59 | { 60 | var policy = new AuthorizationPolicyBuilder() 61 | .RequireAuthenticatedUser() 62 | .Build(); 63 | options.Filters.Add(new AuthorizeFilter(policy)); 64 | }); 65 | 66 | builder.Services.AddSingleton(); 67 | 68 | builder.Services.AddControllersWithViews(options => 69 | options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())); 70 | 71 | var app = builder.Build(); 72 | 73 | JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear(); 74 | 75 | if (!app.Environment.IsDevelopment()) 76 | { 77 | app.UseExceptionHandler("/Error"); 78 | app.UseHsts(); 79 | } 80 | 81 | // Using an unsecure CSP as CSP nonce is not supported in Blazor Web ... 82 | app.UseSecurityHeaders(); 83 | 84 | app.UseMiddleware(); 85 | 86 | app.UseHttpsRedirection(); 87 | 88 | app.UseStaticFiles(); 89 | 90 | app.UseRouting(); 91 | 92 | app.UseAuthentication(); 93 | app.UseAuthorization(); 94 | 95 | app.UseAntiforgery(); 96 | 97 | app.MapRazorPages(); 98 | app.MapControllers(); 99 | 100 | app.MapRazorComponents() 101 | .AddInteractiveServerRenderMode().RequireAuthorization(); 102 | 103 | app.Run(); 104 | } 105 | } -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "https://localhost:5001", 7 | "sslPort": 5001 8 | } 9 | }, 10 | "profiles": { 11 | "BlazorWebFromBlazorServerOidc": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "https://localhost:5001", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/Routes.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | 4 | 5 | 6 | 7 | 8 | @{ 9 | var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); 10 | NavigationManager.NavigateTo($"api/account/login?redirectUri={returnUrl}", forceLoad: true); 11 | } 12 | 13 | 14 | Wait... 15 | 16 | 17 | 18 | 19 | 20 |

    Sorry, there's nothing at this address.

    21 |
    22 |
    23 |
    24 |
    -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/SecurityHeadersDefinitions.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorWebFromBlazorServerOidc; 2 | 3 | public static class SecurityHeadersDefinitions 4 | { 5 | public static HeaderPolicyCollection GetHeaderPolicyCollection(bool isDev, string? idpHost) 6 | { 7 | ArgumentNullException.ThrowIfNull(idpHost); 8 | 9 | var policy = new HeaderPolicyCollection() 10 | .AddFrameOptionsDeny() 11 | .AddContentTypeOptionsNoSniff() 12 | .AddReferrerPolicyStrictOriginWhenCrossOrigin() 13 | .AddCrossOriginOpenerPolicy(builder => builder.SameOrigin()) 14 | .AddCrossOriginResourcePolicy(builder => builder.SameOrigin()) 15 | .AddCrossOriginEmbedderPolicy(builder => builder.RequireCorp()) // remove for dev if using hot reload 16 | .AddContentSecurityPolicy(builder => 17 | { 18 | builder.AddObjectSrc().None(); 19 | builder.AddBlockAllMixedContent(); 20 | builder.AddImgSrc().Self().From("data:"); 21 | builder.AddFormAction().Self().From(idpHost); 22 | builder.AddFontSrc().Self(); 23 | builder.AddBaseUri().Self(); 24 | builder.AddFrameAncestors().None(); 25 | 26 | builder.AddStyleSrc() 27 | .UnsafeInline() 28 | .Self(); 29 | 30 | // due to Blazor 31 | builder.AddScriptSrc() 32 | .WithNonce() 33 | .UnsafeEval() // due to Blazor WASM 34 | .StrictDynamic() 35 | .OverHttps() 36 | .UnsafeInline(); // only a fallback for older browsers when the nonce is used 37 | }) 38 | .RemoveServerHeader() 39 | .AddPermissionsPolicyWithDefaultSecureDirectives(); 40 | 41 | if (!isDev) 42 | { 43 | // maxage = one year in seconds 44 | policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(); 45 | } 46 | 47 | return policy; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using BlazorWebFromBlazorServerOidc 10 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 11 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "OpenIDConnectSettings": { 3 | "Authority": "https://localhost:44318", 4 | "ClientId": "oidc-pkce-confidential", 5 | "ClientSecret": "oidc-pkce-confidential_secret" 6 | }, 7 | "Logging": { 8 | "LogLevel": { 9 | "Default": "Information", 10 | "Microsoft.AspNetCore": "Warning" 11 | } 12 | }, 13 | "AllowedHosts": "*" 14 | } 15 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/wwwroot/.well-known/security.txt: -------------------------------------------------------------------------------- 1 | Security email 2 | 3 | contact: mailto: security-dev-officer@secure.ch 4 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](https://github.com/iconic/open-iconic) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](https://github.com/iconic/open-iconic). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](https://github.com/iconic/open-iconic) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](https://github.com/iconic/open-iconic) and [Reference](https://github.com/iconic/open-iconic) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/Old/BlazorWebFromBlazorServerOidc/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | h1:focus { 8 | outline: none; 9 | } 10 | 11 | a, .btn-link { 12 | color: #0071c1; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .content { 22 | padding-top: 1.1rem; 23 | } 24 | 25 | .valid.modified:not([type=checkbox]) { 26 | outline: 1px solid #26b050; 27 | } 28 | 29 | .invalid { 30 | outline: 1px solid red; 31 | } 32 | 33 | .validation-message { 34 | color: red; 35 | } 36 | 37 | #blazor-error-ui { 38 | background: lightyellow; 39 | bottom: 0; 40 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 41 | display: none; 42 | left: 0; 43 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 44 | position: fixed; 45 | width: 100%; 46 | z-index: 1000; 47 | } 48 | 49 | #blazor-error-ui .dismiss { 50 | cursor: pointer; 51 | position: absolute; 52 | right: 0.75rem; 53 | top: 0.5rem; 54 | } 55 | 56 | .blazor-error-boundary { 57 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 58 | padding: 1rem 1rem 1rem 3.7rem; 59 | color: white; 60 | } 61 | 62 | .blazor-error-boundary::after { 63 | content: "An error has occurred." 64 | } 65 | -------------------------------------------------------------------------------- /Old/BlazorWebFromBlazorServerOidc/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damienbod/BlazorServerOidc/e8832463ab851601db495e16989e2d9e3531d1ee/Old/BlazorWebFromBlazorServerOidc/wwwroot/favicon.ico -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blazor Server OpenID Connect 2 | 3 | Implements a confidential client using OpenID Connect (code flow with PKCE) 4 | 5 | [![.NET](https://github.com/damienbod/BlazorServerOidc/actions/workflows/dotnet.yml/badge.svg)](https://github.com/damienbod/BlazorServerOidc/actions/workflows/dotnet.yml) 6 | 7 | [Securing a Blazor Server application using OpenID Connect and security headers](https://damienbod.com/2024/01/03/securing-a-blazor-server-application-using-openid-connect-and-security-headers/) 8 | 9 | [Migrate ASP.NET Core Blazor Server to Blazor Web](https://damienbod.com/2024/01/15/migrate-asp-net-core-blazor-server-to-blazor-web/) 10 | 11 | [Revisiting using a Content Security Policy (CSP) nonce in Blazor](https://damienbod.com/2025/05/26/revisiting-using-a-content-security-policy-csp-nonce-in-blazor/) 12 | 13 | ## Old blogs 14 | 15 | [Using a CSP nonce in Blazor Web](https://damienbod.com/2024/02/19/using-a-csp-nonce-in-blazor-web/) 16 | 17 | ## Migrations 18 | 19 | ### Powershell (identity provider project) 20 | 21 | Add-Migration "init_sts" -c ApplicationDbContext 22 | 23 | ### Running manually 24 | 25 | Update-Database -Context ApplicationDbContext 26 | 27 | ## History 28 | 29 | - 2025-05-25 Updated packages, new CSP nonce implementation 30 | - 2025-05-06 Updated packages 31 | - 2024-12-31 Updated packages, .NET 9 32 | - 2024-10-21 Updated packages 33 | - 2024-10-03 Updated packages, updated security headers 34 | - 2024-06-22 Updated packages 35 | - 2024-05-26 Updated packages 36 | - 2024-04-24 Updated packages 37 | - 2024-03-24 Updated packages 38 | - 2024-02-19 Updated packages 39 | - 2024-02-16 Updated packages 40 | - 2024-02-12 Fix CSP, use nonce 41 | - 2024-01-14 Updated packages 42 | - 2024-01-11 Added support for Blazor Web, migrated from Blazor Server 43 | 44 | ## Links 45 | 46 | https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/ 47 | 48 | https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/interactive-server-side-rendering 49 | 50 | https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/quick-start-blazor-server-app 51 | 52 | https://stackoverflow.com/questions/64853618/oidc-authentication-in-server-side-blazor 53 | 54 | https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims 55 | 56 | https://openid.net/developers/how-connect-works/ 57 | 58 | https://github.com/openiddict/openiddict-core 59 | 60 | https://datatracker.ietf.org/doc/html/rfc9126 61 | 62 | https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims 63 | 64 | https://stackoverflow.com/questions/59121741/anti-forgery-token-validation-in-mvc-app-with-blazor-server-side-component 65 | 66 | ## Switch Blazor Server to Blazor Web (Server) 67 | 68 | > [!WARNING] 69 | > The required security headers can only be applied to Blazor Web in InteractiveServer mode 70 | 71 | https://github.com/javiercn/BlazorWebNonceService 72 | 73 | https://learn.microsoft.com/en-us/aspnet/core/migration/70-80 74 | 75 | https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models --------------------------------------------------------------------------------