├── Identity ├── Pages │ ├── _ViewStart.cshtml │ ├── _ViewImports.cshtml │ ├── Account │ │ ├── Logout │ │ │ ├── LogoutOptions.cs │ │ │ ├── LoggedOutViewModel.cs │ │ │ ├── Index.cshtml │ │ │ ├── LoggedOut.cshtml │ │ │ ├── LoggedOut.cshtml.cs │ │ │ └── Index.cshtml.cs │ │ ├── AccessDenied.cshtml.cs │ │ ├── AccessDenied.cshtml │ │ └── Login │ │ │ ├── LoginOptions.cs │ │ │ ├── InputModel.cs │ │ │ ├── ViewModel.cs │ │ │ ├── Index.cshtml │ │ │ └── Index.cshtml.cs │ ├── Shared │ │ ├── _ValidationSummary.cshtml │ │ ├── _Layout.cshtml │ │ └── _Nav.cshtml │ ├── Device │ │ ├── Success.cshtml │ │ ├── Success.cshtml.cs │ │ ├── InputModel.cs │ │ ├── ViewModel.cs │ │ ├── DeviceOptions.cs │ │ ├── _ScopeListItem.cshtml │ │ ├── Index.cshtml │ │ └── Index.cshtml.cs │ ├── ExternalLogin │ │ ├── Callback.cshtml │ │ ├── Challenge.cshtml │ │ ├── Challenge.cshtml.cs │ │ └── Callback.cshtml.cs │ ├── Ciba │ │ ├── InputModel.cs │ │ ├── ConsentOptions.cs │ │ ├── Index.cshtml │ │ ├── ViewModel.cs │ │ ├── All.cshtml.cs │ │ ├── Index.cshtml.cs │ │ ├── _ScopeListItem.cshtml │ │ ├── All.cshtml │ │ ├── Consent.cshtml │ │ └── Consent.cshtml.cs │ ├── Redirect │ │ ├── Index.cshtml │ │ └── Index.cshtml.cs │ ├── Consent │ │ ├── InputModel.cs │ │ ├── ConsentOptions.cs │ │ ├── ViewModel.cs │ │ ├── _ScopeListItem.cshtml │ │ ├── Index.cshtml │ │ └── Index.cshtml.cs │ ├── Home │ │ └── Error │ │ │ ├── ViewModel.cs │ │ │ ├── Index.cshtml │ │ │ └── Index.cshtml.cs │ ├── Index.cshtml.cs │ ├── Grants │ │ ├── ViewModel.cs │ │ ├── Index.cshtml.cs │ │ └── Index.cshtml │ ├── Diagnostics │ │ ├── Index.cshtml.cs │ │ ├── ViewModel.cs │ │ └── Index.cshtml │ ├── Index.cshtml │ ├── Extensions.cs │ ├── ServerSideSessions │ │ ├── Index.cshtml.cs │ │ └── Index.cshtml │ ├── TestUsers.cs │ └── SecurityHeadersAttribute.cs ├── wwwroot │ ├── favicon.ico │ ├── js │ │ ├── signin-redirect.js │ │ └── signout-redirect.js │ ├── lib │ │ ├── bootstrap4-glyphicons │ │ │ ├── fonts │ │ │ │ └── glyphicons │ │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ ├── css │ │ │ │ └── bootstrap-glyphicons.min.css │ │ │ └── LICENSE │ │ ├── jquery │ │ │ ├── LICENSE.txt │ │ │ └── README.md │ │ └── bootstrap │ │ │ ├── LICENSE │ │ │ ├── dist │ │ │ └── css │ │ │ │ ├── bootstrap-reboot.min.css │ │ │ │ └── bootstrap-reboot.css │ │ │ └── README.md │ ├── css │ │ ├── site.min.css │ │ ├── site.css │ │ └── site.scss │ └── duende-logo.svg ├── Properties │ └── launchSettings.json ├── Identity.csproj ├── Program.cs ├── Config.cs ├── keys │ └── is-signing-key-10F4A01466079A821AA98468F5C832D6.json └── HostingExtensions.cs ├── Backend ├── appsettings.Development.json ├── appsettings.json ├── Backend.csproj ├── Properties │ └── launchSettings.json └── Program.cs ├── Proxy ├── appsettings.Development.json ├── Proxy.csproj ├── Properties │ └── launchSettings.json ├── appsettings.json └── Program.cs ├── .idea ├── .idea.Flowing │ └── .idea │ │ ├── encodings.xml │ │ ├── vcs.xml │ │ ├── indexLayout.xml │ │ ├── misc.xml │ │ └── .gitignore ├── modules.xml ├── Flowing.iml └── workspace.xml ├── Flowing.sln └── .gitignore /Identity/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } -------------------------------------------------------------------------------- /Identity/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Identity.Pages 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -------------------------------------------------------------------------------- /Identity/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khalidabuhakmeh/YarpReverseProxyFlowThroughAuth/HEAD/Identity/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Identity/wwwroot/js/signin-redirect.js: -------------------------------------------------------------------------------- 1 | window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); 2 | -------------------------------------------------------------------------------- /Backend/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Proxy/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Backend/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /.idea/.idea.Flowing/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.idea.Flowing/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Identity/wwwroot/js/signout-redirect.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function () { 2 | var a = document.querySelector("a.PostLogoutRedirectUri"); 3 | if (a) { 4 | window.location = a.href; 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /Identity/Pages/Account/Logout/LogoutOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Identity.Pages.Logout; 2 | 3 | public class LogoutOptions 4 | { 5 | public static bool ShowLogoutPrompt = true; 6 | public static bool AutomaticRedirectAfterSignOut = false; 7 | } -------------------------------------------------------------------------------- /Identity/Pages/Account/AccessDenied.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace Identity.Pages.Account; 4 | 5 | public class AccessDeniedModel : PageModel 6 | { 7 | public void OnGet() 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /Identity/Pages/Shared/_ValidationSummary.cshtml: -------------------------------------------------------------------------------- 1 | @if (ViewContext.ModelState.IsValid == false) 2 | { 3 |
4 | Error 5 |
6 |
7 | } -------------------------------------------------------------------------------- /.idea/.idea.Flowing/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Identity/wwwroot/lib/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khalidabuhakmeh/YarpReverseProxyFlowThroughAuth/HEAD/Identity/wwwroot/lib/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /Identity/wwwroot/lib/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khalidabuhakmeh/YarpReverseProxyFlowThroughAuth/HEAD/Identity/wwwroot/lib/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /Identity/wwwroot/lib/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khalidabuhakmeh/YarpReverseProxyFlowThroughAuth/HEAD/Identity/wwwroot/lib/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /Identity/wwwroot/lib/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khalidabuhakmeh/YarpReverseProxyFlowThroughAuth/HEAD/Identity/wwwroot/lib/bootstrap4-glyphicons/fonts/glyphicons/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /.idea/.idea.Flowing/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /Identity/Pages/Account/AccessDenied.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Account.AccessDeniedModel 3 | @{ 4 | } 5 |
6 |
7 |

Access Denied

8 |

You do not have permission to access that resource.

9 |
10 |
-------------------------------------------------------------------------------- /Identity/Pages/Device/Success.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Device.SuccessModel 3 | @{ 4 | } 5 | 6 | 7 |
8 |
9 |

Success

10 |

You have successfully authorized the device

11 |
12 |
-------------------------------------------------------------------------------- /Identity/Pages/ExternalLogin/Callback.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.ExternalLogin.Callback 3 | 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Identity/Pages/ExternalLogin/Challenge.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.ExternalLogin.Challenge 3 | 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /Identity/Pages/Device/Success.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace Identity.Pages.Device; 5 | 6 | [SecurityHeaders] 7 | [Authorize] 8 | public class SuccessModel : PageModel 9 | { 10 | public void OnGet() 11 | { 12 | } 13 | } -------------------------------------------------------------------------------- /Identity/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SelfHost": { 4 | "commandName": "Project", 5 | "launchBrowser": false, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://*:5001" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.idea/.idea.Flowing/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /contentModel.xml 6 | /modules.xml 7 | /projectSettingsUpdater.xml 8 | /.idea.Flowing.iml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /.idea/Flowing.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Identity/Pages/Account/Login/LoginOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Identity.Pages.Login; 2 | 3 | public class LoginOptions 4 | { 5 | public static bool AllowLocalLogin = true; 6 | public static bool AllowRememberLogin = true; 7 | public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); 8 | public static string InvalidCredentialsErrorMessage = "Invalid username or password"; 9 | } -------------------------------------------------------------------------------- /Identity/Pages/Device/InputModel.cs: -------------------------------------------------------------------------------- 1 | namespace Identity.Pages.Device; 2 | 3 | public class InputModel 4 | { 5 | public string Button { get; set; } 6 | public IEnumerable ScopesConsented { get; set; } 7 | public bool RememberConsent { get; set; } = true; 8 | public string ReturnUrl { get; set; } 9 | public string Description { get; set; } 10 | public string UserCode { get; set; } 11 | } -------------------------------------------------------------------------------- /Identity/Pages/Ciba/InputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | namespace Identity.Pages.Ciba; 5 | 6 | public class InputModel 7 | { 8 | public string Button { get; set; } 9 | public IEnumerable ScopesConsented { get; set; } 10 | public string Id { get; set; } 11 | public string Description { get; set; } 12 | } -------------------------------------------------------------------------------- /Identity/Pages/Redirect/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Redirect.IndexModel 3 | @{ 4 | } 5 | 6 |
7 |
8 |

You are now being returned to the application

9 |

Once complete, you may close this tab.

10 |
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /Identity/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | .welcome-page .logo{width:64px;}.icon-banner{width:32px;}.body-container{margin-top:60px;padding-bottom:40px;}.welcome-page li{list-style:none;padding:4px;}.logged-out-page iframe{display:none;width:0;height:0;}.grants-page .card{margin-top:20px;border-bottom:1px solid #d3d3d3;}.grants-page .card .card-title{font-size:120%;font-weight:bold;}.grants-page .card .card-title img{width:100px;height:100px;}.grants-page .card label{font-weight:bold;} -------------------------------------------------------------------------------- /Identity/Pages/Account/Logout/LoggedOutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | namespace Identity.Pages.Logout; 6 | 7 | public class LoggedOutViewModel 8 | { 9 | public string PostLogoutRedirectUri { get; set; } 10 | public string ClientName { get; set; } 11 | public string SignOutIframeUrl { get; set; } 12 | public bool AutomaticRedirectAfterSignOut { get; set; } 13 | } -------------------------------------------------------------------------------- /Identity/Pages/Consent/InputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | namespace Identity.Pages.Consent; 5 | 6 | public class InputModel 7 | { 8 | public string Button { get; set; } 9 | public IEnumerable ScopesConsented { get; set; } 10 | public bool RememberConsent { get; set; } = true; 11 | public string ReturnUrl { get; set; } 12 | public string Description { get; set; } 13 | } -------------------------------------------------------------------------------- /Identity/Pages/Home/Error/ViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | using Duende.IdentityServer.Models; 5 | 6 | namespace Identity.Pages.Error; 7 | 8 | public class ViewModel 9 | { 10 | public ViewModel() 11 | { 12 | } 13 | 14 | public ViewModel(string error) 15 | { 16 | Error = new ErrorMessage { Error = error }; 17 | } 18 | 19 | public ErrorMessage Error { get; set; } 20 | } -------------------------------------------------------------------------------- /Backend/Backend.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Identity/Pages/Account/Logout/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Logout.Index 3 | 4 |
5 |
6 |

Logout

7 |

Would you like to logout of IdentityServer?

8 |
9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 |
17 |
-------------------------------------------------------------------------------- /Identity/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace Identity.Pages.Home; 6 | 7 | [AllowAnonymous] 8 | public class Index : PageModel 9 | { 10 | public string Version; 11 | 12 | public void OnGet() 13 | { 14 | Version = typeof(Duende.IdentityServer.Hosting.IdentityServerMiddleware).Assembly 15 | .GetCustomAttribute()?.InformationalVersion.Split('+').First(); 16 | } 17 | } -------------------------------------------------------------------------------- /Identity/Pages/Account/Login/InputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace Identity.Pages.Login; 8 | 9 | public class InputModel 10 | { 11 | [Required] public string Username { get; set; } 12 | 13 | [Required] public string Password { get; set; } 14 | 15 | public bool RememberLogin { get; set; } 16 | 17 | public string ReturnUrl { get; set; } 18 | 19 | public string Button { get; set; } 20 | } -------------------------------------------------------------------------------- /Identity/Pages/Redirect/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace Identity.Pages.Redirect; 6 | 7 | [AllowAnonymous] 8 | public class IndexModel : PageModel 9 | { 10 | public string RedirectUri { get; set; } 11 | 12 | public IActionResult OnGet(string redirectUri) 13 | { 14 | if (!Url.IsLocalUrl(redirectUri)) 15 | { 16 | return RedirectToPage("/Home/Error/Index"); 17 | } 18 | 19 | RedirectUri = redirectUri; 20 | return Page(); 21 | } 22 | } -------------------------------------------------------------------------------- /Identity/Identity.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Identity/Pages/Grants/ViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Identity.Pages.Grants; 2 | 3 | public class ViewModel 4 | { 5 | public IEnumerable Grants { get; set; } 6 | } 7 | 8 | public class GrantViewModel 9 | { 10 | public string ClientId { get; set; } 11 | public string ClientName { get; set; } 12 | public string ClientUrl { get; set; } 13 | public string ClientLogoUrl { get; set; } 14 | public string Description { get; set; } 15 | public DateTime Created { get; set; } 16 | public DateTime? Expires { get; set; } 17 | public IEnumerable IdentityGrantNames { get; set; } 18 | public IEnumerable ApiGrantNames { get; set; } 19 | } -------------------------------------------------------------------------------- /Identity/Pages/Ciba/ConsentOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | namespace Identity.Pages.Ciba; 6 | 7 | public class ConsentOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | 12 | public static string OfflineAccessDescription = 13 | "Access to your applications and resources, even when you are offline"; 14 | 15 | public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; 16 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 17 | } -------------------------------------------------------------------------------- /Identity/Pages/Consent/ConsentOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | namespace Identity.Pages.Consent; 6 | 7 | public class ConsentOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | 12 | public static string OfflineAccessDescription = 13 | "Access to your applications and resources, even when you are offline"; 14 | 15 | public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; 16 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 17 | } -------------------------------------------------------------------------------- /Identity/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | .welcome-page .logo { 2 | width: 64px; 3 | } 4 | 5 | .icon-banner { 6 | width: 32px; 7 | } 8 | 9 | .body-container { 10 | margin-top: 60px; 11 | padding-bottom: 40px; 12 | } 13 | 14 | .welcome-page li { 15 | list-style: none; 16 | padding: 4px; 17 | } 18 | 19 | .logged-out-page iframe { 20 | display: none; 21 | width: 0; 22 | height: 0; 23 | } 24 | 25 | .grants-page .card { 26 | margin-top: 20px; 27 | border-bottom: 1px solid lightgray; 28 | } 29 | .grants-page .card .card-title { 30 | font-size: 120%; 31 | font-weight: bold; 32 | } 33 | .grants-page .card .card-title img { 34 | width: 100px; 35 | height: 100px; 36 | } 37 | .grants-page .card label { 38 | font-weight: bold; 39 | } 40 | -------------------------------------------------------------------------------- /Identity/Pages/Device/ViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Identity.Pages.Device; 2 | 3 | public class ViewModel 4 | { 5 | public string ClientName { get; set; } 6 | public string ClientUrl { get; set; } 7 | public string ClientLogoUrl { get; set; } 8 | public bool AllowRememberConsent { get; set; } 9 | 10 | public IEnumerable IdentityScopes { get; set; } 11 | public IEnumerable ApiScopes { get; set; } 12 | } 13 | 14 | public class ScopeViewModel 15 | { 16 | public string Value { get; set; } 17 | public string DisplayName { get; set; } 18 | public string Description { get; set; } 19 | public bool Emphasize { get; set; } 20 | public bool Required { get; set; } 21 | public bool Checked { get; set; } 22 | } -------------------------------------------------------------------------------- /Proxy/Proxy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <_ContentIncludedByDefault Remove="Pages\Account\Login\Index.cshtml" /> 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Identity/Pages/Device/DeviceOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | namespace Identity.Pages.Device; 6 | 7 | public class DeviceOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | 12 | public static string OfflineAccessDescription = 13 | "Access to your applications and resources, even when you are offline"; 14 | 15 | public static readonly string InvalidUserCode = "Invalid user code"; 16 | public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; 17 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 18 | } -------------------------------------------------------------------------------- /Proxy/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:14991", 7 | "sslPort": 44385 8 | } 9 | }, 10 | "profiles": { 11 | "Proxy": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "https://localhost:7291;http://localhost:5121", 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 | -------------------------------------------------------------------------------- /Backend/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:26337", 7 | "sslPort": 44388 8 | } 9 | }, 10 | "profiles": { 11 | "Backend": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": false, 15 | "applicationUrl": "https://localhost:7008;http://localhost:5135", 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 | -------------------------------------------------------------------------------- /Identity/wwwroot/css/site.scss: -------------------------------------------------------------------------------- 1 | .welcome-page { 2 | .logo { 3 | width: 64px; 4 | } 5 | } 6 | 7 | .icon-banner { 8 | width: 32px; 9 | } 10 | 11 | .body-container { 12 | margin-top: 60px; 13 | padding-bottom: 40px; 14 | } 15 | 16 | .welcome-page { 17 | li { 18 | list-style: none; 19 | padding: 4px; 20 | } 21 | } 22 | 23 | .logged-out-page { 24 | iframe { 25 | display: none; 26 | width: 0; 27 | height: 0; 28 | } 29 | } 30 | 31 | .grants-page { 32 | .card { 33 | margin-top: 20px; 34 | border-bottom: 1px solid lightgray; 35 | 36 | .card-title { 37 | img { 38 | width: 100px; 39 | height: 100px; 40 | } 41 | 42 | font-size: 120%; 43 | font-weight: bold; 44 | } 45 | 46 | label { 47 | font-weight: bold; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Identity/wwwroot/duende-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | true 14 | 15 | -------------------------------------------------------------------------------- /Identity/Pages/Diagnostics/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | using Microsoft.AspNetCore.Authorization; 5 | 6 | namespace Identity.Pages.Diagnostics; 7 | 8 | [SecurityHeaders] 9 | [Authorize] 10 | public class Index : PageModel 11 | { 12 | public ViewModel View { get; set; } 13 | 14 | public async Task OnGet() 15 | { 16 | var localAddresses = new string[] { "127.0.0.1", "::1", HttpContext.Connection.LocalIpAddress.ToString() }; 17 | if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString())) 18 | { 19 | return NotFound(); 20 | } 21 | 22 | View = new ViewModel(await HttpContext.AuthenticateAsync()); 23 | 24 | return Page(); 25 | } 26 | } -------------------------------------------------------------------------------- /Identity/Pages/Account/Logout/LoggedOut.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Logout.LoggedOut 3 | 4 |
5 |

6 | Logout 7 | You are now logged out 8 |

9 | 10 | @if (Model.View.PostLogoutRedirectUri != null) 11 | { 12 |
13 | Click here to return to the 14 | @Model.View.ClientName application. 15 |
16 | } 17 | 18 | @if (Model.View.SignOutIframeUrl != null) 19 | { 20 | 21 | } 22 |
23 | 24 | @section scripts 25 | { 26 | @if (Model.View.AutomaticRedirectAfterSignOut) 27 | { 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /Identity/Program.cs: -------------------------------------------------------------------------------- 1 | using Identity; 2 | using Serilog; 3 | 4 | Log.Logger = new LoggerConfiguration() 5 | .WriteTo.Console() 6 | .CreateBootstrapLogger(); 7 | 8 | Log.Information("Starting up"); 9 | 10 | try 11 | { 12 | var builder = WebApplication.CreateBuilder(args); 13 | 14 | builder.Host.UseSerilog((ctx, lc) => lc 15 | .WriteTo.Console( 16 | outputTemplate: 17 | "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}") 18 | .Enrich.FromLogContext() 19 | .ReadFrom.Configuration(ctx.Configuration)); 20 | 21 | var app = builder 22 | .ConfigureServices() 23 | .ConfigurePipeline(); 24 | 25 | app.Run(); 26 | } 27 | catch (Exception ex) 28 | { 29 | Log.Fatal(ex, "Unhandled exception"); 30 | } 31 | finally 32 | { 33 | Log.Information("Shut down complete"); 34 | Log.CloseAndFlush(); 35 | } -------------------------------------------------------------------------------- /Identity/Pages/Diagnostics/ViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityModel; 6 | using Microsoft.AspNetCore.Authentication; 7 | using System.Text; 8 | using System.Text.Json; 9 | 10 | namespace Identity.Pages.Diagnostics; 11 | 12 | public class ViewModel 13 | { 14 | public ViewModel(AuthenticateResult result) 15 | { 16 | AuthenticateResult = result; 17 | 18 | if (result.Properties.Items.ContainsKey("client_list")) 19 | { 20 | var encoded = result.Properties.Items["client_list"]; 21 | var bytes = Base64Url.Decode(encoded); 22 | var value = Encoding.UTF8.GetString(bytes); 23 | 24 | Clients = JsonSerializer.Deserialize(value); 25 | } 26 | } 27 | 28 | public AuthenticateResult AuthenticateResult { get; } 29 | public IEnumerable Clients { get; } = new List(); 30 | } -------------------------------------------------------------------------------- /Identity/Pages/Ciba/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Ciba.IndexModel 3 | @{ 4 | } 5 | 6 |
7 |
8 | @if (Model.LoginRequest.Client.LogoUri != null) 9 | { 10 | 13 | } 14 |

15 | @Model.LoginRequest.Client.ClientName 16 | is requesting your permission 17 |

18 | 19 |

20 | Verify that this identifier matches what the client is displaying: 21 | @Model.LoginRequest.BindingMessage 22 |

23 | 24 |

25 | Do you wish to continue? 26 |

27 |
28 | Yes, Continue 29 |
30 | 31 |
32 |
-------------------------------------------------------------------------------- /Identity/Pages/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Duende IdentityServer 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | @RenderBody() 22 |
23 | 24 | 25 | 26 | 27 | @RenderSection("scripts", required: false) 28 | 29 | -------------------------------------------------------------------------------- /Identity/Pages/Home/Error/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Error.Index 3 | 4 |
5 |
6 |

Error

7 |
8 | 9 |
10 |
11 |
12 | Sorry, there was an error 13 | 14 | @if (Model.View.Error != null) 15 | { 16 | 17 | 18 | : @Model.View.Error.Error 19 | 20 | 21 | 22 | if (Model.View.Error.ErrorDescription != null) 23 | { 24 |
@Model.View.Error.ErrorDescription
25 | } 26 | } 27 |
28 | 29 | @if (Model?.View?.Error?.RequestId != null) 30 | { 31 |
Request Id: @Model.View.Error.RequestId
32 | } 33 |
34 |
35 |
-------------------------------------------------------------------------------- /Identity/Pages/Account/Login/ViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | namespace Identity.Pages.Login; 5 | 6 | public class ViewModel 7 | { 8 | public bool AllowRememberLogin { get; set; } = true; 9 | public bool EnableLocalLogin { get; set; } = true; 10 | 11 | public IEnumerable ExternalProviders { get; set; } = 12 | Enumerable.Empty(); 13 | 14 | public IEnumerable VisibleExternalProviders => 15 | ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); 16 | 17 | public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; 18 | 19 | public string ExternalLoginScheme => 20 | IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; 21 | 22 | public class ExternalProvider 23 | { 24 | public string DisplayName { get; set; } 25 | public string AuthenticationScheme { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /Identity/Pages/Shared/_Nav.cshtml: -------------------------------------------------------------------------------- 1 | @using Duende.IdentityServer.Extensions 2 | @{ 3 | string name = null; 4 | if (!true.Equals(ViewData["signed-out"])) 5 | { 6 | name = Context.User?.GetDisplayName(); 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Identity/Pages/Ciba/ViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | namespace Identity.Pages.Ciba; 5 | 6 | public class ViewModel 7 | { 8 | public string ClientName { get; set; } 9 | public string ClientUrl { get; set; } 10 | public string ClientLogoUrl { get; set; } 11 | 12 | public string BindingMessage { get; set; } 13 | 14 | public IEnumerable IdentityScopes { get; set; } 15 | public IEnumerable ApiScopes { get; set; } 16 | } 17 | 18 | public class ScopeViewModel 19 | { 20 | public string Name { get; set; } 21 | public string Value { get; set; } 22 | public string DisplayName { get; set; } 23 | public string Description { get; set; } 24 | public bool Emphasize { get; set; } 25 | public bool Required { get; set; } 26 | public bool Checked { get; set; } 27 | public IEnumerable Resources { get; set; } 28 | } 29 | 30 | public class ResourceViewModel 31 | { 32 | public string Name { get; set; } 33 | public string DisplayName { get; set; } 34 | } -------------------------------------------------------------------------------- /Identity/Pages/Consent/ViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | namespace Identity.Pages.Consent; 5 | 6 | public class ViewModel 7 | { 8 | public string ClientName { get; set; } 9 | public string ClientUrl { get; set; } 10 | public string ClientLogoUrl { get; set; } 11 | public bool AllowRememberConsent { get; set; } 12 | 13 | public IEnumerable IdentityScopes { get; set; } 14 | public IEnumerable ApiScopes { get; set; } 15 | } 16 | 17 | public class ScopeViewModel 18 | { 19 | public string Name { get; set; } 20 | public string Value { get; set; } 21 | public string DisplayName { get; set; } 22 | public string Description { get; set; } 23 | public bool Emphasize { get; set; } 24 | public bool Required { get; set; } 25 | public bool Checked { get; set; } 26 | public IEnumerable Resources { get; set; } 27 | } 28 | 29 | public class ResourceViewModel 30 | { 31 | public string Name { get; set; } 32 | public string DisplayName { get; set; } 33 | } -------------------------------------------------------------------------------- /Identity/Pages/Device/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @using Identity.Pages.Device 2 | @model ScopeViewModel 3 | 4 |
  • 5 | 25 | @if (Model.Required) 26 | { 27 | 28 | (required) 29 | 30 | } 31 | @if (Model.Description != null) 32 | { 33 | 36 | } 37 |
  • -------------------------------------------------------------------------------- /Identity/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Identity/Pages/Home/Error/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Services; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace Identity.Pages.Error; 6 | 7 | [AllowAnonymous] 8 | [SecurityHeaders] 9 | public class Index : PageModel 10 | { 11 | private readonly IIdentityServerInteractionService _interaction; 12 | private readonly IWebHostEnvironment _environment; 13 | 14 | public ViewModel View { get; set; } 15 | 16 | public Index(IIdentityServerInteractionService interaction, IWebHostEnvironment environment) 17 | { 18 | _interaction = interaction; 19 | _environment = environment; 20 | } 21 | 22 | public async Task OnGet(string errorId) 23 | { 24 | View = new ViewModel(); 25 | 26 | // retrieve error details from identityserver 27 | var message = await _interaction.GetErrorContextAsync(errorId); 28 | if (message != null) 29 | { 30 | View.Error = message; 31 | 32 | if (!_environment.IsDevelopment()) 33 | { 34 | // only show in development 35 | message.ErrorDescription = null; 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Identity/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2020 Twitter, Inc. 4 | Copyright (c) 2011-2020 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 | -------------------------------------------------------------------------------- /Identity/Pages/Ciba/All.cshtml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | using Duende.IdentityServer.Models; 6 | using Duende.IdentityServer.Services; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.RazorPages; 10 | 11 | namespace Identity.Pages.Ciba; 12 | 13 | [SecurityHeaders] 14 | [Authorize] 15 | public class AllModel : PageModel 16 | { 17 | public IEnumerable Logins { get; set; } 18 | 19 | [BindProperty, Required] public string Id { get; set; } 20 | [BindProperty, Required] public string Button { get; set; } 21 | 22 | private readonly IBackchannelAuthenticationInteractionService _backchannelAuthenticationInteraction; 23 | 24 | public AllModel(IBackchannelAuthenticationInteractionService backchannelAuthenticationInteractionService) 25 | { 26 | _backchannelAuthenticationInteraction = backchannelAuthenticationInteractionService; 27 | } 28 | 29 | public async Task OnGet() 30 | { 31 | Logins = await _backchannelAuthenticationInteraction.GetPendingLoginRequestsForCurrentUserAsync(); 32 | } 33 | } -------------------------------------------------------------------------------- /Identity/Pages/Account/Logout/LoggedOut.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Services; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace Identity.Pages.Logout; 6 | 7 | [SecurityHeaders] 8 | [AllowAnonymous] 9 | public class LoggedOut : PageModel 10 | { 11 | private readonly IIdentityServerInteractionService _interactionService; 12 | 13 | public LoggedOutViewModel View { get; set; } 14 | 15 | public LoggedOut(IIdentityServerInteractionService interactionService) 16 | { 17 | _interactionService = interactionService; 18 | } 19 | 20 | public async Task OnGet(string logoutId) 21 | { 22 | // get context information (client name, post logout redirect URI and iframe for federated signout) 23 | var logout = await _interactionService.GetLogoutContextAsync(logoutId); 24 | 25 | View = new LoggedOutViewModel 26 | { 27 | AutomaticRedirectAfterSignOut = LogoutOptions.AutomaticRedirectAfterSignOut, 28 | PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, 29 | ClientName = String.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName, 30 | SignOutIframeUrl = logout?.SignOutIFrameUrl 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /Identity/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Home.Index 3 | 4 |
    5 |

    6 | 7 | Welcome to Duende IdentityServer 8 | (version @Model.Version) 9 |

    10 | 11 |
      12 |
    • 13 | IdentityServer publishes a 14 | discovery document 15 | where you can find metadata and links to all the endpoints, key material, etc. 16 |
    • 17 |
    • 18 | Click here to see the claims for your current session. 19 |
    • 20 |
    • 21 | Click here to manage your stored grants. 22 |
    • 23 |
    • 24 | Click here to view the server side sessions. 25 |
    • 26 |
    • 27 | Click here to view your pending CIBA login requests. 28 |
    • 29 |
    • 30 | Here are links to the 31 | source code repository, 32 | and ready to use samples. 33 |
    • 34 |
    35 |
    -------------------------------------------------------------------------------- /Identity/Pages/Ciba/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | using Duende.IdentityServer.Models; 5 | using Duende.IdentityServer.Services; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | 10 | namespace Identity.Pages.Ciba; 11 | 12 | [AllowAnonymous] 13 | [SecurityHeaders] 14 | public class IndexModel : PageModel 15 | { 16 | public BackchannelUserLoginRequest LoginRequest { get; set; } 17 | 18 | private readonly IBackchannelAuthenticationInteractionService _backchannelAuthenticationInteraction; 19 | private readonly ILogger _logger; 20 | 21 | public IndexModel(IBackchannelAuthenticationInteractionService backchannelAuthenticationInteractionService, 22 | ILogger logger) 23 | { 24 | _backchannelAuthenticationInteraction = backchannelAuthenticationInteractionService; 25 | _logger = logger; 26 | } 27 | 28 | public async Task OnGet(string id) 29 | { 30 | LoginRequest = await _backchannelAuthenticationInteraction.GetLoginRequestByInternalIdAsync(id); 31 | if (LoginRequest == null) 32 | { 33 | _logger.LogWarning("Invalid backchannel login id {id}", id); 34 | return RedirectToPage("/Home/Error/Index"); 35 | } 36 | 37 | return Page(); 38 | } 39 | } -------------------------------------------------------------------------------- /Backend/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Duende.IdentityServer.Extensions; 3 | using Microsoft.AspNetCore.DataProtection; 4 | using Microsoft.AspNetCore.HttpOverrides; 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | 8 | builder.Services.Configure(options => 9 | { 10 | options.ForwardedHeaders = ForwardedHeaders.All; 11 | 12 | options.KnownNetworks.Clear(); 13 | options.KnownProxies.Clear(); 14 | }); 15 | builder.Services.AddAuthorization(o => { o.AddPolicy("auth", p => p.RequireAuthenticatedUser()); }); 16 | 17 | builder.Services.AddAuthentication(o => { o.DefaultScheme = "Cookies"; }) 18 | .AddCookie("Cookies", o => 19 | { 20 | o.Cookie.Name = ".myapp"; 21 | o.Cookie.Domain = "localhost"; 22 | o.Cookie.SameSite = SameSiteMode.Lax; 23 | o.DataProtectionProvider = DataProtectionProvider.Create("yarp-test"); 24 | }); 25 | 26 | var app = builder.Build(); 27 | 28 | app.UseForwardedHeaders(); 29 | app.Use((context, next) => 30 | { 31 | if (context.Request.Headers.TryGetValue("X-Forwarded-Prefix", out var pathBase)) 32 | { 33 | context.Request.PathBase = pathBase.ToString(); 34 | } 35 | 36 | return next(); 37 | }); 38 | 39 | app.UseAuthentication(); 40 | app.UseAuthorization(); 41 | 42 | app.MapGet("/", (ClaimsPrincipal user, HttpContext ctx) => 43 | new 44 | { 45 | name = user.GetName(), 46 | claims = user.Claims.Select(x => new { name = x.Type, value = x.Value }), 47 | host = ctx.Request.Host 48 | }) 49 | .RequireAuthorization("auth"); 50 | 51 | app.Run(); -------------------------------------------------------------------------------- /Identity/Pages/Ciba/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @using Identity.Pages.Ciba 2 | @model ScopeViewModel 3 | 4 |
  • 5 | 25 | @if (Model.Required) 26 | { 27 | 28 | (required) 29 | 30 | } 31 | @if (Model.Description != null) 32 | { 33 | 36 | } 37 | @if (Model.Resources?.Any() == true) 38 | { 39 | 48 | } 49 |
  • -------------------------------------------------------------------------------- /Identity/Pages/Consent/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @using Identity.Pages.Consent 2 | @model ScopeViewModel 3 | 4 |
  • 5 | 25 | @if (Model.Required) 26 | { 27 | 28 | (required) 29 | 30 | } 31 | @if (Model.Description != null) 32 | { 33 | 36 | } 37 | @if (Model.Resources?.Any() == true) 38 | { 39 | 48 | } 49 |
  • -------------------------------------------------------------------------------- /Proxy/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ReverseProxy": { 10 | "Routes": { 11 | "app" : { 12 | "ClusterId": "app", 13 | "AuthorizationPolicy": "auth", 14 | "Match": { 15 | "Hosts": [ "localhost" ], 16 | "Path" : "/app/{**remainder}" 17 | }, 18 | "Transforms" : [ 19 | { "PathRemovePrefix": "/app" }, 20 | { "X-Forwarded": "Set"}, 21 | { 22 | "RequestHeader": "X-Forwarded-Prefix", 23 | "Set": "/app" 24 | } 25 | ] 26 | }, 27 | "identity" : { 28 | "ClusterId" : "identity", 29 | "Match" : { 30 | "Hosts" : [ "localhost" ], 31 | "Path" : "/auth/{**remainder}" 32 | }, 33 | "Transforms" : [ 34 | { "PathRemovePrefix": "/auth" }, 35 | { "X-Forwarded": "Set"}, 36 | { 37 | "RequestHeader": "X-Forwarded-Prefix", 38 | "Set": "/auth" 39 | } 40 | ] 41 | } 42 | }, 43 | "Clusters": { 44 | "app": { 45 | "Destinations": { 46 | "app/destination": { 47 | "Address": "https://localhost:7008/" 48 | } 49 | } 50 | }, 51 | "identity": { 52 | "Destinations": { 53 | "identity/destination": { 54 | "Address": "https://localhost:5001/" 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Identity/Pages/ExternalLogin/Challenge.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Services; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace Identity.Pages.ExternalLogin; 8 | 9 | [AllowAnonymous] 10 | [SecurityHeaders] 11 | public class Challenge : PageModel 12 | { 13 | private readonly IIdentityServerInteractionService _interactionService; 14 | 15 | public Challenge(IIdentityServerInteractionService interactionService) 16 | { 17 | _interactionService = interactionService; 18 | } 19 | 20 | public IActionResult OnGet(string scheme, string returnUrl) 21 | { 22 | if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/"; 23 | 24 | // validate returnUrl - either it is a valid OIDC URL or back to a local page 25 | if (Url.IsLocalUrl(returnUrl) == false && _interactionService.IsValidReturnUrl(returnUrl) == false) 26 | { 27 | // user might have clicked on a malicious link - should be logged 28 | throw new Exception("invalid return URL"); 29 | } 30 | 31 | // start challenge and roundtrip the return URL and scheme 32 | var props = new AuthenticationProperties 33 | { 34 | RedirectUri = Url.Page("/externallogin/callback"), 35 | 36 | Items = 37 | { 38 | { "returnUrl", returnUrl }, 39 | { "scheme", scheme }, 40 | } 41 | }; 42 | 43 | return Challenge(props, scheme); 44 | } 45 | } -------------------------------------------------------------------------------- /Proxy/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Duende.IdentityServer.Extensions; 3 | using Microsoft.AspNetCore.DataProtection; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | builder.Services.AddReverseProxy() 8 | .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); 9 | 10 | builder.Services.AddAuthorization(o => 11 | { 12 | o.AddPolicy("auth", p => p.RequireAuthenticatedUser()); 13 | }); 14 | 15 | builder.Services.AddAuthentication(o => 16 | { 17 | o.DefaultScheme = "Cookies"; 18 | o.DefaultChallengeScheme = "oidc"; 19 | }) 20 | .AddCookie("Cookies", o => 21 | { 22 | o.Cookie.Name = ".myapp"; 23 | o.Cookie.Domain = "localhost"; 24 | o.Cookie.SameSite = SameSiteMode.Lax; 25 | o.DataProtectionProvider = DataProtectionProvider.Create("yarp-test"); 26 | }) 27 | .AddOpenIdConnect("oidc", o => 28 | { 29 | // proxy url 30 | o.Authority = "https://localhost:7291/auth"; 31 | 32 | o.ClientId = "interactive"; 33 | // copied from Identity project 34 | o.ClientSecret = "49C1A7E1-0C79-4A89-A3D6-A37998FB86B0"; 35 | o.ResponseType = "code"; 36 | 37 | o.SaveTokens = true; 38 | 39 | o.Scope.Clear(); 40 | o.Scope.Add("openid"); 41 | o.Scope.Add("profile"); 42 | o.GetClaimsFromUserInfoEndpoint = true; 43 | }); 44 | 45 | var app = builder.Build(); 46 | 47 | app.UseAuthentication(); 48 | app.UseAuthorization(); 49 | 50 | 51 | app.MapGet("/", () => "Hi, try going to /auth or /app"); 52 | app.MapGet("/proxy/has-user", (ClaimsPrincipal user) => user.GetName()) 53 | .RequireAuthorization(); 54 | 55 | app.MapReverseProxy(); 56 | 57 | 58 | 59 | app.Run(); -------------------------------------------------------------------------------- /Identity/Pages/Extensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | using Duende.IdentityServer.Models; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | 10 | namespace Identity.Pages; 11 | 12 | public static class Extensions 13 | { 14 | /// 15 | /// Determines if the authentication scheme support signout. 16 | /// 17 | public static async Task GetSchemeSupportsSignOutAsync(this HttpContext context, string scheme) 18 | { 19 | var provider = context.RequestServices.GetRequiredService(); 20 | var handler = await provider.GetHandlerAsync(context, scheme); 21 | return (handler is IAuthenticationSignOutHandler); 22 | } 23 | 24 | /// 25 | /// Checks if the redirect URI is for a native client. 26 | /// 27 | public static bool IsNativeClient(this AuthorizationRequest context) 28 | { 29 | return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal) 30 | && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal); 31 | } 32 | 33 | /// 34 | /// Renders a loading page that is used to redirect back to the redirectUri. 35 | /// 36 | public static IActionResult LoadingPage(this PageModel page, string redirectUri) 37 | { 38 | page.HttpContext.Response.StatusCode = 200; 39 | page.HttpContext.Response.Headers["Location"] = ""; 40 | 41 | return page.RedirectToPage("/Redirect/Index", new { RedirectUri = redirectUri }); 42 | } 43 | } -------------------------------------------------------------------------------- /Flowing.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proxy", "Proxy\Proxy.csproj", "{F2D790D6-2F0D-4B59-BD06-ACC6A16AC72F}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend", "Backend\Backend.csproj", "{7A2D11A7-7F6C-4434-B1BA-C5BC3D806BF1}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity", "Identity\Identity.csproj", "{5B3B19BF-AF4B-49EE-897C-1BB5934084F8}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {F2D790D6-2F0D-4B59-BD06-ACC6A16AC72F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {F2D790D6-2F0D-4B59-BD06-ACC6A16AC72F}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {F2D790D6-2F0D-4B59-BD06-ACC6A16AC72F}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {F2D790D6-2F0D-4B59-BD06-ACC6A16AC72F}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {7A2D11A7-7F6C-4434-B1BA-C5BC3D806BF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {7A2D11A7-7F6C-4434-B1BA-C5BC3D806BF1}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {7A2D11A7-7F6C-4434-B1BA-C5BC3D806BF1}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {7A2D11A7-7F6C-4434-B1BA-C5BC3D806BF1}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {5B3B19BF-AF4B-49EE-897C-1BB5934084F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {5B3B19BF-AF4B-49EE-897C-1BB5934084F8}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {5B3B19BF-AF4B-49EE-897C-1BB5934084F8}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {5B3B19BF-AF4B-49EE-897C-1BB5934084F8}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /Identity/Config.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Models; 2 | 3 | namespace Identity; 4 | 5 | public static class Config 6 | { 7 | public static IEnumerable IdentityResources => 8 | new IdentityResource[] 9 | { 10 | new IdentityResources.OpenId(), 11 | new IdentityResources.Profile(), 12 | }; 13 | 14 | public static IEnumerable ApiScopes => 15 | new ApiScope[] 16 | { 17 | new ApiScope("scope1"), 18 | new ApiScope("scope2"), 19 | }; 20 | 21 | public static IEnumerable Clients => 22 | new Client[] 23 | { 24 | // m2m client credentials flow client 25 | new Client 26 | { 27 | ClientId = "m2m.client", 28 | ClientName = "Client Credentials Client", 29 | 30 | AllowedGrantTypes = GrantTypes.ClientCredentials, 31 | ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) }, 32 | 33 | AllowedScopes = { "scope1" } 34 | }, 35 | 36 | // interactive client using code flow + pkce 37 | new Client 38 | { 39 | ClientId = "interactive", 40 | ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) }, 41 | 42 | AllowedGrantTypes = GrantTypes.Code, 43 | 44 | RedirectUris = { "https://localhost:7291/signin-oidc" }, 45 | FrontChannelLogoutUri = "https://localhost:7291/signout-oidc", 46 | PostLogoutRedirectUris = { "https://localhost:7291/signout-callback-oidc" }, 47 | 48 | AllowOfflineAccess = true, 49 | AllowedScopes = { "openid", "profile", "scope2" } 50 | }, 51 | }; 52 | } -------------------------------------------------------------------------------- /Identity/Pages/ServerSideSessions/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Models; 2 | using Duende.IdentityServer.Services; 3 | using Duende.IdentityServer.Stores; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace Identity.Pages.ServerSideSessions 8 | { 9 | public class IndexModel : PageModel 10 | { 11 | private readonly ISessionManagementService _sessionManagementService; 12 | 13 | public IndexModel(ISessionManagementService sessionManagementService = null) 14 | { 15 | _sessionManagementService = sessionManagementService; 16 | } 17 | 18 | public QueryResult UserSessions { get; set; } 19 | 20 | [BindProperty(SupportsGet = true)] public string Filter { get; set; } 21 | 22 | [BindProperty(SupportsGet = true)] public string Token { get; set; } 23 | 24 | [BindProperty(SupportsGet = true)] public string Prev { get; set; } 25 | 26 | public async Task OnGet() 27 | { 28 | if (_sessionManagementService != null) 29 | { 30 | UserSessions = await _sessionManagementService.QuerySessionsAsync(new SessionQuery 31 | { 32 | ResultsToken = Token, 33 | RequestPriorResults = Prev == "true", 34 | DisplayName = Filter, 35 | SessionId = Filter, 36 | SubjectId = Filter, 37 | }); 38 | } 39 | } 40 | 41 | [BindProperty] public string SessionId { get; set; } 42 | 43 | public async Task OnPost() 44 | { 45 | await _sessionManagementService.RemoveSessionsAsync(new RemoveSessionsContext 46 | { 47 | SessionId = SessionId, 48 | }); 49 | return RedirectToPage("/ServerSideSessions/Index", new { Token, Filter, Prev }); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /Identity/Pages/Ciba/All.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Ciba.AllModel 3 | @{ 4 | } 5 | 6 |
    7 |
    8 |
    9 |
    10 |
    11 |

    Pending Backchannel Login Requests

    12 |
    13 |
    14 | @if (Model.Logins?.Any() == true) 15 | { 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @foreach (var login in Model.Logins) 27 | { 28 | 29 | 30 | 31 | 32 | 35 | 36 | } 37 | 38 |
    IdClient IdBinding Message
    @login.InternalId@login.Client.ClientId@login.BindingMessage 33 | Process 34 |
    39 | } 40 | else 41 | { 42 |
    No Pending Login Requests
    43 | } 44 |
    45 |
    46 |
    47 |
    48 |
    -------------------------------------------------------------------------------- /Identity/wwwroot/lib/jquery/README.md: -------------------------------------------------------------------------------- 1 | # jQuery 2 | 3 | > jQuery is a fast, small, and feature-rich JavaScript library. 4 | 5 | For information on how to get started and how to use jQuery, please see [jQuery's documentation](https://api.jquery.com/). 6 | For source files and issues, please visit the [jQuery repo](https://github.com/jquery/jquery). 7 | 8 | If upgrading, please see the [blog post for 3.5.1](https://blog.jquery.com/2020/05/04/jquery-3-5-1-released-fixing-a-regression/). This includes notable differences from the previous version and a more readable changelog. 9 | 10 | ## Including jQuery 11 | 12 | Below are some of the most common ways to include jQuery. 13 | 14 | ### Browser 15 | 16 | #### Script tag 17 | 18 | ```html 19 | 20 | ``` 21 | 22 | #### Babel 23 | 24 | [Babel](https://babeljs.io/) is a next generation JavaScript compiler. One of the features is the ability to use ES6/ES2015 modules now, even though browsers do not yet support this feature natively. 25 | 26 | ```js 27 | import $ from "jquery"; 28 | ``` 29 | 30 | #### Browserify/Webpack 31 | 32 | There are several ways to use [Browserify](http://browserify.org/) and [Webpack](https://webpack.github.io/). For more information on using these tools, please refer to the corresponding project's documentation. In the script, including jQuery will usually look like this... 33 | 34 | ```js 35 | var $ = require( "jquery" ); 36 | ``` 37 | 38 | #### AMD (Asynchronous Module Definition) 39 | 40 | AMD is a module format built for the browser. For more information, we recommend [require.js' documentation](https://requirejs.org/docs/whyamd.html). 41 | 42 | ```js 43 | define( [ "jquery" ], function( $ ) { 44 | 45 | } ); 46 | ``` 47 | 48 | ### Node 49 | 50 | To include jQuery in [Node](https://nodejs.org/), first install with npm. 51 | 52 | ```sh 53 | npm install jquery 54 | ``` 55 | 56 | For jQuery to work in Node, a window with a document is required. Since no such window exists natively in Node, one can be mocked by tools such as [jsdom](https://github.com/jsdom/jsdom). This can be useful for testing purposes. 57 | 58 | ```js 59 | const { JSDOM } = require( "jsdom" ); 60 | const { window } = new JSDOM( "" ); 61 | const $ = require( "jquery" )( window ); 62 | ``` 63 | -------------------------------------------------------------------------------- /Identity/Pages/Diagnostics/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Diagnostics.Index 3 | 4 |
    5 |
    6 |

    Authentication Cookie

    7 |
    8 | 9 |
    10 |
    11 |
    12 |
    13 |

    Claims

    14 |
    15 |
    16 |
    17 | @foreach (var claim in Model.View.AuthenticateResult.Principal.Claims) 18 | { 19 |
    @claim.Type
    20 |
    @claim.Value
    21 | } 22 |
    23 |
    24 |
    25 |
    26 | 27 |
    28 |
    29 |
    30 |

    Properties

    31 |
    32 |
    33 |
    34 | @foreach (var prop in Model.View.AuthenticateResult.Properties.Items) 35 | { 36 |
    @prop.Key
    37 |
    @prop.Value
    38 | } 39 | @if (Model.View.Clients.Any()) 40 | { 41 |
    Clients
    42 |
    43 | @{ 44 | var clients = Model.View.Clients.ToArray(); 45 | for (var i = 0; i < clients.Length; i++) 46 | { 47 | @clients[i] 48 | if (i < clients.Length - 1) 49 | { 50 | , 51 | } 52 | } 53 | } 54 |
    55 | } 56 |
    57 |
    58 |
    59 |
    60 |
    61 |
    -------------------------------------------------------------------------------- /Identity/keys/is-signing-key-10F4A01466079A821AA98468F5C832D6.json: -------------------------------------------------------------------------------- 1 | {"Version":1,"Id":"10F4A01466079A821AA98468F5C832D6","Created":"2022-06-27T17:26:00.724659Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8KKL3kkFM1xNtyumRkiW9n3s84P1dZVUfKXtL7QPQ6yk3jPTvpFp61fnOUqt-uFeCqbmZ1kjzAnyg72j9a5eiNIMMKjf-Q20NoLrYUfaOyGfsCFKj75sl0jWoQoDfH660mOVQMmFgKZ9XWWtBAmTg9oWUucx1a0mMHnIoLylBvhBxsZVw1Myk_Xu85_QV46rVwXGrSHq81A5BcGREBh7veqm0n5egQxm2E7ER-K6GMSBPFzNjSRPNY4NH8SBzwoXKMkixOTJ512Q3yJTEBesfKDn4AbDMEggPfQYW8lZXz0c4qrqoKIJgYPltTR3lom70ux4DFJTOgFO4fATBlIph_gIEgKyPmJMQWjJEWKbWtZJ8H5EHm6nqq75kTwVAT9SgPxy83mlPC70pzOLeBPWCMbneN0t9RTYfCG0v6yJvvyT8IfqM9ZhR2OQ_fe64KzF4ZEEzhCFQK8ft8xjvnouIlrU480RF-ZxVGvfXVjW26g8BLPi-sHH4BOoWrsgmhsInsQmAcQ9pzD4MSyT5KCFNATslpC-pr2PHPHrb2RskGOvyU6rfraTvFUcwIN2IaOfGemgmLbml1GYPLgvs_st-1o3HhWR1K3CkoyzFTfmJ4npjXD-km7q-w7gg5d658DhuTTS4DY_4Qa0E-HPKdpLpjfI6gfIm7kGxfdulEU60AJcqxIcFLrtim_Lyn469iGjeoX9c5Cdw_1LFh5kQLdQLcl8_WiAj45O9Azwy9kKsF0_dJax_Zm6RvdvTTcnc2_KLNnK-DTzVmICyg6aPziBWQG8SPlmAv06Hm9MImJ39A63zGvRJc5nkdgyU4-89tUoAs628lH_rQiD-eqbiWfnYqYfRTA8pqSwEtf4J4NYsKFXa2hnwjoeqFtsQ759g4NS3AQtRXur83uOwLoPcq_ND1IGVznGspEor-j6nqFHsRLptL5llnA81WCfVVbwx1xaH-QlMKcPOpWhCOgIsWnBvynKlon1N6xstkP77-rYVH1kSoZmZW41Ev02fjse_KXo64-fsagGmwYH6V5UNUZRRyGSkV5ePmgrHI24BHTUqTtO3MBZP9dO4CXCXIQ44msiR6_4CCTPp8Rh9heacAhocQSlG_WiIko7wDpgB4Ox5KBcb6jxpvVA2IxjBjtopBO18aCwMwYJQ57to0qFzMciJ_vAAE52gS8LC89zbarU4uZJQUd5As9o5fSUfApUtBaz7kz1Ugta3wFY6WtNE_4VL8dp8__FP0Eqp3rwRGsninFO4PMdKreJr93jVBd1ztVfZa8p9gxKOKF7HkyDz4Q-V3ExbgV2e7cvEeBSjYgq_pTb9aQ-7AkgRT6w27i6WdW6BTr4rEakxnhWA0gT37dBOIZ2fPsIwi5mtymqceSLGMYVAAq_p2h2RB-tgtgXBaqToE4VWV6eakNLB5r54VCWbcQHnFTaSPMeNASg7WglE9hXB1FZnh26NKLQ_KHtVaHqMph-oka-jiEy6kgZ8egfmag16ap7RR8qRVUBytpi-dRgh5B3iopUNpfOi0bvUEui4kutcDdDhnARQVw84MNeNA-RKx6mt9j2_aA8y1x3aZz2tFy0OElTbe0oVertYyLnl6lmhw_G7bQ9hsegKg4AqtkPxLFGYx__FwRYr_twN--Cz-19DsfCgVYIekI6RLI2m0g0ue7jBGDGHCuPPRXIT7AT7mpTRzGNTgJzUsUZub4E5n3xxb4WeAZg2YOSXcpiERJ_plF8tJQJ85Hujl9BnOvPQ_JnHTEqtQLJz9QZKKmxyP8UHN_i0-aQkSTvtRKytyuHDQC4GnZfuErtzI-c2v8_Ka_cMBFvo6P_nh7SioVjWzUjuuWxLQ2xAM1A48OR9qnX4er7R3JmY8oN7jwpwf-MY_IgAOCthCCiYLPzDsFG8nQ_a_QB5tTAqsNPbUqPVVyf_cnGRzSNVr8AebmsCfkhpU-oIQ16UbOiTqmtfA3BGhsbArnEoaaw8INTUAlwyU4bAbc9417fr0LflsKCftZcaRhuH2kYoLyUDH-kscRwb_lGSMMpxcdH-iKHc6oS-BkSXkg11sAZyufFQJceHnXIu2NUibDaZVvBUD6l5Id8aa_aQhAwLMUGtCcKEmYKMtwKtUKvEipxsamdH-xgtGK9KkeV8rLV3PQP6l1LLSQOx6_tZ7cszviDO4DFt090JZGkgwavp2QHp4nH-xd5Qh4vIPen8jKhl8OXXMo_E62MMzlezGxNaTIGJMjp-lrnUZ6vfNHHkTBagLN1L1kKV8i5NI6Pw0F1zc8IU6NPJkXmepOWbsRMhsdZOQMVsjAz1dcQYUNzMYqIOgNjQ4T9OL372f_rNvkpbCOgZEj7f-WT37vpqIpTWCvhpEdyFu5rSE2OW3JvzVex83MB73RixQNtvPuHg3dTdhS9g1tohrKjjAtfSswMZqlGm-xfOLu13Eb5I2nk5qzAyvO5DskB4RVaig_y14KB2ido-NH9Z2vI","DataProtected":true} -------------------------------------------------------------------------------- /Identity/Pages/TestUsers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityModel; 6 | using System.Security.Claims; 7 | using System.Text.Json; 8 | using Duende.IdentityServer; 9 | using Duende.IdentityServer.Test; 10 | 11 | namespace Identity; 12 | 13 | public class TestUsers 14 | { 15 | public static List Users 16 | { 17 | get 18 | { 19 | var address = new 20 | { 21 | street_address = "One Hacker Way", 22 | locality = "Heidelberg", 23 | postal_code = 69118, 24 | country = "Germany" 25 | }; 26 | 27 | return new List 28 | { 29 | new TestUser 30 | { 31 | SubjectId = "1", 32 | Username = "alice", 33 | Password = "alice", 34 | Claims = 35 | { 36 | new Claim(JwtClaimTypes.Name, "Alice Smith"), 37 | new Claim(JwtClaimTypes.GivenName, "Alice"), 38 | new Claim(JwtClaimTypes.FamilyName, "Smith"), 39 | new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), 40 | new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 41 | new Claim(JwtClaimTypes.WebSite, "http://alice.com"), 42 | new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), 43 | IdentityServerConstants.ClaimValueTypes.Json) 44 | } 45 | }, 46 | new TestUser 47 | { 48 | SubjectId = "2", 49 | Username = "bob", 50 | Password = "bob", 51 | Claims = 52 | { 53 | new Claim(JwtClaimTypes.Name, "Bob Smith"), 54 | new Claim(JwtClaimTypes.GivenName, "Bob"), 55 | new Claim(JwtClaimTypes.FamilyName, "Smith"), 56 | new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), 57 | new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 58 | new Claim(JwtClaimTypes.WebSite, "http://bob.com"), 59 | new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), 60 | IdentityServerConstants.ClaimValueTypes.Json) 61 | } 62 | } 63 | }; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Identity/Pages/SecurityHeadersAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace Identity.Pages; 9 | 10 | public class SecurityHeadersAttribute : ActionFilterAttribute 11 | { 12 | public override void OnResultExecuting(ResultExecutingContext context) 13 | { 14 | var result = context.Result; 15 | if (result is PageResult) 16 | { 17 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 18 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options")) 19 | { 20 | context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff"); 21 | } 22 | 23 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 24 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options")) 25 | { 26 | context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); 27 | } 28 | 29 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 30 | var csp = 31 | "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';"; 32 | // also consider adding upgrade-insecure-requests once you have HTTPS in place for production 33 | //csp += "upgrade-insecure-requests;"; 34 | // also an example if you need client images to be displayed from twitter 35 | // csp += "img-src 'self' https://pbs.twimg.com;"; 36 | 37 | // once for standards compliant browsers 38 | if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy")) 39 | { 40 | context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp); 41 | } 42 | 43 | // and once again for IE 44 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy")) 45 | { 46 | context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp); 47 | } 48 | 49 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy 50 | var referrer_policy = "no-referrer"; 51 | if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy")) 52 | { 53 | context.HttpContext.Response.Headers.Add("Referrer-Policy", referrer_policy); 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Identity/Pages/Grants/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Duende.IdentityServer.Events; 3 | using Duende.IdentityServer.Extensions; 4 | using Duende.IdentityServer.Services; 5 | using Duende.IdentityServer.Stores; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | 10 | namespace Identity.Pages.Grants; 11 | 12 | [SecurityHeaders] 13 | [Authorize] 14 | public class Index : PageModel 15 | { 16 | private readonly IIdentityServerInteractionService _interaction; 17 | private readonly IClientStore _clients; 18 | private readonly IResourceStore _resources; 19 | private readonly IEventService _events; 20 | 21 | public Index(IIdentityServerInteractionService interaction, 22 | IClientStore clients, 23 | IResourceStore resources, 24 | IEventService events) 25 | { 26 | _interaction = interaction; 27 | _clients = clients; 28 | _resources = resources; 29 | _events = events; 30 | } 31 | 32 | public ViewModel View { get; set; } 33 | 34 | public async Task OnGet() 35 | { 36 | var grants = await _interaction.GetAllUserGrantsAsync(); 37 | 38 | var list = new List(); 39 | foreach (var grant in grants) 40 | { 41 | var client = await _clients.FindClientByIdAsync(grant.ClientId); 42 | if (client != null) 43 | { 44 | var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes); 45 | 46 | var item = new GrantViewModel() 47 | { 48 | ClientId = client.ClientId, 49 | ClientName = client.ClientName ?? client.ClientId, 50 | ClientLogoUrl = client.LogoUri, 51 | ClientUrl = client.ClientUri, 52 | Description = grant.Description, 53 | Created = grant.CreationTime, 54 | Expires = grant.Expiration, 55 | IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(), 56 | ApiGrantNames = resources.ApiScopes.Select(x => x.DisplayName ?? x.Name).ToArray() 57 | }; 58 | 59 | list.Add(item); 60 | } 61 | } 62 | 63 | View = new ViewModel 64 | { 65 | Grants = list 66 | }; 67 | } 68 | 69 | [BindProperty] [Required] public string ClientId { get; set; } 70 | 71 | public async Task OnPost() 72 | { 73 | await _interaction.RevokeUserConsentAsync(ClientId); 74 | await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), ClientId)); 75 | 76 | return RedirectToPage("/Grants/Index"); 77 | } 78 | } -------------------------------------------------------------------------------- /Identity/Pages/Grants/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Grants.Index 3 | @{ 4 | } 5 | 6 |
    7 |
    8 |

    Client Application Permissions

    9 |

    Below is the list of applications you have given permission to and the resources they have access to.

    10 |
    11 | 12 | @if (Model.View.Grants.Any() == false) 13 | { 14 |
    15 |
    16 |
    17 | You have not given access to any applications 18 |
    19 |
    20 |
    21 | } 22 | else 23 | { 24 | foreach (var grant in Model.View.Grants) 25 | { 26 |
    27 |
    28 |
    29 |
    30 | @if (grant.ClientLogoUrl != null) 31 | { 32 | 33 | } 34 | @grant.ClientName 35 |
    36 | 37 |
    38 |
    39 | 40 | 41 |
    42 |
    43 |
    44 |
    45 | 46 |
      47 | @if (grant.Description != null) 48 | { 49 |
    • 50 | @grant.Description 51 |
    • 52 | } 53 |
    • 54 | @grant.Created.ToString("yyyy-MM-dd") 55 |
    • 56 | @if (grant.Expires.HasValue) 57 | { 58 |
    • 59 | @grant.Expires.Value.ToString("yyyy-MM-dd") 60 |
    • 61 | } 62 | @if (grant.IdentityGrantNames.Any()) 63 | { 64 |
    • 65 | 66 |
        67 | @foreach (var name in grant.IdentityGrantNames) 68 | { 69 |
      • @name
      • 70 | } 71 |
      72 |
    • 73 | } 74 | @if (grant.ApiGrantNames.Any()) 75 | { 76 |
    • 77 | 78 |
        79 | @foreach (var name in grant.ApiGrantNames) 80 | { 81 |
      • @name
      • 82 | } 83 |
      84 |
    • 85 | } 86 |
    87 |
    88 | } 89 | } 90 |
    -------------------------------------------------------------------------------- /Identity/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/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:not(:focus-visible){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([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}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;-ms-overflow-style:scrollbar}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;text-align:-webkit-match-parent}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}[role=button]{cursor:pointer}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}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 */ -------------------------------------------------------------------------------- /Identity/Pages/Account/Login/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Login.Index 3 | 4 | -------------------------------------------------------------------------------- /Identity/Pages/Account/Logout/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Events; 2 | using Duende.IdentityServer.Extensions; 3 | using Duende.IdentityServer.Services; 4 | using IdentityModel; 5 | using Microsoft.AspNetCore.Authentication; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.RazorPages; 10 | 11 | namespace Identity.Pages.Logout; 12 | 13 | [SecurityHeaders] 14 | [AllowAnonymous] 15 | public class Index : PageModel 16 | { 17 | private readonly IIdentityServerInteractionService _interaction; 18 | private readonly IEventService _events; 19 | 20 | [BindProperty] public string LogoutId { get; set; } 21 | 22 | public Index(IIdentityServerInteractionService interaction, IEventService events) 23 | { 24 | _interaction = interaction; 25 | _events = events; 26 | } 27 | 28 | public async Task OnGet(string logoutId) 29 | { 30 | LogoutId = logoutId; 31 | 32 | var showLogoutPrompt = LogoutOptions.ShowLogoutPrompt; 33 | 34 | if (User?.Identity.IsAuthenticated != true) 35 | { 36 | // if the user is not authenticated, then just show logged out page 37 | showLogoutPrompt = false; 38 | } 39 | else 40 | { 41 | var context = await _interaction.GetLogoutContextAsync(LogoutId); 42 | if (context?.ShowSignoutPrompt == false) 43 | { 44 | // it's safe to automatically sign-out 45 | showLogoutPrompt = false; 46 | } 47 | } 48 | 49 | if (showLogoutPrompt == false) 50 | { 51 | // if the request for logout was properly authenticated from IdentityServer, then 52 | // we don't need to show the prompt and can just log the user out directly. 53 | return await OnPost(); 54 | } 55 | 56 | return Page(); 57 | } 58 | 59 | public async Task OnPost() 60 | { 61 | if (User?.Identity.IsAuthenticated == true) 62 | { 63 | // if there's no current logout context, we need to create one 64 | // this captures necessary info from the current logged in user 65 | // this can still return null if there is no context needed 66 | LogoutId ??= await _interaction.CreateLogoutContextAsync(); 67 | 68 | // delete local authentication cookie 69 | await HttpContext.SignOutAsync(); 70 | 71 | // remove other cookies 72 | await HttpContext.SignOutAsync("Cookies"); 73 | 74 | // raise the logout event 75 | await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); 76 | 77 | // see if we need to trigger federated logout 78 | var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; 79 | 80 | // if it's a local login we can ignore this workflow 81 | if (idp != null && idp != Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider) 82 | { 83 | // we need to see if the provider supports external logout 84 | if (await HttpContext.GetSchemeSupportsSignOutAsync(idp)) 85 | { 86 | // build a return URL so the upstream provider will redirect back 87 | // to us after the user has logged out. this allows us to then 88 | // complete our single sign-out processing. 89 | string url = Url.Page("/Account/Logout/Loggedout", new { logoutId = LogoutId }); 90 | 91 | // this triggers a redirect to the external provider for sign-out 92 | return SignOut(new AuthenticationProperties { RedirectUri = url }, idp); 93 | } 94 | } 95 | } 96 | 97 | return RedirectToPage("/Account/Logout/LoggedOut", new { logoutId = LogoutId }); 98 | } 99 | } -------------------------------------------------------------------------------- /Identity/HostingExtensions.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer; 2 | using Identity; 3 | using Microsoft.AspNetCore.DataProtection; 4 | using Microsoft.AspNetCore.HttpOverrides; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | using Serilog; 7 | 8 | namespace Identity; 9 | 10 | internal static class HostingExtensions 11 | { 12 | public static WebApplication ConfigureServices(this WebApplicationBuilder builder) 13 | { 14 | builder.Services.AddRazorPages(); 15 | builder.Services.Configure(options => 16 | { 17 | options.ForwardedHeaders = ForwardedHeaders.All; 18 | 19 | options.KnownNetworks.Clear(); 20 | options.KnownProxies.Clear(); 21 | }); 22 | 23 | var isBuilder = builder.Services.AddIdentityServer(options => 24 | { 25 | options.Events.RaiseErrorEvents = true; 26 | options.Events.RaiseInformationEvents = true; 27 | options.Events.RaiseFailureEvents = true; 28 | options.Events.RaiseSuccessEvents = true; 29 | 30 | // see https://docs.duendesoftware.com/identityserver/v6/fundamentals/resources/ 31 | options.EmitStaticAudienceClaim = true; 32 | }) 33 | .AddTestUsers(TestUsers.Users); 34 | 35 | // in-memory, code config 36 | isBuilder.AddInMemoryIdentityResources(Config.IdentityResources); 37 | isBuilder.AddInMemoryApiScopes(Config.ApiScopes); 38 | isBuilder.AddInMemoryClients(Config.Clients); 39 | 40 | // if you want to use server-side sessions: https://blog.duendesoftware.com/posts/20220406_session_management/ 41 | // then enable it 42 | // isBuilder.AddServerSideSessions(); 43 | // 44 | // and put some authorization on the admin/management pages 45 | //builder.Services.AddAuthorization(options => 46 | // options.AddPolicy("admin", 47 | // policy => policy.RequireClaim("sub", "1")) 48 | // ); 49 | //builder.Services.Configure(options => 50 | // options.Conventions.AuthorizeFolder("/ServerSideSessions", "admin")); 51 | 52 | 53 | builder.Services 54 | .AddAuthentication() 55 | .AddCookie("Cookies", o => 56 | { 57 | o.Cookie.Path = "/"; 58 | o.Cookie.Name = ".myapp"; 59 | o.Cookie.Domain = "localhost"; 60 | o.Cookie.SameSite = SameSiteMode.Lax; 61 | o.DataProtectionProvider = DataProtectionProvider.Create("yarp-test"); 62 | }) 63 | .AddGoogle(options => 64 | { 65 | options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; 66 | 67 | // register your IdentityServer with Google at https://console.developers.google.com 68 | // enable the Google+ API 69 | // set the redirect URI to https://localhost:5001/signin-google 70 | options.ClientId = "copy client ID from Google here"; 71 | options.ClientSecret = "copy client secret from Google here"; 72 | }); 73 | 74 | return builder.Build(); 75 | } 76 | 77 | public static WebApplication ConfigurePipeline(this WebApplication app) 78 | { 79 | app.UseSerilogRequestLogging(); 80 | app.UseForwardedHeaders(); 81 | app.Use((context, next) => 82 | { 83 | if (context.Request.Headers.TryGetValue("X-Forwarded-Prefix", out var pathBase)) 84 | { 85 | context.Request.PathBase = pathBase.ToString(); 86 | } 87 | return next(); 88 | }); 89 | 90 | 91 | if (app.Environment.IsDevelopment()) 92 | { 93 | app.UseDeveloperExceptionPage(); 94 | } 95 | 96 | app.UseStaticFiles(); 97 | app.UseRouting(); 98 | app.UseIdentityServer(); 99 | app.UseAuthorization(); 100 | 101 | 102 | app.MapRazorPages() 103 | .RequireAuthorization(); 104 | 105 | return app; 106 | } 107 | } -------------------------------------------------------------------------------- /Identity/Pages/Ciba/Consent.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Ciba.Consent 3 | @{ 4 | } 5 | 6 | -------------------------------------------------------------------------------- /Identity/Pages/Consent/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Consent.Index 3 | @{ 4 | } 5 | 6 | -------------------------------------------------------------------------------- /Identity/Pages/Device/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.Device.Index 3 | @{ 4 | } 5 | 6 | @if (Model.Input.UserCode == null) 7 | { 8 | @*We need to collect the user code*@ 9 |
    10 |
    11 |

    User Code

    12 |

    Please enter the code displayed on your device.

    13 |
    14 | 15 |
    16 |
    17 | 18 |
    19 |
    20 | 21 |
    22 |
    23 |
    24 |
    25 | 26 | 27 |
    28 | 29 | 30 |
    31 |
    32 |
    33 |
    34 | } 35 | else 36 | { 37 | @*collect consent for the user code provided*@ 38 |
    39 |
    40 | @if (Model.View.ClientLogoUrl != null) 41 | { 42 | 45 | } 46 |

    47 | @Model.View.ClientName 48 | is requesting your permission 49 |

    50 |

    Please confirm that the authorization request matches the code: @Model.Input.UserCode.

    51 |

    Uncheck the permissions you do not wish to grant.

    52 |
    53 | 54 |
    55 |
    56 | 57 |
    58 |
    59 | 60 |
    61 | 62 |
    63 |
    64 | @if (Model.View.IdentityScopes.Any()) 65 | { 66 |
    67 |
    68 |
    69 | 70 | Personal Information 71 |
    72 |
      73 | @foreach (var scope in Model.View.IdentityScopes) 74 | { 75 | 76 | } 77 |
    78 |
    79 |
    80 | } 81 | 82 | @if (Model.View.ApiScopes.Any()) 83 | { 84 |
    85 |
    86 |
    87 | 88 | Application Access 89 |
    90 |
      91 | @foreach (var scope in Model.View.ApiScopes) 92 | { 93 | 94 | } 95 |
    96 |
    97 |
    98 | } 99 | 100 |
    101 |
    102 |
    103 | 104 | Description 105 |
    106 |
    107 | 108 |
    109 |
    110 |
    111 | 112 | @if (Model.View.AllowRememberConsent) 113 | { 114 |
    115 |
    116 | 117 | 120 |
    121 |
    122 | } 123 |
    124 |
    125 | 126 |
    127 |
    128 | 129 | 130 |
    131 |
    132 | @if (Model.View.ClientUrl != null) 133 | { 134 | 135 | 136 | @Model.View.ClientName 137 | 138 | } 139 |
    140 |
    141 |
    142 |
    143 | } -------------------------------------------------------------------------------- /Identity/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | 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"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus:not(:focus-visible) { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]):not([class]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):not([class]):hover { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | pre, 147 | code, 148 | kbd, 149 | samp { 150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 151 | font-size: 1em; 152 | } 153 | 154 | pre { 155 | margin-top: 0; 156 | margin-bottom: 1rem; 157 | overflow: auto; 158 | -ms-overflow-style: scrollbar; 159 | } 160 | 161 | figure { 162 | margin: 0 0 1rem; 163 | } 164 | 165 | img { 166 | vertical-align: middle; 167 | border-style: none; 168 | } 169 | 170 | svg { 171 | overflow: hidden; 172 | vertical-align: middle; 173 | } 174 | 175 | table { 176 | border-collapse: collapse; 177 | } 178 | 179 | caption { 180 | padding-top: 0.75rem; 181 | padding-bottom: 0.75rem; 182 | color: #6c757d; 183 | text-align: left; 184 | caption-side: bottom; 185 | } 186 | 187 | th { 188 | text-align: inherit; 189 | text-align: -webkit-match-parent; 190 | } 191 | 192 | label { 193 | display: inline-block; 194 | margin-bottom: 0.5rem; 195 | } 196 | 197 | button { 198 | border-radius: 0; 199 | } 200 | 201 | button:focus { 202 | outline: 1px dotted; 203 | outline: 5px auto -webkit-focus-ring-color; 204 | } 205 | 206 | input, 207 | button, 208 | select, 209 | optgroup, 210 | textarea { 211 | margin: 0; 212 | font-family: inherit; 213 | font-size: inherit; 214 | line-height: inherit; 215 | } 216 | 217 | button, 218 | input { 219 | overflow: visible; 220 | } 221 | 222 | button, 223 | select { 224 | text-transform: none; 225 | } 226 | 227 | [role="button"] { 228 | cursor: pointer; 229 | } 230 | 231 | select { 232 | word-wrap: normal; 233 | } 234 | 235 | button, 236 | [type="button"], 237 | [type="reset"], 238 | [type="submit"] { 239 | -webkit-appearance: button; 240 | } 241 | 242 | button:not(:disabled), 243 | [type="button"]:not(:disabled), 244 | [type="reset"]:not(:disabled), 245 | [type="submit"]:not(:disabled) { 246 | cursor: pointer; 247 | } 248 | 249 | button::-moz-focus-inner, 250 | [type="button"]::-moz-focus-inner, 251 | [type="reset"]::-moz-focus-inner, 252 | [type="submit"]::-moz-focus-inner { 253 | padding: 0; 254 | border-style: none; 255 | } 256 | 257 | input[type="radio"], 258 | input[type="checkbox"] { 259 | box-sizing: border-box; 260 | padding: 0; 261 | } 262 | 263 | textarea { 264 | overflow: auto; 265 | resize: vertical; 266 | } 267 | 268 | fieldset { 269 | min-width: 0; 270 | padding: 0; 271 | margin: 0; 272 | border: 0; 273 | } 274 | 275 | legend { 276 | display: block; 277 | width: 100%; 278 | max-width: 100%; 279 | padding: 0; 280 | margin-bottom: .5rem; 281 | font-size: 1.5rem; 282 | line-height: inherit; 283 | color: inherit; 284 | white-space: normal; 285 | } 286 | 287 | progress { 288 | vertical-align: baseline; 289 | } 290 | 291 | [type="number"]::-webkit-inner-spin-button, 292 | [type="number"]::-webkit-outer-spin-button { 293 | height: auto; 294 | } 295 | 296 | [type="search"] { 297 | outline-offset: -2px; 298 | -webkit-appearance: none; 299 | } 300 | 301 | [type="search"]::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | ::-webkit-file-upload-button { 306 | font: inherit; 307 | -webkit-appearance: button; 308 | } 309 | 310 | output { 311 | display: inline-block; 312 | } 313 | 314 | summary { 315 | display: list-item; 316 | cursor: pointer; 317 | } 318 | 319 | template { 320 | display: none; 321 | } 322 | 323 | [hidden] { 324 | display: none !important; 325 | } 326 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /Identity/Pages/ExternalLogin/Callback.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Duende.IdentityServer; 3 | using Duende.IdentityServer.Events; 4 | using Duende.IdentityServer.Services; 5 | using Duende.IdentityServer.Test; 6 | using IdentityModel; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.AspNetCore.Mvc.RazorPages; 11 | 12 | namespace Identity.Pages.ExternalLogin; 13 | 14 | [AllowAnonymous] 15 | [SecurityHeaders] 16 | public class Callback : PageModel 17 | { 18 | private readonly TestUserStore _users; 19 | private readonly IIdentityServerInteractionService _interaction; 20 | private readonly ILogger _logger; 21 | private readonly IEventService _events; 22 | 23 | public Callback( 24 | IIdentityServerInteractionService interaction, 25 | IEventService events, 26 | ILogger logger, 27 | TestUserStore users = null) 28 | { 29 | // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) 30 | _users = users ?? 31 | throw new Exception( 32 | "Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController."); 33 | 34 | _interaction = interaction; 35 | _logger = logger; 36 | _events = events; 37 | } 38 | 39 | public async Task OnGet() 40 | { 41 | // read external identity from the temporary cookie 42 | var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); 43 | if (result?.Succeeded != true) 44 | { 45 | throw new Exception("External authentication error"); 46 | } 47 | 48 | var externalUser = result.Principal; 49 | 50 | if (_logger.IsEnabled(LogLevel.Debug)) 51 | { 52 | var externalClaims = externalUser.Claims.Select(c => $"{c.Type}: {c.Value}"); 53 | _logger.LogDebug("External claims: {@claims}", externalClaims); 54 | } 55 | 56 | // lookup our user and external provider info 57 | // try to determine the unique id of the external user (issued by the provider) 58 | // the most common claim type for that are the sub claim and the NameIdentifier 59 | // depending on the external provider, some other claim type might be used 60 | var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ?? 61 | externalUser.FindFirst(ClaimTypes.NameIdentifier) ?? 62 | throw new Exception("Unknown userid"); 63 | 64 | var provider = result.Properties.Items["scheme"]; 65 | var providerUserId = userIdClaim.Value; 66 | 67 | // find external user 68 | var user = _users.FindByExternalProvider(provider, providerUserId); 69 | if (user == null) 70 | { 71 | // this might be where you might initiate a custom workflow for user registration 72 | // in this sample we don't show how that would be done, as our sample implementation 73 | // simply auto-provisions new external user 74 | // 75 | // remove the user id claim so we don't include it as an extra claim if/when we provision the user 76 | var claims = externalUser.Claims.ToList(); 77 | claims.Remove(userIdClaim); 78 | user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); 79 | } 80 | 81 | // this allows us to collect any additional claims or properties 82 | // for the specific protocols used and store them in the local auth cookie. 83 | // this is typically used to store data needed for signout from those protocols. 84 | var additionalLocalClaims = new List(); 85 | var localSignInProps = new AuthenticationProperties(); 86 | CaptureExternalLoginContext(result, additionalLocalClaims, localSignInProps); 87 | 88 | // issue authentication cookie for user 89 | var isuser = new IdentityServerUser(user.SubjectId) 90 | { 91 | DisplayName = user.Username, 92 | IdentityProvider = provider, 93 | AdditionalClaims = additionalLocalClaims 94 | }; 95 | 96 | await HttpContext.SignInAsync(isuser, localSignInProps); 97 | 98 | // delete temporary cookie used during external authentication 99 | await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); 100 | 101 | // retrieve return URL 102 | var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; 103 | 104 | // check if external login is in the context of an OIDC request 105 | var context = await _interaction.GetAuthorizationContextAsync(returnUrl); 106 | await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, 107 | true, context?.Client.ClientId)); 108 | 109 | if (context != null) 110 | { 111 | if (context.IsNativeClient()) 112 | { 113 | // The client is native, so this change in how to 114 | // return the response is for better UX for the end user. 115 | return this.LoadingPage(returnUrl); 116 | } 117 | } 118 | 119 | return Redirect(returnUrl); 120 | } 121 | 122 | // if the external login is OIDC-based, there are certain things we need to preserve to make logout work 123 | // this will be different for WS-Fed, SAML2p or other protocols 124 | private void CaptureExternalLoginContext(AuthenticateResult externalResult, List localClaims, 125 | AuthenticationProperties localSignInProps) 126 | { 127 | // if the external system sent a session id claim, copy it over 128 | // so we can use it for single sign-out 129 | var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); 130 | if (sid != null) 131 | { 132 | localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); 133 | } 134 | 135 | // if the external provider issued an id_token, we'll keep it for signout 136 | var idToken = externalResult.Properties.GetTokenValue("id_token"); 137 | if (idToken != null) 138 | { 139 | localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } }); 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /Identity/Pages/ServerSideSessions/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Identity.Pages.ServerSideSessions.IndexModel 3 | @{ 4 | } 5 | 6 |
    7 |
    8 |
    9 |
    10 |
    11 |

    User Sessions

    12 |
    13 | 14 |
    15 | 16 | @if (Model.UserSessions != null) 17 | { 18 |
    19 |
    20 | @if (Model.UserSessions.HasPrevResults) 21 | { 22 | Prev 23 | } 24 |
    25 |
    26 |
    27 | 28 | 29 | 30 |
    31 |
    32 |
    33 | @if (Model.UserSessions.HasNextResults) 34 | { 35 | Next 36 | } 37 |
    38 |
    39 | 40 | @if (Model.UserSessions.TotalCount.HasValue) 41 | { 42 |
    43 | @if (Model.UserSessions.CurrentPage.HasValue && Model.UserSessions.TotalPages.HasValue) 44 | { 45 | 46 | Total Results: @Model.UserSessions.TotalCount, 47 | Page @Model.UserSessions.CurrentPage of @Model.UserSessions.TotalPages 48 | 49 | } 50 | else 51 | { 52 | 53 | Total Results: @Model.UserSessions.TotalCount 54 | 55 | } 56 |
    57 | } 58 | 59 |
    60 | 61 | @if (Model.UserSessions.Results.Any()) 62 | { 63 |
    64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | @foreach (var session in Model.UserSessions.Results) 77 | { 78 | 79 | 80 | 81 | 82 | 83 | 84 | 90 | 91 | 92 | 103 | 104 | } 105 | 106 |
    Subject IdSession IdDisplay NameCreatedExpires
    @session.SubjectId@session.SessionId@session.DisplayName@session.Created@session.Expires 85 |
    86 | 87 | 88 |
    89 |
    93 | Clients: 94 | @if (session.ClientIds?.Any() == true) 95 | { 96 | @(session.ClientIds.Aggregate((x, y) => $"{x}, {y}")) 97 | } 98 | else 99 | { 100 | @("None") 101 | } 102 |
    107 |
    108 | } 109 | else 110 | { 111 |
    No User Sessions
    112 | } 113 | } 114 | else 115 | { 116 |
    117 |
    118 | You do not have server-side sessions enabled. 119 | To do so, use AddServerSideSessions on your IdentityServer configuration. 120 | See the documentation for more information. 121 |
    122 |
    123 | } 124 |
    125 |
    126 |
    127 |
    128 |
    -------------------------------------------------------------------------------- /Identity/Pages/Device/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Configuration; 2 | using Duende.IdentityServer.Events; 3 | using Duende.IdentityServer.Extensions; 4 | using Duende.IdentityServer.Models; 5 | using Duende.IdentityServer.Services; 6 | using Duende.IdentityServer.Validation; 7 | using Identity.Pages.Consent; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.AspNetCore.Mvc.RazorPages; 11 | using Microsoft.Extensions.Options; 12 | 13 | namespace Identity.Pages.Device; 14 | 15 | [SecurityHeaders] 16 | [Authorize] 17 | public class Index : PageModel 18 | { 19 | private readonly IDeviceFlowInteractionService _interaction; 20 | private readonly IEventService _events; 21 | private readonly IOptions _options; 22 | private readonly ILogger _logger; 23 | 24 | public Index( 25 | IDeviceFlowInteractionService interaction, 26 | IEventService eventService, 27 | IOptions options, 28 | ILogger logger) 29 | { 30 | _interaction = interaction; 31 | _events = eventService; 32 | _options = options; 33 | _logger = logger; 34 | } 35 | 36 | public ViewModel View { get; set; } 37 | 38 | [BindProperty] public InputModel Input { get; set; } 39 | 40 | public async Task OnGet(string userCode) 41 | { 42 | if (String.IsNullOrWhiteSpace(userCode)) 43 | { 44 | View = new ViewModel(); 45 | Input = new InputModel(); 46 | return Page(); 47 | } 48 | 49 | View = await BuildViewModelAsync(userCode); 50 | if (View == null) 51 | { 52 | ModelState.AddModelError("", DeviceOptions.InvalidUserCode); 53 | View = new ViewModel(); 54 | Input = new InputModel(); 55 | return Page(); 56 | } 57 | 58 | Input = new InputModel 59 | { 60 | UserCode = userCode, 61 | }; 62 | 63 | return Page(); 64 | } 65 | 66 | public async Task OnPost() 67 | { 68 | var request = await _interaction.GetAuthorizationContextAsync(Input.UserCode); 69 | if (request == null) return RedirectToPage("/Home/Error/Index"); 70 | 71 | ConsentResponse grantedConsent = null; 72 | 73 | // user clicked 'no' - send back the standard 'access_denied' response 74 | if (Input.Button == "no") 75 | { 76 | grantedConsent = new ConsentResponse 77 | { 78 | Error = AuthorizationError.AccessDenied 79 | }; 80 | 81 | // emit event 82 | await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, 83 | request.ValidatedResources.RawScopeValues)); 84 | } 85 | // user clicked 'yes' - validate the data 86 | else if (Input.Button == "yes") 87 | { 88 | // if the user consented to some scope, build the response model 89 | if (Input.ScopesConsented != null && Input.ScopesConsented.Any()) 90 | { 91 | var scopes = Input.ScopesConsented; 92 | if (ConsentOptions.EnableOfflineAccess == false) 93 | { 94 | scopes = scopes.Where(x => 95 | x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess); 96 | } 97 | 98 | grantedConsent = new ConsentResponse 99 | { 100 | RememberConsent = Input.RememberConsent, 101 | ScopesValuesConsented = scopes.ToArray(), 102 | Description = Input.Description 103 | }; 104 | 105 | // emit event 106 | await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, 107 | request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, 108 | grantedConsent.RememberConsent)); 109 | } 110 | else 111 | { 112 | ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage); 113 | } 114 | } 115 | else 116 | { 117 | ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage); 118 | } 119 | 120 | if (grantedConsent != null) 121 | { 122 | // communicate outcome of consent back to identityserver 123 | await _interaction.HandleRequestAsync(Input.UserCode, grantedConsent); 124 | 125 | // indicate that's it ok to redirect back to authorization endpoint 126 | return RedirectToPage("/Device/Success"); 127 | } 128 | 129 | // we need to redisplay the consent UI 130 | View = await BuildViewModelAsync(Input.UserCode, Input); 131 | return Page(); 132 | } 133 | 134 | 135 | private async Task BuildViewModelAsync(string userCode, InputModel model = null) 136 | { 137 | var request = await _interaction.GetAuthorizationContextAsync(userCode); 138 | if (request != null) 139 | { 140 | return CreateConsentViewModel(model, request); 141 | } 142 | 143 | return null; 144 | } 145 | 146 | private ViewModel CreateConsentViewModel(InputModel model, DeviceFlowAuthorizationRequest request) 147 | { 148 | var vm = new ViewModel 149 | { 150 | ClientName = request.Client.ClientName ?? request.Client.ClientId, 151 | ClientUrl = request.Client.ClientUri, 152 | ClientLogoUrl = request.Client.LogoUri, 153 | AllowRememberConsent = request.Client.AllowRememberConsent 154 | }; 155 | 156 | vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => 157 | CreateScopeViewModel(x, model == null || model.ScopesConsented?.Contains(x.Name) == true)).ToArray(); 158 | 159 | var apiScopes = new List(); 160 | foreach (var parsedScope in request.ValidatedResources.ParsedScopes) 161 | { 162 | var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); 163 | if (apiScope != null) 164 | { 165 | var scopeVm = CreateScopeViewModel(parsedScope, apiScope, 166 | model == null || model.ScopesConsented?.Contains(parsedScope.RawValue) == true); 167 | apiScopes.Add(scopeVm); 168 | } 169 | } 170 | 171 | if (DeviceOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess) 172 | { 173 | apiScopes.Add(GetOfflineAccessScope(model == null || 174 | model.ScopesConsented?.Contains(Duende.IdentityServer 175 | .IdentityServerConstants.StandardScopes.OfflineAccess) == true)); 176 | } 177 | 178 | vm.ApiScopes = apiScopes; 179 | 180 | return vm; 181 | } 182 | 183 | private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) 184 | { 185 | return new ScopeViewModel 186 | { 187 | Value = identity.Name, 188 | DisplayName = identity.DisplayName ?? identity.Name, 189 | Description = identity.Description, 190 | Emphasize = identity.Emphasize, 191 | Required = identity.Required, 192 | Checked = check || identity.Required 193 | }; 194 | } 195 | 196 | public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) 197 | { 198 | return new ScopeViewModel 199 | { 200 | Value = parsedScopeValue.RawValue, 201 | // todo: use the parsed scope value in the display? 202 | DisplayName = apiScope.DisplayName ?? apiScope.Name, 203 | Description = apiScope.Description, 204 | Emphasize = apiScope.Emphasize, 205 | Required = apiScope.Required, 206 | Checked = check || apiScope.Required 207 | }; 208 | } 209 | 210 | private ScopeViewModel GetOfflineAccessScope(bool check) 211 | { 212 | return new ScopeViewModel 213 | { 214 | Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess, 215 | DisplayName = DeviceOptions.OfflineAccessDisplayName, 216 | Description = DeviceOptions.OfflineAccessDescription, 217 | Emphasize = true, 218 | Checked = check 219 | }; 220 | } 221 | } -------------------------------------------------------------------------------- /Identity/Pages/Ciba/Consent.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Events; 2 | using Duende.IdentityServer.Extensions; 3 | using Duende.IdentityServer.Models; 4 | using Duende.IdentityServer.Services; 5 | using Duende.IdentityServer.Validation; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | 10 | namespace Identity.Pages.Ciba; 11 | 12 | [Authorize] 13 | [SecurityHeadersAttribute] 14 | public class Consent : PageModel 15 | { 16 | private readonly IBackchannelAuthenticationInteractionService _interaction; 17 | private readonly IEventService _events; 18 | private readonly ILogger _logger; 19 | 20 | public Consent( 21 | IBackchannelAuthenticationInteractionService interaction, 22 | IEventService events, 23 | ILogger logger) 24 | { 25 | _interaction = interaction; 26 | _events = events; 27 | _logger = logger; 28 | } 29 | 30 | public ViewModel View { get; set; } 31 | 32 | [BindProperty] public InputModel Input { get; set; } 33 | 34 | public async Task OnGet(string id) 35 | { 36 | View = await BuildViewModelAsync(id); 37 | if (View == null) 38 | { 39 | return RedirectToPage("/Home/Error/Index"); 40 | } 41 | 42 | Input = new InputModel 43 | { 44 | Id = id 45 | }; 46 | 47 | return Page(); 48 | } 49 | 50 | public async Task OnPost() 51 | { 52 | // validate return url is still valid 53 | var request = await _interaction.GetLoginRequestByInternalIdAsync(Input.Id); 54 | if (request == null || request.Subject.GetSubjectId() != User.GetSubjectId()) 55 | { 56 | _logger.LogError("Invalid id {id}", Input.Id); 57 | return RedirectToPage("/Home/Error/Index"); 58 | } 59 | 60 | CompleteBackchannelLoginRequest result = null; 61 | 62 | // user clicked 'no' - send back the standard 'access_denied' response 63 | if (Input?.Button == "no") 64 | { 65 | result = new CompleteBackchannelLoginRequest(Input.Id); 66 | 67 | // emit event 68 | await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, 69 | request.ValidatedResources.RawScopeValues)); 70 | } 71 | // user clicked 'yes' - validate the data 72 | else if (Input?.Button == "yes") 73 | { 74 | // if the user consented to some scope, build the response model 75 | if (Input.ScopesConsented != null && Input.ScopesConsented.Any()) 76 | { 77 | var scopes = Input.ScopesConsented; 78 | if (ConsentOptions.EnableOfflineAccess == false) 79 | { 80 | scopes = scopes.Where(x => 81 | x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess); 82 | } 83 | 84 | result = new CompleteBackchannelLoginRequest(Input.Id) 85 | { 86 | ScopesValuesConsented = scopes.ToArray(), 87 | Description = Input.Description 88 | }; 89 | 90 | // emit event 91 | await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, 92 | request.ValidatedResources.RawScopeValues, result.ScopesValuesConsented, false)); 93 | } 94 | else 95 | { 96 | ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage); 97 | } 98 | } 99 | else 100 | { 101 | ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage); 102 | } 103 | 104 | if (result != null) 105 | { 106 | // communicate outcome of consent back to identityserver 107 | await _interaction.CompleteLoginRequestAsync(result); 108 | 109 | return RedirectToPage("/Ciba/All"); 110 | } 111 | 112 | // we need to redisplay the consent UI 113 | View = await BuildViewModelAsync(Input.Id, Input); 114 | return Page(); 115 | } 116 | 117 | private async Task BuildViewModelAsync(string id, InputModel model = null) 118 | { 119 | var request = await _interaction.GetLoginRequestByInternalIdAsync(id); 120 | if (request != null && request.Subject.GetSubjectId() == User.GetSubjectId()) 121 | { 122 | return CreateConsentViewModel(model, id, request); 123 | } 124 | else 125 | { 126 | _logger.LogError("No backchannel login request matching id: {id}", id); 127 | } 128 | 129 | return null; 130 | } 131 | 132 | private ViewModel CreateConsentViewModel( 133 | InputModel model, string id, 134 | BackchannelUserLoginRequest request) 135 | { 136 | var vm = new ViewModel 137 | { 138 | ClientName = request.Client.ClientName ?? request.Client.ClientId, 139 | ClientUrl = request.Client.ClientUri, 140 | ClientLogoUrl = request.Client.LogoUri, 141 | BindingMessage = request.BindingMessage 142 | }; 143 | 144 | vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources 145 | .Select(x => CreateScopeViewModel(x, 146 | model?.ScopesConsented == null || model.ScopesConsented?.Contains(x.Name) == true)) 147 | .ToArray(); 148 | 149 | var resourceIndicators = request.RequestedResourceIndicators ?? Enumerable.Empty(); 150 | var apiResources = 151 | request.ValidatedResources.Resources.ApiResources.Where(x => resourceIndicators.Contains(x.Name)); 152 | 153 | var apiScopes = new List(); 154 | foreach (var parsedScope in request.ValidatedResources.ParsedScopes) 155 | { 156 | var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); 157 | if (apiScope != null) 158 | { 159 | var scopeVm = CreateScopeViewModel(parsedScope, apiScope, 160 | model == null || model.ScopesConsented?.Contains(parsedScope.RawValue) == true); 161 | scopeVm.Resources = apiResources.Where(x => x.Scopes.Contains(parsedScope.ParsedName)) 162 | .Select(x => new ResourceViewModel 163 | { 164 | Name = x.Name, 165 | DisplayName = x.DisplayName ?? x.Name, 166 | }).ToArray(); 167 | apiScopes.Add(scopeVm); 168 | } 169 | } 170 | 171 | if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess) 172 | { 173 | apiScopes.Add(GetOfflineAccessScope(model == null || 174 | model.ScopesConsented?.Contains(Duende.IdentityServer 175 | .IdentityServerConstants.StandardScopes.OfflineAccess) == true)); 176 | } 177 | 178 | vm.ApiScopes = apiScopes; 179 | 180 | return vm; 181 | } 182 | 183 | private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) 184 | { 185 | return new ScopeViewModel 186 | { 187 | Name = identity.Name, 188 | Value = identity.Name, 189 | DisplayName = identity.DisplayName ?? identity.Name, 190 | Description = identity.Description, 191 | Emphasize = identity.Emphasize, 192 | Required = identity.Required, 193 | Checked = check || identity.Required 194 | }; 195 | } 196 | 197 | public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) 198 | { 199 | var displayName = apiScope.DisplayName ?? apiScope.Name; 200 | if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter)) 201 | { 202 | displayName += ":" + parsedScopeValue.ParsedParameter; 203 | } 204 | 205 | return new ScopeViewModel 206 | { 207 | Name = parsedScopeValue.ParsedName, 208 | Value = parsedScopeValue.RawValue, 209 | DisplayName = displayName, 210 | Description = apiScope.Description, 211 | Emphasize = apiScope.Emphasize, 212 | Required = apiScope.Required, 213 | Checked = check || apiScope.Required 214 | }; 215 | } 216 | 217 | private ScopeViewModel GetOfflineAccessScope(bool check) 218 | { 219 | return new ScopeViewModel 220 | { 221 | Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess, 222 | DisplayName = ConsentOptions.OfflineAccessDisplayName, 223 | Description = ConsentOptions.OfflineAccessDescription, 224 | Emphasize = true, 225 | Checked = check 226 | }; 227 | } 228 | } -------------------------------------------------------------------------------- /Identity/Pages/Account/Login/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer; 2 | using Duende.IdentityServer.Events; 3 | using Duende.IdentityServer.Models; 4 | using Duende.IdentityServer.Services; 5 | using Duende.IdentityServer.Stores; 6 | using Duende.IdentityServer.Test; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.AspNetCore.Mvc.RazorPages; 11 | 12 | namespace Identity.Pages.Login; 13 | 14 | [SecurityHeaders] 15 | [AllowAnonymous] 16 | public class Index : PageModel 17 | { 18 | private readonly TestUserStore _users; 19 | private readonly IIdentityServerInteractionService _interaction; 20 | private readonly IEventService _events; 21 | private readonly IAuthenticationSchemeProvider _schemeProvider; 22 | private readonly IIdentityProviderStore _identityProviderStore; 23 | 24 | public ViewModel View { get; set; } 25 | 26 | [BindProperty] public InputModel Input { get; set; } 27 | 28 | public Index( 29 | IIdentityServerInteractionService interaction, 30 | IAuthenticationSchemeProvider schemeProvider, 31 | IIdentityProviderStore identityProviderStore, 32 | IEventService events, 33 | TestUserStore users = null) 34 | { 35 | // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) 36 | _users = users ?? 37 | throw new Exception( 38 | "Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController."); 39 | 40 | _interaction = interaction; 41 | _schemeProvider = schemeProvider; 42 | _identityProviderStore = identityProviderStore; 43 | _events = events; 44 | } 45 | 46 | public async Task OnGet(string returnUrl) 47 | { 48 | await BuildModelAsync(returnUrl); 49 | 50 | if (View.IsExternalLoginOnly) 51 | { 52 | // we only have one option for logging in and it's an external provider 53 | return RedirectToPage("/ExternalLogin/Challenge", new { scheme = View.ExternalLoginScheme, returnUrl }); 54 | } 55 | 56 | return Page(); 57 | } 58 | 59 | public async Task OnPost() 60 | { 61 | // check if we are in the context of an authorization request 62 | var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl); 63 | 64 | // the user clicked the "cancel" button 65 | if (Input.Button != "login") 66 | { 67 | if (context != null) 68 | { 69 | // if the user cancels, send a result back into IdentityServer as if they 70 | // denied the consent (even if this client does not require consent). 71 | // this will send back an access denied OIDC error response to the client. 72 | await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied); 73 | 74 | // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null 75 | if (context.IsNativeClient()) 76 | { 77 | // The client is native, so this change in how to 78 | // return the response is for better UX for the end user. 79 | return this.LoadingPage(Input.ReturnUrl); 80 | } 81 | 82 | return Redirect(Input.ReturnUrl); 83 | } 84 | else 85 | { 86 | // since we don't have a valid context, then we just go back to the home page 87 | return Redirect("~/"); 88 | } 89 | } 90 | 91 | if (ModelState.IsValid) 92 | { 93 | // validate username/password against in-memory store 94 | if (_users.ValidateCredentials(Input.Username, Input.Password)) 95 | { 96 | var user = _users.FindByUsername(Input.Username); 97 | await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, 98 | clientId: context?.Client.ClientId)); 99 | 100 | // only set explicit expiration here if user chooses "remember me". 101 | // otherwise we rely upon expiration configured in cookie middleware. 102 | AuthenticationProperties props = null; 103 | if (LoginOptions.AllowRememberLogin && Input.RememberLogin) 104 | { 105 | props = new AuthenticationProperties 106 | { 107 | IsPersistent = true, 108 | ExpiresUtc = DateTimeOffset.UtcNow.Add(LoginOptions.RememberMeLoginDuration) 109 | }; 110 | } 111 | 112 | ; 113 | 114 | // issue authentication cookie with subject ID and username 115 | var isuser = new IdentityServerUser(user.SubjectId) 116 | { 117 | DisplayName = user.Username 118 | }; 119 | 120 | await HttpContext.SignInAsync(isuser, props); 121 | 122 | if (context != null) 123 | { 124 | if (context.IsNativeClient()) 125 | { 126 | // The client is native, so this change in how to 127 | // return the response is for better UX for the end user. 128 | return this.LoadingPage(Input.ReturnUrl); 129 | } 130 | 131 | // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null 132 | return Redirect(Input.ReturnUrl); 133 | } 134 | 135 | // request for a local page 136 | if (Url.IsLocalUrl(Input.ReturnUrl)) 137 | { 138 | return Redirect(Input.ReturnUrl); 139 | } 140 | else if (string.IsNullOrEmpty(Input.ReturnUrl)) 141 | { 142 | return Redirect("~/"); 143 | } 144 | else 145 | { 146 | // user might have clicked on a malicious link - should be logged 147 | throw new Exception("invalid return URL"); 148 | } 149 | } 150 | 151 | await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, "invalid credentials", 152 | clientId: context?.Client.ClientId)); 153 | ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage); 154 | } 155 | 156 | // something went wrong, show form with error 157 | await BuildModelAsync(Input.ReturnUrl); 158 | return Page(); 159 | } 160 | 161 | private async Task BuildModelAsync(string returnUrl) 162 | { 163 | Input = new InputModel 164 | { 165 | ReturnUrl = returnUrl 166 | }; 167 | 168 | var context = await _interaction.GetAuthorizationContextAsync(returnUrl); 169 | if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null) 170 | { 171 | var local = context.IdP == Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider; 172 | 173 | // this is meant to short circuit the UI and only trigger the one external IdP 174 | View = new ViewModel 175 | { 176 | EnableLocalLogin = local, 177 | }; 178 | 179 | Input.Username = context?.LoginHint; 180 | 181 | if (!local) 182 | { 183 | View.ExternalProviders = new[] 184 | { new ViewModel.ExternalProvider { AuthenticationScheme = context.IdP } }; 185 | } 186 | 187 | return; 188 | } 189 | 190 | var schemes = await _schemeProvider.GetAllSchemesAsync(); 191 | 192 | var providers = schemes 193 | .Where(x => x.DisplayName != null) 194 | .Select(x => new ViewModel.ExternalProvider 195 | { 196 | DisplayName = x.DisplayName ?? x.Name, 197 | AuthenticationScheme = x.Name 198 | }).ToList(); 199 | 200 | var dyanmicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync()) 201 | .Where(x => x.Enabled) 202 | .Select(x => new ViewModel.ExternalProvider 203 | { 204 | AuthenticationScheme = x.Scheme, 205 | DisplayName = x.DisplayName 206 | }); 207 | providers.AddRange(dyanmicSchemes); 208 | 209 | 210 | var allowLocal = true; 211 | var client = context?.Client; 212 | if (client != null) 213 | { 214 | allowLocal = client.EnableLocalLogin; 215 | if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) 216 | { 217 | providers = providers.Where(provider => 218 | client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); 219 | } 220 | } 221 | 222 | View = new ViewModel 223 | { 224 | AllowRememberLogin = LoginOptions.AllowRememberLogin, 225 | EnableLocalLogin = allowLocal && LoginOptions.AllowLocalLogin, 226 | ExternalProviders = providers.ToArray() 227 | }; 228 | } 229 | } -------------------------------------------------------------------------------- /Identity/Pages/Consent/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Events; 2 | using Duende.IdentityServer.Extensions; 3 | using Duende.IdentityServer.Models; 4 | using Duende.IdentityServer.Services; 5 | using Duende.IdentityServer.Validation; 6 | using IdentityModel; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.RazorPages; 10 | 11 | namespace Identity.Pages.Consent; 12 | 13 | [Authorize] 14 | [SecurityHeadersAttribute] 15 | public class Index : PageModel 16 | { 17 | private readonly IIdentityServerInteractionService _interaction; 18 | private readonly IEventService _events; 19 | private readonly ILogger _logger; 20 | 21 | public Index( 22 | IIdentityServerInteractionService interaction, 23 | IEventService events, 24 | ILogger logger) 25 | { 26 | _interaction = interaction; 27 | _events = events; 28 | _logger = logger; 29 | } 30 | 31 | public ViewModel View { get; set; } 32 | 33 | [BindProperty] public InputModel Input { get; set; } 34 | 35 | public async Task OnGet(string returnUrl) 36 | { 37 | View = await BuildViewModelAsync(returnUrl); 38 | if (View == null) 39 | { 40 | return RedirectToPage("/Home/Error/Index"); 41 | } 42 | 43 | Input = new InputModel 44 | { 45 | ReturnUrl = returnUrl, 46 | }; 47 | 48 | return Page(); 49 | } 50 | 51 | public async Task OnPost() 52 | { 53 | // validate return url is still valid 54 | var request = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl); 55 | if (request == null) return RedirectToPage("/Home/Error/Index"); 56 | 57 | ConsentResponse grantedConsent = null; 58 | 59 | // user clicked 'no' - send back the standard 'access_denied' response 60 | if (Input?.Button == "no") 61 | { 62 | grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; 63 | 64 | // emit event 65 | await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, 66 | request.ValidatedResources.RawScopeValues)); 67 | } 68 | // user clicked 'yes' - validate the data 69 | else if (Input?.Button == "yes") 70 | { 71 | // if the user consented to some scope, build the response model 72 | if (Input.ScopesConsented != null && Input.ScopesConsented.Any()) 73 | { 74 | var scopes = Input.ScopesConsented; 75 | if (ConsentOptions.EnableOfflineAccess == false) 76 | { 77 | scopes = scopes.Where(x => 78 | x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess); 79 | } 80 | 81 | grantedConsent = new ConsentResponse 82 | { 83 | RememberConsent = Input.RememberConsent, 84 | ScopesValuesConsented = scopes.ToArray(), 85 | Description = Input.Description 86 | }; 87 | 88 | // emit event 89 | await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, 90 | request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, 91 | grantedConsent.RememberConsent)); 92 | } 93 | else 94 | { 95 | ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage); 96 | } 97 | } 98 | else 99 | { 100 | ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage); 101 | } 102 | 103 | if (grantedConsent != null) 104 | { 105 | // communicate outcome of consent back to identityserver 106 | await _interaction.GrantConsentAsync(request, grantedConsent); 107 | 108 | // redirect back to authorization endpoint 109 | if (request.IsNativeClient() == true) 110 | { 111 | // The client is native, so this change in how to 112 | // return the response is for better UX for the end user. 113 | return this.LoadingPage(Input.ReturnUrl); 114 | } 115 | 116 | return Redirect(Input.ReturnUrl); 117 | } 118 | 119 | // we need to redisplay the consent UI 120 | View = await BuildViewModelAsync(Input.ReturnUrl, Input); 121 | return Page(); 122 | } 123 | 124 | private async Task BuildViewModelAsync(string returnUrl, InputModel model = null) 125 | { 126 | var request = await _interaction.GetAuthorizationContextAsync(returnUrl); 127 | if (request != null) 128 | { 129 | return CreateConsentViewModel(model, returnUrl, request); 130 | } 131 | else 132 | { 133 | _logger.LogError("No consent request matching request: {0}", returnUrl); 134 | } 135 | 136 | return null; 137 | } 138 | 139 | private ViewModel CreateConsentViewModel( 140 | InputModel model, string returnUrl, 141 | AuthorizationRequest request) 142 | { 143 | var vm = new ViewModel 144 | { 145 | ClientName = request.Client.ClientName ?? request.Client.ClientId, 146 | ClientUrl = request.Client.ClientUri, 147 | ClientLogoUrl = request.Client.LogoUri, 148 | AllowRememberConsent = request.Client.AllowRememberConsent 149 | }; 150 | 151 | vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources 152 | .Select(x => CreateScopeViewModel(x, 153 | model?.ScopesConsented == null || model.ScopesConsented?.Contains(x.Name) == true)) 154 | .ToArray(); 155 | 156 | var resourceIndicators = request.Parameters.GetValues(OidcConstants.AuthorizeRequest.Resource) ?? 157 | Enumerable.Empty(); 158 | var apiResources = 159 | request.ValidatedResources.Resources.ApiResources.Where(x => resourceIndicators.Contains(x.Name)); 160 | 161 | var apiScopes = new List(); 162 | foreach (var parsedScope in request.ValidatedResources.ParsedScopes) 163 | { 164 | var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); 165 | if (apiScope != null) 166 | { 167 | var scopeVm = CreateScopeViewModel(parsedScope, apiScope, 168 | model == null || model.ScopesConsented?.Contains(parsedScope.RawValue) == true); 169 | scopeVm.Resources = apiResources.Where(x => x.Scopes.Contains(parsedScope.ParsedName)) 170 | .Select(x => new ResourceViewModel 171 | { 172 | Name = x.Name, 173 | DisplayName = x.DisplayName ?? x.Name, 174 | }).ToArray(); 175 | apiScopes.Add(scopeVm); 176 | } 177 | } 178 | 179 | if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess) 180 | { 181 | apiScopes.Add(GetOfflineAccessScope(model == null || 182 | model.ScopesConsented?.Contains(Duende.IdentityServer 183 | .IdentityServerConstants.StandardScopes.OfflineAccess) == true)); 184 | } 185 | 186 | vm.ApiScopes = apiScopes; 187 | 188 | return vm; 189 | } 190 | 191 | private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) 192 | { 193 | return new ScopeViewModel 194 | { 195 | Name = identity.Name, 196 | Value = identity.Name, 197 | DisplayName = identity.DisplayName ?? identity.Name, 198 | Description = identity.Description, 199 | Emphasize = identity.Emphasize, 200 | Required = identity.Required, 201 | Checked = check || identity.Required 202 | }; 203 | } 204 | 205 | public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) 206 | { 207 | var displayName = apiScope.DisplayName ?? apiScope.Name; 208 | if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter)) 209 | { 210 | displayName += ":" + parsedScopeValue.ParsedParameter; 211 | } 212 | 213 | return new ScopeViewModel 214 | { 215 | Name = parsedScopeValue.ParsedName, 216 | Value = parsedScopeValue.RawValue, 217 | DisplayName = displayName, 218 | Description = apiScope.Description, 219 | Emphasize = apiScope.Emphasize, 220 | Required = apiScope.Required, 221 | Checked = check || apiScope.Required 222 | }; 223 | } 224 | 225 | private ScopeViewModel GetOfflineAccessScope(bool check) 226 | { 227 | return new ScopeViewModel 228 | { 229 | Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess, 230 | DisplayName = ConsentOptions.OfflineAccessDisplayName, 231 | Description = ConsentOptions.OfflineAccessDescription, 232 | Emphasize = true, 233 | Checked = check 234 | }; 235 | } 236 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | 403 | ## 404 | ## Visual studio for Mac 405 | ## 406 | 407 | 408 | # globs 409 | Makefile.in 410 | *.userprefs 411 | *.usertasks 412 | config.make 413 | config.status 414 | aclocal.m4 415 | install-sh 416 | autom4te.cache/ 417 | *.tar.gz 418 | tarballs/ 419 | test-results/ 420 | 421 | # Mac bundle stuff 422 | *.dmg 423 | *.app 424 | 425 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 426 | # General 427 | .DS_Store 428 | .AppleDouble 429 | .LSOverride 430 | 431 | # Icon must end with two \r 432 | Icon 433 | 434 | 435 | # Thumbnails 436 | ._* 437 | 438 | # Files that might appear in the root of a volume 439 | .DocumentRevisions-V100 440 | .fseventsd 441 | .Spotlight-V100 442 | .TemporaryItems 443 | .Trashes 444 | .VolumeIcon.icns 445 | .com.apple.timemachine.donotpresent 446 | 447 | # Directories potentially created on remote AFP share 448 | .AppleDB 449 | .AppleDesktop 450 | Network Trash Folder 451 | Temporary Items 452 | .apdisk 453 | 454 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 455 | # Windows thumbnail cache files 456 | Thumbs.db 457 | ehthumbs.db 458 | ehthumbs_vista.db 459 | 460 | # Dump file 461 | *.stackdump 462 | 463 | # Folder config file 464 | [Dd]esktop.ini 465 | 466 | # Recycle Bin used on file shares 467 | $RECYCLE.BIN/ 468 | 469 | # Windows Installer files 470 | *.cab 471 | *.msi 472 | *.msix 473 | *.msm 474 | *.msp 475 | 476 | # Windows shortcuts 477 | *.lnk 478 | -------------------------------------------------------------------------------- /Identity/wwwroot/lib/bootstrap4-glyphicons/css/bootstrap-glyphicons.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2018 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | @font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons/glyphicons-halflings-regular.eot?');src:url('../fonts/glyphicons/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons/glyphicons-halflings-regular.woff2') format('woff2'),url('../fonts/glyphicons/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"*"}.glyphicon-plus:before{content:"+"}.glyphicon-euro:before,.glyphicon-eur:before{content:"€"}.glyphicon-minus:before{content:"−"}.glyphicon-cloud:before{content:"☁"}.glyphicon-envelope:before{content:"✉"}.glyphicon-pencil:before{content:"✏"}.glyphicon-glass:before{content:""}.glyphicon-music:before{content:""}.glyphicon-search:before{content:""}.glyphicon-heart:before{content:""}.glyphicon-star:before{content:""}.glyphicon-star-empty:before{content:""}.glyphicon-user:before{content:""}.glyphicon-film:before{content:""}.glyphicon-th-large:before{content:""}.glyphicon-th:before{content:""}.glyphicon-th-list:before{content:""}.glyphicon-ok:before{content:""}.glyphicon-remove:before{content:""}.glyphicon-zoom-in:before{content:""}.glyphicon-zoom-out:before{content:""}.glyphicon-off:before{content:""}.glyphicon-signal:before{content:""}.glyphicon-cog:before{content:""}.glyphicon-trash:before{content:""}.glyphicon-home:before{content:""}.glyphicon-file:before{content:""}.glyphicon-time:before{content:""}.glyphicon-road:before{content:""}.glyphicon-download-alt:before{content:""}.glyphicon-download:before{content:""}.glyphicon-upload:before{content:""}.glyphicon-inbox:before{content:""}.glyphicon-play-circle:before{content:""}.glyphicon-repeat:before{content:""}.glyphicon-refresh:before{content:""}.glyphicon-list-alt:before{content:""}.glyphicon-lock:before{content:""}.glyphicon-flag:before{content:""}.glyphicon-headphones:before{content:""}.glyphicon-volume-off:before{content:""}.glyphicon-volume-down:before{content:""}.glyphicon-volume-up:before{content:""}.glyphicon-qrcode:before{content:""}.glyphicon-barcode:before{content:""}.glyphicon-tag:before{content:""}.glyphicon-tags:before{content:""}.glyphicon-book:before{content:""}.glyphicon-bookmark:before{content:""}.glyphicon-print:before{content:""}.glyphicon-camera:before{content:""}.glyphicon-font:before{content:""}.glyphicon-bold:before{content:""}.glyphicon-italic:before{content:""}.glyphicon-text-height:before{content:""}.glyphicon-text-width:before{content:""}.glyphicon-align-left:before{content:""}.glyphicon-align-center:before{content:""}.glyphicon-align-right:before{content:""}.glyphicon-align-justify:before{content:""}.glyphicon-list:before{content:""}.glyphicon-indent-left:before{content:""}.glyphicon-indent-right:before{content:""}.glyphicon-facetime-video:before{content:""}.glyphicon-picture:before{content:""}.glyphicon-map-marker:before{content:""}.glyphicon-adjust:before{content:""}.glyphicon-tint:before{content:""}.glyphicon-edit:before{content:""}.glyphicon-share:before{content:""}.glyphicon-check:before{content:""}.glyphicon-move:before{content:""}.glyphicon-step-backward:before{content:""}.glyphicon-fast-backward:before{content:""}.glyphicon-backward:before{content:""}.glyphicon-play:before{content:""}.glyphicon-pause:before{content:""}.glyphicon-stop:before{content:""}.glyphicon-forward:before{content:""}.glyphicon-fast-forward:before{content:""}.glyphicon-step-forward:before{content:""}.glyphicon-eject:before{content:""}.glyphicon-chevron-left:before{content:""}.glyphicon-chevron-right:before{content:""}.glyphicon-plus-sign:before{content:""}.glyphicon-minus-sign:before{content:""}.glyphicon-remove-sign:before{content:""}.glyphicon-ok-sign:before{content:""}.glyphicon-question-sign:before{content:""}.glyphicon-info-sign:before{content:""}.glyphicon-screenshot:before{content:""}.glyphicon-remove-circle:before{content:""}.glyphicon-ok-circle:before{content:""}.glyphicon-ban-circle:before{content:""}.glyphicon-arrow-left:before{content:""}.glyphicon-arrow-right:before{content:""}.glyphicon-arrow-up:before{content:""}.glyphicon-arrow-down:before{content:""}.glyphicon-share-alt:before{content:""}.glyphicon-resize-full:before{content:""}.glyphicon-resize-small:before{content:""}.glyphicon-exclamation-sign:before{content:""}.glyphicon-gift:before{content:""}.glyphicon-leaf:before{content:""}.glyphicon-fire:before{content:""}.glyphicon-eye-open:before{content:""}.glyphicon-eye-close:before{content:""}.glyphicon-warning-sign:before{content:""}.glyphicon-plane:before{content:""}.glyphicon-calendar:before{content:""}.glyphicon-random:before{content:""}.glyphicon-comment:before{content:""}.glyphicon-magnet:before{content:""}.glyphicon-chevron-up:before{content:""}.glyphicon-chevron-down:before{content:""}.glyphicon-retweet:before{content:""}.glyphicon-shopping-cart:before{content:""}.glyphicon-folder-close:before{content:""}.glyphicon-folder-open:before{content:""}.glyphicon-resize-vertical:before{content:""}.glyphicon-resize-horizontal:before{content:""}.glyphicon-hdd:before{content:""}.glyphicon-bullhorn:before{content:""}.glyphicon-bell:before{content:""}.glyphicon-certificate:before{content:""}.glyphicon-thumbs-up:before{content:""}.glyphicon-thumbs-down:before{content:""}.glyphicon-hand-right:before{content:""}.glyphicon-hand-left:before{content:""}.glyphicon-hand-up:before{content:""}.glyphicon-hand-down:before{content:""}.glyphicon-circle-arrow-right:before{content:""}.glyphicon-circle-arrow-left:before{content:""}.glyphicon-circle-arrow-up:before{content:""}.glyphicon-circle-arrow-down:before{content:""}.glyphicon-globe:before{content:""}.glyphicon-wrench:before{content:""}.glyphicon-tasks:before{content:""}.glyphicon-filter:before{content:""}.glyphicon-briefcase:before{content:""}.glyphicon-fullscreen:before{content:""}.glyphicon-dashboard:before{content:""}.glyphicon-paperclip:before{content:""}.glyphicon-heart-empty:before{content:""}.glyphicon-link:before{content:""}.glyphicon-phone:before{content:""}.glyphicon-pushpin:before{content:""}.glyphicon-usd:before{content:""}.glyphicon-gbp:before{content:""}.glyphicon-sort:before{content:""}.glyphicon-sort-by-alphabet:before{content:""}.glyphicon-sort-by-alphabet-alt:before{content:""}.glyphicon-sort-by-order:before{content:""}.glyphicon-sort-by-order-alt:before{content:""}.glyphicon-sort-by-attributes:before{content:""}.glyphicon-sort-by-attributes-alt:before{content:""}.glyphicon-unchecked:before{content:""}.glyphicon-expand:before{content:""}.glyphicon-collapse-down:before{content:""}.glyphicon-collapse-up:before{content:""}.glyphicon-log-in:before{content:""}.glyphicon-flash:before{content:""}.glyphicon-log-out:before{content:""}.glyphicon-new-window:before{content:""}.glyphicon-record:before{content:""}.glyphicon-save:before{content:""}.glyphicon-open:before{content:""}.glyphicon-saved:before{content:""}.glyphicon-import:before{content:""}.glyphicon-export:before{content:""}.glyphicon-send:before{content:""}.glyphicon-floppy-disk:before{content:""}.glyphicon-floppy-saved:before{content:""}.glyphicon-floppy-remove:before{content:""}.glyphicon-floppy-save:before{content:""}.glyphicon-floppy-open:before{content:""}.glyphicon-credit-card:before{content:""}.glyphicon-transfer:before{content:""}.glyphicon-cutlery:before{content:""}.glyphicon-header:before{content:""}.glyphicon-compressed:before{content:""}.glyphicon-earphone:before{content:""}.glyphicon-phone-alt:before{content:""}.glyphicon-tower:before{content:""}.glyphicon-stats:before{content:""}.glyphicon-sd-video:before{content:""}.glyphicon-hd-video:before{content:""}.glyphicon-subtitles:before{content:""}.glyphicon-sound-stereo:before{content:""}.glyphicon-sound-dolby:before{content:""}.glyphicon-sound-5-1:before{content:""}.glyphicon-sound-6-1:before{content:""}.glyphicon-sound-7-1:before{content:""}.glyphicon-copyright-mark:before{content:""}.glyphicon-registration-mark:before{content:""}.glyphicon-cloud-download:before{content:""}.glyphicon-cloud-upload:before{content:""}.glyphicon-tree-conifer:before{content:""}.glyphicon-tree-deciduous:before{content:""}.glyphicon-cd:before{content:""}.glyphicon-save-file:before{content:""}.glyphicon-open-file:before{content:""}.glyphicon-level-up:before{content:""}.glyphicon-copy:before{content:""}.glyphicon-paste:before{content:""}.glyphicon-alert:before{content:""}.glyphicon-equalizer:before{content:""}.glyphicon-king:before{content:""}.glyphicon-queen:before{content:""}.glyphicon-pawn:before{content:""}.glyphicon-bishop:before{content:""}.glyphicon-knight:before{content:""}.glyphicon-baby-formula:before{content:""}.glyphicon-tent:before{content:"⛺"}.glyphicon-blackboard:before{content:""}.glyphicon-bed:before{content:""}.glyphicon-apple:before{content:""}.glyphicon-erase:before{content:""}.glyphicon-hourglass:before{content:"⌛"}.glyphicon-lamp:before{content:""}.glyphicon-duplicate:before{content:""}.glyphicon-piggy-bank:before{content:""}.glyphicon-scissors:before{content:""}.glyphicon-bitcoin:before{content:""}.glyphicon-btc:before{content:""}.glyphicon-xbt:before{content:""}.glyphicon-yen:before{content:"¥"}.glyphicon-jpy:before{content:"¥"}.glyphicon-ruble:before{content:"₽"}.glyphicon-rub:before{content:"₽"}.glyphicon-scale:before{content:""}.glyphicon-ice-lolly:before{content:""}.glyphicon-ice-lolly-tasted:before{content:""}.glyphicon-education:before{content:""}.glyphicon-option-horizontal:before{content:""}.glyphicon-option-vertical:before{content:""}.glyphicon-menu-hamburger:before{content:""}.glyphicon-modal-window:before{content:""}.glyphicon-oil:before{content:""}.glyphicon-grain:before{content:""}.glyphicon-sunglasses:before{content:""}.glyphicon-text-size:before{content:""}.glyphicon-text-color:before{content:""}.glyphicon-text-background:before{content:""}.glyphicon-object-align-top:before{content:""}.glyphicon-object-align-bottom:before{content:""}.glyphicon-object-align-horizontal:before{content:""}.glyphicon-object-align-left:before{content:""}.glyphicon-object-align-vertical:before{content:""}.glyphicon-object-align-right:before{content:""}.glyphicon-triangle-right:before{content:""}.glyphicon-triangle-left:before{content:""}.glyphicon-triangle-bottom:before{content:""}.glyphicon-triangle-top:before{content:""}.glyphicon-console:before{content:""}.glyphicon-superscript:before{content:""}.glyphicon-subscript:before{content:""}.glyphicon-menu-left:before{content:""}.glyphicon-menu-right:before{content:""}.glyphicon-menu-down:before{content:""}.glyphicon-menu-up:before{content:""} -------------------------------------------------------------------------------- /Identity/wwwroot/lib/bootstrap4-glyphicons/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Identity/wwwroot/lib/bootstrap/README.md: -------------------------------------------------------------------------------- 1 |

    2 | 3 | Bootstrap logo 4 | 5 |

    6 | 7 |

    Bootstrap

    8 | 9 |

    10 | Sleek, intuitive, and powerful front-end framework for faster and easier web development. 11 |
    12 | Explore Bootstrap docs » 13 |
    14 |
    15 | Report bug 16 | · 17 | Request feature 18 | · 19 | Themes 20 | · 21 | Blog 22 |

    23 | 24 | 25 | ## Table of contents 26 | 27 | - [Quick start](#quick-start) 28 | - [Status](#status) 29 | - [What's included](#whats-included) 30 | - [Bugs and feature requests](#bugs-and-feature-requests) 31 | - [Documentation](#documentation) 32 | - [Contributing](#contributing) 33 | - [Community](#community) 34 | - [Versioning](#versioning) 35 | - [Creators](#creators) 36 | - [Thanks](#thanks) 37 | - [Copyright and license](#copyright-and-license) 38 | 39 | 40 | ## Quick start 41 | 42 | Several quick start options are available: 43 | 44 | - [Download the latest release.](https://github.com/twbs/bootstrap/archive/v4.5.3.zip) 45 | - Clone the repo: `git clone https://github.com/twbs/bootstrap.git` 46 | - Install with [npm](https://www.npmjs.com/): `npm install bootstrap` 47 | - Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap@4.5.3` 48 | - Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:4.5.3` 49 | - Install with [NuGet](https://www.nuget.org/): CSS: `Install-Package bootstrap` Sass: `Install-Package bootstrap.sass` 50 | 51 | Read the [Getting started page](https://getbootstrap.com/docs/4.5/getting-started/introduction/) for information on the framework contents, templates and examples, and more. 52 | 53 | 54 | ## Status 55 | 56 | [![Slack](https://bootstrap-slack.herokuapp.com/badge.svg)](https://bootstrap-slack.herokuapp.com/) 57 | [![Build Status](https://github.com/twbs/bootstrap/workflows/JS%20Tests/badge.svg?branch=v4-dev)](https://github.com/twbs/bootstrap/actions?query=workflow%3AJS+Tests+branch%3Av4-dev) 58 | [![npm version](https://img.shields.io/npm/v/bootstrap)](https://www.npmjs.com/package/bootstrap) 59 | [![Gem version](https://img.shields.io/gem/v/bootstrap)](https://rubygems.org/gems/bootstrap) 60 | [![Meteor Atmosphere](https://img.shields.io/badge/meteor-twbs%3Abootstrap-blue)](https://atmospherejs.com/twbs/bootstrap) 61 | [![Packagist Prerelease](https://img.shields.io/packagist/vpre/twbs/bootstrap)](https://packagist.org/packages/twbs/bootstrap) 62 | [![NuGet](https://img.shields.io/nuget/vpre/bootstrap)](https://www.nuget.org/packages/bootstrap/absoluteLatest) 63 | [![peerDependencies Status](https://img.shields.io/david/peer/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=peer) 64 | [![devDependency Status](https://img.shields.io/david/dev/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=dev) 65 | [![Coverage Status](https://img.shields.io/coveralls/github/twbs/bootstrap/v4-dev)](https://coveralls.io/github/twbs/bootstrap?branch=v4-dev) 66 | [![CSS gzip size](https://img.badgesize.io/twbs/bootstrap/v4-dev/dist/css/bootstrap.min.css?compression=gzip&label=CSS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/v4-dev/dist/css/bootstrap.min.css) 67 | [![JS gzip size](https://img.badgesize.io/twbs/bootstrap/v4-dev/dist/js/bootstrap.min.js?compression=gzip&label=JS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/v4-dev/dist/js/bootstrap.min.js) 68 | [![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229)](https://www.browserstack.com/automate/public-build/SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229) 69 | [![Backers on Open Collective](https://img.shields.io/opencollective/backers/bootstrap)](#backers) 70 | [![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/bootstrap)](#sponsors) 71 | 72 | 73 | ## What's included 74 | 75 | Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this: 76 | 77 | ```text 78 | bootstrap/ 79 | └── dist/ 80 | ├── css/ 81 | │ ├── bootstrap-grid.css 82 | │ ├── bootstrap-grid.css.map 83 | │ ├── bootstrap-grid.min.css 84 | │ ├── bootstrap-grid.min.css.map 85 | │ ├── bootstrap-reboot.css 86 | │ ├── bootstrap-reboot.css.map 87 | │ ├── bootstrap-reboot.min.css 88 | │ ├── bootstrap-reboot.min.css.map 89 | │ ├── bootstrap.css 90 | │ ├── bootstrap.css.map 91 | │ ├── bootstrap.min.css 92 | │ └── bootstrap.min.css.map 93 | └── js/ 94 | ├── bootstrap.bundle.js 95 | ├── bootstrap.bundle.js.map 96 | ├── bootstrap.bundle.min.js 97 | ├── bootstrap.bundle.min.js.map 98 | ├── bootstrap.js 99 | ├── bootstrap.js.map 100 | ├── bootstrap.min.js 101 | └── bootstrap.min.js.map 102 | ``` 103 | 104 | We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/), but not [jQuery](https://jquery.com/). 105 | 106 | 107 | ## Bugs and feature requests 108 | 109 | Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/twbs/bootstrap/blob/v4-dev/.github/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/twbs/bootstrap/issues/new). 110 | 111 | 112 | ## Documentation 113 | 114 | Bootstrap's documentation, included in this repo in the root directory, is built with [Jekyll](https://jekyllrb.com/) and publicly hosted on GitHub Pages at . The docs may also be run locally. 115 | 116 | Documentation search is powered by [Algolia's DocSearch](https://community.algolia.com/docsearch/). Working on our search? Be sure to set `debug: true` in `site/docs/4.5/assets/js/src/search.js` file. 117 | 118 | ### Running documentation locally 119 | 120 | 1. Run through the [tooling setup](https://getbootstrap.com/docs/4.5/getting-started/build-tools/#tooling-setup) to install Jekyll (the site builder) and other Ruby dependencies with `bundle install`. 121 | 2. Run `npm install` to install Node.js dependencies. 122 | 3. Run `npm start` to compile CSS and JavaScript files, generate our docs, and watch for changes. 123 | 4. Open `http://localhost:9001` in your browser, and voilà. 124 | 125 | Learn more about using Jekyll by reading its [documentation](https://jekyllrb.com/docs/). 126 | 127 | ### Documentation for previous releases 128 | 129 | You can find all our previous releases docs on . 130 | 131 | [Previous releases](https://github.com/twbs/bootstrap/releases) and their documentation are also available for download. 132 | 133 | 134 | ## Contributing 135 | 136 | Please read through our [contributing guidelines](https://github.com/twbs/bootstrap/blob/v4-dev/.github/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development. 137 | 138 | Moreover, if your pull request contains JavaScript patches or features, you must include [relevant unit tests](https://github.com/twbs/bootstrap/tree/v4-dev/js/tests). All HTML and CSS should conform to the [Code Guide](https://github.com/mdo/code-guide), maintained by [Mark Otto](https://github.com/mdo). 139 | 140 | Editor preferences are available in the [editor config](https://github.com/twbs/bootstrap/blob/v4-dev/.editorconfig) for easy use in common text editors. Read more and download plugins at . 141 | 142 | 143 | ## Community 144 | 145 | Get updates on Bootstrap's development and chat with the project maintainers and community members. 146 | 147 | - Follow [@getbootstrap on Twitter](https://twitter.com/getbootstrap). 148 | - Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.com/). 149 | - Join [the official Slack room](https://bootstrap-slack.herokuapp.com/). 150 | - Chat with fellow Bootstrappers in IRC. On the `irc.freenode.net` server, in the `##bootstrap` channel. 151 | - Implementation help may be found at Stack Overflow (tagged [`bootstrap-4`](https://stackoverflow.com/questions/tagged/bootstrap-4)). 152 | - Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/browse/keyword/bootstrap) or similar delivery mechanisms for maximum discoverability. 153 | 154 | 155 | ## Versioning 156 | 157 | For transparency into our release cycle and in striving to maintain backward compatibility, Bootstrap is maintained under [the Semantic Versioning guidelines](https://semver.org/). Sometimes we screw up, but we adhere to those rules whenever possible. 158 | 159 | See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release. 160 | 161 | 162 | ## Creators 163 | 164 | **Mark Otto** 165 | 166 | - 167 | - 168 | 169 | **Jacob Thornton** 170 | 171 | - 172 | - 173 | 174 | 175 | ## Thanks 176 | 177 | 178 | BrowserStack Logo 179 | 180 | 181 | Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers! 182 | 183 | 184 | ## Sponsors 185 | 186 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/bootstrap#sponsor)] 187 | 188 | [![](https://opencollective.com/bootstrap/sponsor/0/avatar.svg)](https://opencollective.com/bootstrap/sponsor/0/website) 189 | [![](https://opencollective.com/bootstrap/sponsor/1/avatar.svg)](https://opencollective.com/bootstrap/sponsor/1/website) 190 | [![](https://opencollective.com/bootstrap/sponsor/2/avatar.svg)](https://opencollective.com/bootstrap/sponsor/2/website) 191 | [![](https://opencollective.com/bootstrap/sponsor/3/avatar.svg)](https://opencollective.com/bootstrap/sponsor/3/website) 192 | [![](https://opencollective.com/bootstrap/sponsor/4/avatar.svg)](https://opencollective.com/bootstrap/sponsor/4/website) 193 | [![](https://opencollective.com/bootstrap/sponsor/5/avatar.svg)](https://opencollective.com/bootstrap/sponsor/5/website) 194 | [![](https://opencollective.com/bootstrap/sponsor/6/avatar.svg)](https://opencollective.com/bootstrap/sponsor/6/website) 195 | [![](https://opencollective.com/bootstrap/sponsor/7/avatar.svg)](https://opencollective.com/bootstrap/sponsor/7/website) 196 | [![](https://opencollective.com/bootstrap/sponsor/8/avatar.svg)](https://opencollective.com/bootstrap/sponsor/8/website) 197 | [![](https://opencollective.com/bootstrap/sponsor/9/avatar.svg)](https://opencollective.com/bootstrap/sponsor/9/website) 198 | 199 | 200 | ## Backers 201 | 202 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/bootstrap#backer)] 203 | 204 | [![Backers](https://opencollective.com/bootstrap/backers.svg?width=890)](https://opencollective.com/bootstrap#backers) 205 | 206 | 207 | ## Copyright and license 208 | 209 | Code and documentation copyright 2011-2020 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors) and [Twitter, Inc.](https://twitter.com) Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/). 210 | --------------------------------------------------------------------------------