From cede04a33e4f8b64fef5909c16ddc61f7f84c1fc Mon Sep 17 00:00:00 2001
From: Paul Schneider
Date: Sat, 10 Apr 2021 14:11:09 +0100
Subject: [PATCH] more references.
---
.vscode/tasks.json | 13 +
Config.cs | 64 +
Quickstart/Account/AccountController.cs | 369 +
Quickstart/Account/AccountOptions.cs | 20 +
Quickstart/Account/ExternalController.cs | 196 +
Quickstart/Account/ExternalProvider.cs | 12 +
Quickstart/Account/LoggedOutViewModel.cs | 19 +
Quickstart/Account/LoginInputModel.cs | 18 +
Quickstart/Account/LoginViewModel.cs | 22 +
Quickstart/Account/LogoutInputModel.cs | 11 +
Quickstart/Account/LogoutViewModel.cs | 11 +
Quickstart/Account/RedirectViewModel.cs | 12 +
Quickstart/Consent/ConsentController.cs | 261 +
Quickstart/Consent/ConsentInputModel.cs | 17 +
Quickstart/Consent/ConsentOptions.cs | 16 +
Quickstart/Consent/ConsentViewModel.cs | 19 +
Quickstart/Consent/ProcessConsentResult.cs | 21 +
Quickstart/Consent/ScopeViewModel.cs | 16 +
.../Device/DeviceAuthorizationInputModel.cs | 11 +
.../Device/DeviceAuthorizationViewModel.cs | 12 +
Quickstart/Device/DeviceController.cs | 231 +
.../Diagnostics/DiagnosticsController.cs | 29 +
.../Diagnostics/DiagnosticsViewModel.cs | 32 +
Quickstart/Extensions.cs | 27 +
Quickstart/Grants/GrantsController.cs | 97 +
Quickstart/Grants/GrantsViewModel.cs | 27 +
Quickstart/Home/ErrorViewModel.cs | 22 +
Quickstart/Home/HomeController.cs | 64 +
Quickstart/SecurityHeadersAttribute.cs | 56 +
Quickstart/TestUsers.cs | 66 +
Startup.cs | 16 +-
Views/Account/AccessDenied.cshtml | 7 +
Views/Account/LoggedOut.cshtml | 34 +
Views/Account/Login.cshtml | 87 +
Views/Account/Logout.cshtml | 15 +
Views/Consent/Index.cshtml | 104 +
Views/Device/Success.cshtml | 7 +
Views/Device/UserCodeCapture.cshtml | 23 +
Views/Device/UserCodeConfirmation.cshtml | 108 +
Views/Diagnostics/Index.cshtml | 64 +
Views/Grants/Index.cshtml | 87 +
Views/Home/Index.cshtml | 138 +-
Views/Shared/Error.cshtml | 62 +-
Views/Shared/Redirect.cshtml | 11 +
Views/Shared/_Layout.cshtml | 99 +-
Views/Shared/_Nav.cshtml | 33 +
Views/Shared/_ScopeListItem.cshtml | 34 +
Views/Shared/_ValidationSummary.cshtml | 7 +
Views/_ViewImports.cshtml | 5 +-
Views/_ViewStart.cshtml | 6 +-
nuget-host.csproj | 20 +-
tempkey.rsa | 1 +
wwwroot/css/site.css | 59 +-
wwwroot/css/site.min.css | 2 +-
wwwroot/css/site.scss | 42 +
wwwroot/favicon.ico | Bin 32038 -> 1150 bytes
wwwroot/icon.jpg | Bin 0 -> 19482 bytes
wwwroot/icon.png | Bin 0 -> 20796 bytes
wwwroot/js/signin-redirect.js | 1 +
wwwroot/js/signout-redirect.js | 6 +
wwwroot/lib/bootstrap/LICENSE | 43 +-
wwwroot/lib/bootstrap/README.md | 209 +
.../lib/bootstrap/dist/css/bootstrap-grid.css | 3899 +++
.../bootstrap/dist/css/bootstrap-grid.css.map | 1 +
.../bootstrap/dist/css/bootstrap-grid.min.css | 7 +
.../dist/css/bootstrap-grid.min.css.map | 1 +
.../bootstrap/dist/css/bootstrap-reboot.css | 327 +
.../dist/css/bootstrap-reboot.css.map | 1 +
.../dist/css/bootstrap-reboot.min.css | 8 +
.../dist/css/bootstrap-reboot.min.css.map | 1 +
wwwroot/lib/bootstrap/dist/css/bootstrap.css | 16981 ++++++++-----
.../lib/bootstrap/dist/css/bootstrap.css.map | 2 +-
.../lib/bootstrap/dist/css/bootstrap.min.css | 11 +-
.../bootstrap/dist/css/bootstrap.min.css.map | 2 +-
.../lib/bootstrap/dist/js/bootstrap.bundle.js | 7134 ++++++
.../bootstrap/dist/js/bootstrap.bundle.js.map | 1 +
.../bootstrap/dist/js/bootstrap.bundle.min.js | 7 +
.../dist/js/bootstrap.bundle.min.js.map | 1 +
wwwroot/lib/bootstrap/dist/js/bootstrap.js | 6898 +++--
.../lib/bootstrap/dist/js/bootstrap.js.map | 1 +
.../lib/bootstrap/dist/js/bootstrap.min.js | 14 +-
.../bootstrap/dist/js/bootstrap.min.js.map | 1 +
wwwroot/lib/bootstrap/scss/_alert.scss | 51 +
wwwroot/lib/bootstrap/scss/_badge.scss | 54 +
wwwroot/lib/bootstrap/scss/_breadcrumb.scss | 42 +
wwwroot/lib/bootstrap/scss/_button-group.scss | 163 +
wwwroot/lib/bootstrap/scss/_buttons.scss | 139 +
wwwroot/lib/bootstrap/scss/_card.scss | 278 +
wwwroot/lib/bootstrap/scss/_carousel.scss | 197 +
wwwroot/lib/bootstrap/scss/_close.scss | 41 +
wwwroot/lib/bootstrap/scss/_code.scss | 48 +
wwwroot/lib/bootstrap/scss/_custom-forms.scss | 521 +
wwwroot/lib/bootstrap/scss/_dropdown.scss | 191 +
wwwroot/lib/bootstrap/scss/_forms.scss | 338 +
wwwroot/lib/bootstrap/scss/_functions.scss | 134 +
wwwroot/lib/bootstrap/scss/_grid.scss | 69 +
wwwroot/lib/bootstrap/scss/_images.scss | 42 +
wwwroot/lib/bootstrap/scss/_input-group.scss | 191 +
wwwroot/lib/bootstrap/scss/_jumbotron.scss | 17 +
wwwroot/lib/bootstrap/scss/_list-group.scss | 158 +
wwwroot/lib/bootstrap/scss/_media.scss | 8 +
wwwroot/lib/bootstrap/scss/_mixins.scss | 47 +
wwwroot/lib/bootstrap/scss/_modal.scss | 239 +
wwwroot/lib/bootstrap/scss/_nav.scss | 120 +
wwwroot/lib/bootstrap/scss/_navbar.scss | 324 +
wwwroot/lib/bootstrap/scss/_pagination.scss | 73 +
wwwroot/lib/bootstrap/scss/_popover.scss | 170 +
wwwroot/lib/bootstrap/scss/_print.scss | 141 +
wwwroot/lib/bootstrap/scss/_progress.scss | 46 +
wwwroot/lib/bootstrap/scss/_reboot.scss | 482 +
wwwroot/lib/bootstrap/scss/_root.scss | 20 +
wwwroot/lib/bootstrap/scss/_spinners.scss | 55 +
wwwroot/lib/bootstrap/scss/_tables.scss | 185 +
wwwroot/lib/bootstrap/scss/_toasts.scss | 44 +
wwwroot/lib/bootstrap/scss/_tooltip.scss | 115 +
wwwroot/lib/bootstrap/scss/_transitions.scss | 20 +
wwwroot/lib/bootstrap/scss/_type.scss | 125 +
wwwroot/lib/bootstrap/scss/_utilities.scss | 17 +
wwwroot/lib/bootstrap/scss/_variables.scss | 1143 +
.../lib/bootstrap/scss/bootstrap-grid.scss | 29 +
.../lib/bootstrap/scss/bootstrap-reboot.scss | 12 +
wwwroot/lib/bootstrap/scss/bootstrap.scss | 44 +
wwwroot/lib/bootstrap/scss/mixins/_alert.scss | 13 +
.../scss/mixins/_background-variant.scss | 22 +
wwwroot/lib/bootstrap/scss/mixins/_badge.scss | 17 +
.../bootstrap/scss/mixins/_border-radius.scss | 63 +
.../bootstrap/scss/mixins/_box-shadow.scss | 20 +
.../bootstrap/scss/mixins/_breakpoints.scss | 123 +
.../lib/bootstrap/scss/mixins/_buttons.scss | 110 +
wwwroot/lib/bootstrap/scss/mixins/_caret.scss | 62 +
.../lib/bootstrap/scss/mixins/_clearfix.scss | 7 +
.../lib/bootstrap/scss/mixins/_deprecate.scss | 10 +
wwwroot/lib/bootstrap/scss/mixins/_float.scss | 14 +
wwwroot/lib/bootstrap/scss/mixins/_forms.scss | 177 +
.../lib/bootstrap/scss/mixins/_gradients.scss | 45 +
.../scss/mixins/_grid-framework.scss | 71 +
wwwroot/lib/bootstrap/scss/mixins/_grid.scss | 69 +
wwwroot/lib/bootstrap/scss/mixins/_hover.scss | 37 +
wwwroot/lib/bootstrap/scss/mixins/_image.scss | 36 +
.../bootstrap/scss/mixins/_list-group.scss | 21 +
wwwroot/lib/bootstrap/scss/mixins/_lists.scss | 7 +
.../bootstrap/scss/mixins/_nav-divider.scss | 11 +
.../bootstrap/scss/mixins/_pagination.scss | 22 +
.../bootstrap/scss/mixins/_reset-text.scss | 17 +
.../lib/bootstrap/scss/mixins/_resize.scss | 6 +
.../bootstrap/scss/mixins/_screen-reader.scss | 34 +
wwwroot/lib/bootstrap/scss/mixins/_size.scss | 7 +
.../lib/bootstrap/scss/mixins/_table-row.scss | 39 +
.../bootstrap/scss/mixins/_text-emphasis.scss | 17 +
.../lib/bootstrap/scss/mixins/_text-hide.scss | 11 +
.../bootstrap/scss/mixins/_text-truncate.scss | 8 +
.../bootstrap/scss/mixins/_transition.scss | 16 +
.../bootstrap/scss/mixins/_visibility.scss | 8 +
.../lib/bootstrap/scss/utilities/_align.scss | 8 +
.../bootstrap/scss/utilities/_background.scss | 19 +
.../bootstrap/scss/utilities/_borders.scss | 75 +
.../bootstrap/scss/utilities/_clearfix.scss | 3 +
.../bootstrap/scss/utilities/_display.scss | 26 +
.../lib/bootstrap/scss/utilities/_embed.scss | 39 +
.../lib/bootstrap/scss/utilities/_flex.scss | 51 +
.../lib/bootstrap/scss/utilities/_float.scss | 11 +
.../bootstrap/scss/utilities/_overflow.scss | 5 +
.../bootstrap/scss/utilities/_position.scss | 32 +
.../scss/utilities/_screenreaders.scss | 11 +
.../bootstrap/scss/utilities/_shadows.scss | 6 +
.../lib/bootstrap/scss/utilities/_sizing.scss | 20 +
.../bootstrap/scss/utilities/_spacing.scss | 73 +
.../scss/utilities/_stretched-link.scss | 19 +
.../lib/bootstrap/scss/utilities/_text.scss | 72 +
.../bootstrap/scss/utilities/_visibility.scss | 13 +
wwwroot/lib/bootstrap/scss/vendor/_rfs.scss | 204 +
wwwroot/lib/jquery/LICENSE.txt | 56 +-
wwwroot/lib/jquery/README.md | 62 +
wwwroot/lib/jquery/dist/jquery.js | 20703 ++++++++--------
wwwroot/lib/jquery/dist/jquery.min.js | 6 +-
wwwroot/lib/jquery/dist/jquery.min.map | 2 +-
wwwroot/lib/jquery/dist/jquery.slim.js | 8777 +++++++
wwwroot/lib/jquery/dist/jquery.slim.min.js | 2 +
wwwroot/lib/jquery/dist/jquery.slim.min.map | 1 +
179 files changed, 56679 insertions(+), 19289 deletions(-)
create mode 100644 Config.cs
create mode 100644 Quickstart/Account/AccountController.cs
create mode 100644 Quickstart/Account/AccountOptions.cs
create mode 100644 Quickstart/Account/ExternalController.cs
create mode 100644 Quickstart/Account/ExternalProvider.cs
create mode 100644 Quickstart/Account/LoggedOutViewModel.cs
create mode 100644 Quickstart/Account/LoginInputModel.cs
create mode 100644 Quickstart/Account/LoginViewModel.cs
create mode 100644 Quickstart/Account/LogoutInputModel.cs
create mode 100644 Quickstart/Account/LogoutViewModel.cs
create mode 100644 Quickstart/Account/RedirectViewModel.cs
create mode 100644 Quickstart/Consent/ConsentController.cs
create mode 100644 Quickstart/Consent/ConsentInputModel.cs
create mode 100644 Quickstart/Consent/ConsentOptions.cs
create mode 100644 Quickstart/Consent/ConsentViewModel.cs
create mode 100644 Quickstart/Consent/ProcessConsentResult.cs
create mode 100644 Quickstart/Consent/ScopeViewModel.cs
create mode 100644 Quickstart/Device/DeviceAuthorizationInputModel.cs
create mode 100644 Quickstart/Device/DeviceAuthorizationViewModel.cs
create mode 100644 Quickstart/Device/DeviceController.cs
create mode 100644 Quickstart/Diagnostics/DiagnosticsController.cs
create mode 100644 Quickstart/Diagnostics/DiagnosticsViewModel.cs
create mode 100644 Quickstart/Extensions.cs
create mode 100644 Quickstart/Grants/GrantsController.cs
create mode 100644 Quickstart/Grants/GrantsViewModel.cs
create mode 100644 Quickstart/Home/ErrorViewModel.cs
create mode 100644 Quickstart/Home/HomeController.cs
create mode 100644 Quickstart/SecurityHeadersAttribute.cs
create mode 100644 Quickstart/TestUsers.cs
create mode 100644 Views/Account/AccessDenied.cshtml
create mode 100644 Views/Account/LoggedOut.cshtml
create mode 100644 Views/Account/Login.cshtml
create mode 100644 Views/Account/Logout.cshtml
create mode 100644 Views/Consent/Index.cshtml
create mode 100644 Views/Device/Success.cshtml
create mode 100644 Views/Device/UserCodeCapture.cshtml
create mode 100644 Views/Device/UserCodeConfirmation.cshtml
create mode 100644 Views/Diagnostics/Index.cshtml
create mode 100644 Views/Grants/Index.cshtml
create mode 100644 Views/Shared/Redirect.cshtml
create mode 100644 Views/Shared/_Nav.cshtml
create mode 100644 Views/Shared/_ScopeListItem.cshtml
create mode 100644 Views/Shared/_ValidationSummary.cshtml
create mode 100644 tempkey.rsa
create mode 100644 wwwroot/css/site.scss
create mode 100644 wwwroot/icon.jpg
create mode 100644 wwwroot/icon.png
create mode 100644 wwwroot/js/signin-redirect.js
create mode 100644 wwwroot/js/signout-redirect.js
create mode 100644 wwwroot/lib/bootstrap/README.md
create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
create mode 100644 wwwroot/lib/bootstrap/scss/_alert.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_badge.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_breadcrumb.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_button-group.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_buttons.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_card.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_carousel.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_close.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_code.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_custom-forms.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_dropdown.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_forms.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_functions.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_grid.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_images.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_input-group.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_jumbotron.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_list-group.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_media.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_mixins.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_modal.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_nav.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_navbar.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_pagination.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_popover.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_print.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_progress.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_reboot.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_root.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_spinners.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_tables.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_toasts.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_tooltip.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_transitions.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_type.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_utilities.scss
create mode 100644 wwwroot/lib/bootstrap/scss/_variables.scss
create mode 100644 wwwroot/lib/bootstrap/scss/bootstrap-grid.scss
create mode 100644 wwwroot/lib/bootstrap/scss/bootstrap-reboot.scss
create mode 100644 wwwroot/lib/bootstrap/scss/bootstrap.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_alert.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_background-variant.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_badge.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_border-radius.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_box-shadow.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_breakpoints.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_buttons.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_caret.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_clearfix.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_deprecate.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_float.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_forms.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_gradients.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_grid-framework.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_grid.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_hover.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_image.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_list-group.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_lists.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_nav-divider.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_pagination.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_reset-text.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_resize.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_screen-reader.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_size.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_table-row.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_text-emphasis.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_text-hide.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_text-truncate.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_transition.scss
create mode 100644 wwwroot/lib/bootstrap/scss/mixins/_visibility.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_align.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_background.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_borders.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_clearfix.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_display.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_embed.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_flex.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_float.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_overflow.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_position.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_screenreaders.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_shadows.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_sizing.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_spacing.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_stretched-link.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_text.scss
create mode 100644 wwwroot/lib/bootstrap/scss/utilities/_visibility.scss
create mode 100644 wwwroot/lib/bootstrap/scss/vendor/_rfs.scss
create mode 100644 wwwroot/lib/jquery/README.md
create mode 100644 wwwroot/lib/jquery/dist/jquery.slim.js
create mode 100644 wwwroot/lib/jquery/dist/jquery.slim.min.js
create mode 100644 wwwroot/lib/jquery/dist/jquery.slim.min.map
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 7e27148..9800232 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,6 +1,19 @@
{
"version": "2.0.0",
"tasks": [
+ {
+ "label": "restore",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "restore",
+ "${workspaceFolder}/nuget-host.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary",
+ "--ignore-failed-sources"
+ ],
+ "problemMatcher": "$msCompile"
+ },
{
"label": "build",
"command": "dotnet",
diff --git a/Config.cs b/Config.cs
new file mode 100644
index 0000000..3c7f1e5
--- /dev/null
+++ b/Config.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using IdentityServer4;
+using IdentityServer4.Models;
+using IdentityServer4.Test;
+
+namespace nuget_host
+{
+ public static class Config
+{
+ public static IEnumerable IdentityResources =>
+ new List
+ {
+ new IdentityResources.OpenId(),
+ new IdentityResources.Profile(),
+ };
+
+ public static IEnumerable ApiResources =>
+ new List
+ {
+ new ApiResource(scope_packages)
+ };
+
+ public const string scope_packages = "packages";
+
+ public static IEnumerable Clients =>
+ new List
+ {
+ // machine to machine client
+ new Client
+ {
+ ClientId = "client",
+ ClientSecrets = { new Secret("secret".Sha256()) },
+
+ AllowedGrantTypes = GrantTypes.ClientCredentials,
+ // scopes that client has access to
+ AllowedScopes = { scope_packages }
+ },
+
+ // interactive ASP.NET Core MVC client
+ new Client
+ {
+ ClientId = "mvc",
+ ClientSecrets = { new Secret("secret".Sha256()) },
+
+ AllowedGrantTypes = GrantTypes.Code,
+
+ // where to redirect to after login
+ RedirectUris = { "https://localhost:5002/signin-oidc" },
+
+ // where to redirect to after logout
+ PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
+
+ AllowedScopes = new List
+ {
+ IdentityServerConstants.StandardScopes.OpenId,
+ IdentityServerConstants.StandardScopes.Profile,
+ scope_packages
+ }
+ }
+ };
+
+ public static List TestUsers { get; internal set; }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Account/AccountController.cs b/Quickstart/Account/AccountController.cs
new file mode 100644
index 0000000..963c9c2
--- /dev/null
+++ b/Quickstart/Account/AccountController.cs
@@ -0,0 +1,369 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using IdentityModel;
+using IdentityServer4;
+using IdentityServer4.Events;
+using IdentityServer4.Extensions;
+using IdentityServer4.Models;
+using IdentityServer4.Services;
+using IdentityServer4.Stores;
+using IdentityServer4.Test;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ ///
+ /// This sample controller implements a typical login/logout/provision workflow for local and external accounts.
+ /// The login service encapsulates the interactions with the user data store. This data store is in-memory only and cannot be used for production!
+ /// The interaction service provides a way for the UI to communicate with identityserver for validation and context retrieval
+ ///
+ [SecurityHeaders]
+ [AllowAnonymous]
+ public class AccountController : Controller
+ {
+ private readonly TestUserStore _users;
+ private readonly IIdentityServerInteractionService _interaction;
+ private readonly IClientStore _clientStore;
+ private readonly IAuthenticationSchemeProvider _schemeProvider;
+ private readonly IEventService _events;
+
+ public AccountController(
+ IIdentityServerInteractionService interaction,
+ IClientStore clientStore,
+ IAuthenticationSchemeProvider schemeProvider,
+ IEventService events,
+ TestUserStore users = null)
+ {
+ // if the TestUserStore is not in DI, then we'll just use the global users collection
+ // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
+ _users = users ?? new TestUserStore(TestUsers.Users);
+
+ _interaction = interaction;
+ _clientStore = clientStore;
+ _schemeProvider = schemeProvider;
+ _events = events;
+ }
+
+ ///
+ /// Entry point into the login workflow
+ ///
+ [HttpGet]
+ public async Task Login(string returnUrl)
+ {
+ // build a model so we know what to show on the login page
+ var vm = await BuildLoginViewModelAsync(returnUrl);
+
+ if (vm.IsExternalLoginOnly)
+ {
+ // we only have one option for logging in and it's an external provider
+ return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl });
+ }
+
+ return View(vm);
+ }
+
+ ///
+ /// Handle postback from username/password login
+ ///
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task Login(LoginInputModel model, string button)
+ {
+ // check if we are in the context of an authorization request
+ var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
+
+ // the user clicked the "cancel" button
+ if (button != "login")
+ {
+ if (context != null)
+ {
+ // if the user cancels, send a result back into IdentityServer as if they
+ // denied the consent (even if this client does not require consent).
+ // this will send back an access denied OIDC error response to the client.
+
+ // FIXME await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
+
+ // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
+ if (context.IsNativeClient())
+ {
+ // The client is native, so this change in how to
+ // return the response is for better UX for the end user.
+ return this.LoadingPage("Redirect", model.ReturnUrl);
+ }
+
+ return Redirect(model.ReturnUrl);
+ }
+ else
+ {
+ // since we don't have a valid context, then we just go back to the home page
+ return Redirect("~/");
+ }
+ }
+
+ if (ModelState.IsValid)
+ {
+ // validate username/password against in-memory store
+ if (_users.ValidateCredentials(model.Username, model.Password))
+ {
+ var user = _users.FindByUsername(model.Username);
+ await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.ClientId));
+
+ // only set explicit expiration here if user chooses "remember me".
+ // otherwise we rely upon expiration configured in cookie middleware.
+ AuthenticationProperties props = null;
+ if (AccountOptions.AllowRememberLogin && model.RememberLogin)
+ {
+ props = new AuthenticationProperties
+ {
+ IsPersistent = true,
+ ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
+ };
+ };
+
+ // issue authentication cookie with subject ID and username
+ var isuser = new IdentityServerUser(user.SubjectId)
+ {
+ DisplayName = user.Username
+ };
+
+ await HttpContext.SignInAsync(isuser, props);
+
+ if (context != null)
+ {
+ if (context.IsNativeClient())
+ {
+ // The client is native, so this change in how to
+ // return the response is for better UX for the end user.
+ return this.LoadingPage("Redirect", model.ReturnUrl);
+ }
+
+ // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
+ return Redirect(model.ReturnUrl);
+ }
+
+ // request for a local page
+ if (Url.IsLocalUrl(model.ReturnUrl))
+ {
+ return Redirect(model.ReturnUrl);
+ }
+ else if (string.IsNullOrEmpty(model.ReturnUrl))
+ {
+ return Redirect("~/");
+ }
+ else
+ {
+ // user might have clicked on a malicious link - should be logged
+ throw new Exception("invalid return URL");
+ }
+ }
+
+ await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.ClientId));
+ ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
+ }
+
+ // something went wrong, show form with error
+ var vm = await BuildLoginViewModelAsync(model);
+ return View(vm);
+ }
+
+
+ ///
+ /// Show logout page
+ ///
+ [HttpGet]
+ public async Task Logout(string logoutId)
+ {
+ // build a model so the logout page knows what to display
+ var vm = await BuildLogoutViewModelAsync(logoutId);
+
+ if (vm.ShowLogoutPrompt == false)
+ {
+ // if the request for logout was properly authenticated from IdentityServer, then
+ // we don't need to show the prompt and can just log the user out directly.
+ return await Logout(vm);
+ }
+
+ return View(vm);
+ }
+
+ ///
+ /// Handle logout page postback
+ ///
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task Logout(LogoutInputModel model)
+ {
+ // build a model so the logged out page knows what to display
+ var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
+
+ if (User?.Identity.IsAuthenticated == true)
+ {
+ // delete local authentication cookie
+ await HttpContext.SignOutAsync();
+
+ // raise the logout event
+ await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
+ }
+
+ // check if we need to trigger sign-out at an upstream identity provider
+ if (vm.TriggerExternalSignout)
+ {
+ // build a return URL so the upstream provider will redirect back
+ // to us after the user has logged out. this allows us to then
+ // complete our single sign-out processing.
+ string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
+
+ // this triggers a redirect to the external provider for sign-out
+ return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
+ }
+
+ return View("LoggedOut", vm);
+ }
+
+ [HttpGet]
+ public IActionResult AccessDenied()
+ {
+ return new BadRequestObjectResult(403);
+ }
+
+
+ /*****************************************/
+ /* helper APIs for the AccountController */
+ /*****************************************/
+ private async Task BuildLoginViewModelAsync(string returnUrl)
+ {
+ var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
+ if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
+ {
+ var local = context.IdP == IdentityServer4.IdentityServerConstants.LocalIdentityProvider;
+
+ // this is meant to short circuit the UI and only trigger the one external IdP
+ var vm = new LoginViewModel
+ {
+ EnableLocalLogin = local,
+ ReturnUrl = returnUrl,
+ Username = context?.LoginHint,
+ };
+
+ if (!local)
+ {
+ vm.ExternalProviders = new[] { new ExternalProvider { AuthenticationScheme = context.IdP } };
+ }
+
+ return vm;
+ }
+
+ var schemes = await _schemeProvider.GetAllSchemesAsync();
+
+ var providers = schemes
+ .Where(x => x.DisplayName != null)
+ .Select(x => new ExternalProvider
+ {
+ DisplayName = x.DisplayName ?? x.Name,
+ AuthenticationScheme = x.Name
+ }).ToList();
+
+ var allowLocal = true;
+ if (context?.ClientId != null)
+ {
+ var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId);
+ if (client != null)
+ {
+ allowLocal = client.EnableLocalLogin;
+
+ if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any())
+ {
+ providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
+ }
+ }
+ }
+
+ return new LoginViewModel
+ {
+ AllowRememberLogin = AccountOptions.AllowRememberLogin,
+ EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin,
+ ReturnUrl = returnUrl,
+ Username = context?.LoginHint,
+ ExternalProviders = providers.ToArray()
+ };
+ }
+
+ private async Task BuildLoginViewModelAsync(LoginInputModel model)
+ {
+ var vm = await BuildLoginViewModelAsync(model.ReturnUrl);
+ vm.Username = model.Username;
+ vm.RememberLogin = model.RememberLogin;
+ return vm;
+ }
+
+ private async Task BuildLogoutViewModelAsync(string logoutId)
+ {
+ var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt };
+
+ if (User?.Identity.IsAuthenticated != true)
+ {
+ // if the user is not authenticated, then just show logged out page
+ vm.ShowLogoutPrompt = false;
+ return vm;
+ }
+
+ var context = await _interaction.GetLogoutContextAsync(logoutId);
+ if (context?.ShowSignoutPrompt == false)
+ {
+ // it's safe to automatically sign-out
+ vm.ShowLogoutPrompt = false;
+ return vm;
+ }
+
+ // show the logout prompt. this prevents attacks where the user
+ // is automatically signed out by another malicious web page.
+ return vm;
+ }
+
+ private async Task BuildLoggedOutViewModelAsync(string logoutId)
+ {
+ // get context information (client name, post logout redirect URI and iframe for federated signout)
+ var logout = await _interaction.GetLogoutContextAsync(logoutId);
+
+ var vm = new LoggedOutViewModel
+ {
+ AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,
+ PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
+ ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
+ SignOutIframeUrl = logout?.SignOutIFrameUrl,
+ LogoutId = logoutId
+ };
+
+ if (User?.Identity.IsAuthenticated == true)
+ {
+ var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
+ if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider)
+ {
+ var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
+ if (providerSupportsSignout)
+ {
+ if (vm.LogoutId == null)
+ {
+ // if there's no current logout context, we need to create one
+ // this captures necessary info from the current logged in user
+ // before we signout and redirect away to the external IdP for signout
+ vm.LogoutId = await _interaction.CreateLogoutContextAsync();
+ }
+
+ vm.ExternalAuthenticationScheme = idp;
+ }
+ }
+ }
+
+ return vm;
+ }
+ }
+}
diff --git a/Quickstart/Account/AccountOptions.cs b/Quickstart/Account/AccountOptions.cs
new file mode 100644
index 0000000..552f190
--- /dev/null
+++ b/Quickstart/Account/AccountOptions.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class AccountOptions
+ {
+ public static bool AllowLocalLogin = true;
+ public static bool AllowRememberLogin = true;
+ public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);
+
+ public static bool ShowLogoutPrompt = true;
+ public static bool AutomaticRedirectAfterSignOut = false;
+
+ public static string InvalidCredentialsErrorMessage = "Invalid username or password";
+ }
+}
diff --git a/Quickstart/Account/ExternalController.cs b/Quickstart/Account/ExternalController.cs
new file mode 100644
index 0000000..1a7479e
--- /dev/null
+++ b/Quickstart/Account/ExternalController.cs
@@ -0,0 +1,196 @@
+using IdentityModel;
+using IdentityServer4;
+using IdentityServer4.Events;
+using IdentityServer4.Services;
+using IdentityServer4.Stores;
+using IdentityServer4.Test;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ [SecurityHeaders]
+ [AllowAnonymous]
+ public class ExternalController : Controller
+ {
+ private readonly TestUserStore _users;
+ private readonly IIdentityServerInteractionService _interaction;
+ private readonly IClientStore _clientStore;
+ private readonly ILogger _logger;
+ private readonly IEventService _events;
+
+ public ExternalController(
+ IIdentityServerInteractionService interaction,
+ IClientStore clientStore,
+ IEventService events,
+ ILogger logger,
+ TestUserStore users = null)
+ {
+ // if the TestUserStore is not in DI, then we'll just use the global users collection
+ // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
+ _users = users ?? new TestUserStore(TestUsers.Users);
+
+ _interaction = interaction;
+ _clientStore = clientStore;
+ _logger = logger;
+ _events = events;
+ }
+
+ ///
+ /// initiate roundtrip to external authentication provider
+ ///
+ [HttpGet]
+ public IActionResult Challenge(string scheme, string returnUrl)
+ {
+ if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
+
+ // validate returnUrl - either it is a valid OIDC URL or back to a local page
+ if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
+ {
+ // user might have clicked on a malicious link - should be logged
+ throw new Exception("invalid return URL");
+ }
+
+ // start challenge and roundtrip the return URL and scheme
+ var props = new AuthenticationProperties
+ {
+ RedirectUri = Url.Action(nameof(Callback)),
+ Items =
+ {
+ { "returnUrl", returnUrl },
+ { "scheme", scheme },
+ }
+ };
+
+ return Challenge(props, scheme);
+
+ }
+
+ ///
+ /// Post processing of external authentication
+ ///
+ [HttpGet]
+ public async Task Callback()
+ {
+ // read external identity from the temporary cookie
+ var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
+ if (result?.Succeeded != true)
+ {
+ throw new Exception("External authentication error");
+ }
+
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
+ _logger.LogDebug("External claims: {@claims}", externalClaims);
+ }
+
+ // lookup our user and external provider info
+ var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
+ if (user == null)
+ {
+ // this might be where you might initiate a custom workflow for user registration
+ // in this sample we don't show how that would be done, as our sample implementation
+ // simply auto-provisions new external user
+ user = AutoProvisionUser(provider, providerUserId, claims);
+ }
+
+ // this allows us to collect any additional claims or properties
+ // for the specific protocols used and store them in the local auth cookie.
+ // this is typically used to store data needed for signout from those protocols.
+ var additionalLocalClaims = new List();
+ var localSignInProps = new AuthenticationProperties();
+ ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
+
+ // issue authentication cookie for user
+ var isuser = new IdentityServerUser(user.SubjectId)
+ {
+ DisplayName = user.Username,
+ IdentityProvider = provider,
+ AdditionalClaims = additionalLocalClaims
+ };
+
+ await HttpContext.SignInAsync(isuser, localSignInProps);
+
+ // delete temporary cookie used during external authentication
+ await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
+
+ // retrieve return URL
+ var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
+
+ // check if external login is in the context of an OIDC request
+ var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
+ await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));
+
+ if (context != null)
+ {
+ if (context.IsNativeClient())
+ {
+ // The client is native, so this change in how to
+ // return the response is for better UX for the end user.
+ return this.LoadingPage("Redirect", returnUrl);
+ }
+ }
+
+ return Redirect(returnUrl);
+ }
+
+ private (TestUser user, string provider, string providerUserId, IEnumerable claims) FindUserFromExternalProvider(AuthenticateResult result)
+ {
+ var externalUser = result.Principal;
+
+ // try to determine the unique id of the external user (issued by the provider)
+ // the most common claim type for that are the sub claim and the NameIdentifier
+ // depending on the external provider, some other claim type might be used
+ var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
+ externalUser.FindFirst(ClaimTypes.NameIdentifier) ??
+ throw new Exception("Unknown userid");
+
+ // remove the user id claim so we don't include it as an extra claim if/when we provision the user
+ var claims = externalUser.Claims.ToList();
+ claims.Remove(userIdClaim);
+
+ var provider = result.Properties.Items["scheme"];
+ var providerUserId = userIdClaim.Value;
+
+ // find external user
+ var user = _users.FindByExternalProvider(provider, providerUserId);
+
+ return (user, provider, providerUserId, claims);
+ }
+
+ private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable claims)
+ {
+ var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList());
+ return user;
+ }
+
+ // if the external login is OIDC-based, there are certain things we need to preserve to make logout work
+ // this will be different for WS-Fed, SAML2p or other protocols
+ private void ProcessLoginCallback(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps)
+ {
+ // if the external system sent a session id claim, copy it over
+ // so we can use it for single sign-out
+ var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
+ if (sid != null)
+ {
+ localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
+ }
+
+ // if the external provider issued an id_token, we'll keep it for signout
+ var idToken = externalResult.Properties.GetTokenValue("id_token");
+ if (idToken != null)
+ {
+ localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Account/ExternalProvider.cs b/Quickstart/Account/ExternalProvider.cs
new file mode 100644
index 0000000..a183113
--- /dev/null
+++ b/Quickstart/Account/ExternalProvider.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class ExternalProvider
+ {
+ public string DisplayName { get; set; }
+ public string AuthenticationScheme { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Account/LoggedOutViewModel.cs b/Quickstart/Account/LoggedOutViewModel.cs
new file mode 100644
index 0000000..6368832
--- /dev/null
+++ b/Quickstart/Account/LoggedOutViewModel.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class LoggedOutViewModel
+ {
+ public string PostLogoutRedirectUri { get; set; }
+ public string ClientName { get; set; }
+ public string SignOutIframeUrl { get; set; }
+
+ public bool AutomaticRedirectAfterSignOut { get; set; }
+
+ public string LogoutId { get; set; }
+ public bool TriggerExternalSignout => ExternalAuthenticationScheme != null;
+ public string ExternalAuthenticationScheme { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Account/LoginInputModel.cs b/Quickstart/Account/LoginInputModel.cs
new file mode 100644
index 0000000..bcb7853
--- /dev/null
+++ b/Quickstart/Account/LoginInputModel.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System.ComponentModel.DataAnnotations;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class LoginInputModel
+ {
+ [Required]
+ public string Username { get; set; }
+ [Required]
+ public string Password { get; set; }
+ public bool RememberLogin { get; set; }
+ public string ReturnUrl { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Account/LoginViewModel.cs b/Quickstart/Account/LoginViewModel.cs
new file mode 100644
index 0000000..9bf6c8f
--- /dev/null
+++ b/Quickstart/Account/LoginViewModel.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class LoginViewModel : LoginInputModel
+ {
+ public bool AllowRememberLogin { get; set; } = true;
+ public bool EnableLocalLogin { get; set; } = true;
+
+ public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty();
+ public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName));
+
+ public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1;
+ public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null;
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Account/LogoutInputModel.cs b/Quickstart/Account/LogoutInputModel.cs
new file mode 100644
index 0000000..bb74202
--- /dev/null
+++ b/Quickstart/Account/LogoutInputModel.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class LogoutInputModel
+ {
+ public string LogoutId { get; set; }
+ }
+}
diff --git a/Quickstart/Account/LogoutViewModel.cs b/Quickstart/Account/LogoutViewModel.cs
new file mode 100644
index 0000000..236cd6c
--- /dev/null
+++ b/Quickstart/Account/LogoutViewModel.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class LogoutViewModel : LogoutInputModel
+ {
+ public bool ShowLogoutPrompt { get; set; } = true;
+ }
+}
diff --git a/Quickstart/Account/RedirectViewModel.cs b/Quickstart/Account/RedirectViewModel.cs
new file mode 100644
index 0000000..904ae20
--- /dev/null
+++ b/Quickstart/Account/RedirectViewModel.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class RedirectViewModel
+ {
+ public string RedirectUrl { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Consent/ConsentController.cs b/Quickstart/Consent/ConsentController.cs
new file mode 100644
index 0000000..99d5b11
--- /dev/null
+++ b/Quickstart/Consent/ConsentController.cs
@@ -0,0 +1,261 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using IdentityServer4.Events;
+using IdentityServer4.Models;
+using IdentityServer4.Services;
+using IdentityServer4.Extensions;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System.Linq;
+using System.Threading.Tasks;
+using IdentityServer4.Validation;
+using System.Collections.Generic;
+using System;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ ///
+ /// This controller processes the consent UI
+ ///
+ [SecurityHeaders]
+ [Authorize]
+ public class ConsentController : Controller
+ {
+ private readonly IIdentityServerInteractionService _interaction;
+ private readonly IEventService _events;
+ private readonly ILogger _logger;
+
+ public ConsentController(
+ IIdentityServerInteractionService interaction,
+ IEventService events,
+ ILogger logger)
+ {
+ _interaction = interaction;
+ _events = events;
+ _logger = logger;
+ }
+
+ ///
+ /// Shows the consent screen
+ ///
+ ///
+ ///
+ [HttpGet]
+ public async Task Index(string returnUrl)
+ {
+ var vm = await BuildViewModelAsync(returnUrl);
+ if (vm != null)
+ {
+ return View("Index", vm);
+ }
+
+ return View("Error");
+ }
+
+ ///
+ /// Handles the consent screen postback
+ ///
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task Index(ConsentInputModel model)
+ {
+ var result = await ProcessConsent(model);
+
+ if (result.IsRedirect)
+ {
+ var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
+ if (context?.IsNativeClient() == true)
+ {
+ // The client is native, so this change in how to
+ // return the response is for better UX for the end user.
+ return this.LoadingPage("Redirect", result.RedirectUri);
+ }
+
+ return Redirect(result.RedirectUri);
+ }
+
+ if (result.HasValidationError)
+ {
+ ModelState.AddModelError(string.Empty, result.ValidationError);
+ }
+
+ if (result.ShowView)
+ {
+ return View("Index", result.ViewModel);
+ }
+
+ return View("Error");
+ }
+
+ /*****************************************/
+ /* helper APIs for the ConsentController */
+ /*****************************************/
+ private async Task ProcessConsent(ConsentInputModel model)
+ {
+ var result = new ProcessConsentResult();
+
+ // validate return url is still valid
+ var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
+ if (request == null) return result;
+
+ ConsentResponse grantedConsent = null;
+
+ // user clicked 'no' - send back the standard 'access_denied' response
+ if (model?.Button == "no")
+ {
+ grantedConsent = ConsentResponse.Denied;
+
+ // emit event
+ await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested));
+ }
+ // user clicked 'yes' - validate the data
+ else if (model?.Button == "yes")
+ {
+ // if the user consented to some scope, build the response model
+ if (model.ScopesConsented != null && model.ScopesConsented.Any())
+ {
+ var scopes = model.ScopesConsented;
+ if (ConsentOptions.EnableOfflineAccess == false)
+ {
+ scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
+ }
+
+ grantedConsent = new ConsentResponse
+ {
+ RememberConsent = model.RememberConsent,
+ ScopesConsented = scopes.ToArray()
+ };
+
+ // emit event
+ await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested, grantedConsent.ScopesConsented, grantedConsent.RememberConsent));
+ }
+ else
+ {
+ result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
+ }
+ }
+ else
+ {
+ result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
+ }
+
+ if (grantedConsent != null)
+ {
+ // communicate outcome of consent back to identityserver
+ await _interaction.GrantConsentAsync(request, grantedConsent);
+
+ // indicate that's it ok to redirect back to authorization endpoint
+ result.RedirectUri = model.ReturnUrl;
+ result.Client = request.Client;
+ }
+ else
+ {
+ // we need to redisplay the consent UI
+ result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model);
+ }
+
+ return result;
+ }
+
+ private async Task BuildViewModelAsync(string returnUrl, ConsentInputModel model = null)
+ {
+ var request = await _interaction.GetAuthorizationContextAsync(returnUrl);
+ if (request != null)
+ {
+ return CreateConsentViewModel(model, returnUrl, request);
+ }
+ else
+ {
+ _logger.LogError("No consent request matching request: {0}", returnUrl);
+ }
+
+ return null;
+ }
+
+ private ConsentViewModel CreateConsentViewModel(
+ ConsentInputModel model, string returnUrl,
+ AuthorizationRequest request)
+ {
+ var vm = new ConsentViewModel
+ {
+ RememberConsent = model?.RememberConsent ?? true,
+ ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(),
+ Description = model?.Description,
+
+ ReturnUrl = returnUrl,
+
+ ClientName = request.Client.ClientName ?? request.Client.ClientId,
+ ClientUrl = request.Client.ClientUri,
+ ClientLogoUrl = request.Client.LogoUri,
+ AllowRememberConsent = request.Client.AllowRememberConsent
+ };
+
+ vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
+
+ var apiScopes = new List();
+ foreach(var parsedScope in request.ValidatedResources.ParsedScopes)
+ {
+ var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);
+ if (apiScope != null)
+ {
+ var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null);
+ apiScopes.Add(scopeVm);
+ }
+ }
+ if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
+ {
+ apiScopes.Add(GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null));
+ }
+ vm.ApiScopes = apiScopes;
+
+ return vm;
+ }
+
+ private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
+ {
+ return new ScopeViewModel
+ {
+ Value = identity.Name,
+ DisplayName = identity.DisplayName ?? identity.Name,
+ Description = identity.Description,
+ Emphasize = identity.Emphasize,
+ Required = identity.Required,
+ Checked = check || identity.Required
+ };
+ }
+
+ public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
+ {
+ var displayName = apiScope.DisplayName ?? apiScope.Name;
+ if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter))
+ {
+ displayName += ":" + parsedScopeValue.ParsedParameter;
+ }
+
+ return new ScopeViewModel
+ {
+ Value = parsedScopeValue.RawValue,
+ DisplayName = displayName,
+ Description = apiScope.Description,
+ Emphasize = apiScope.Emphasize,
+ Required = apiScope.Required,
+ Checked = check || apiScope.Required
+ };
+ }
+
+ private ScopeViewModel GetOfflineAccessScope(bool check)
+ {
+ return new ScopeViewModel
+ {
+ Value = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
+ DisplayName = ConsentOptions.OfflineAccessDisplayName,
+ Description = ConsentOptions.OfflineAccessDescription,
+ Emphasize = true,
+ Checked = check
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Consent/ConsentInputModel.cs b/Quickstart/Consent/ConsentInputModel.cs
new file mode 100644
index 0000000..f608fe3
--- /dev/null
+++ b/Quickstart/Consent/ConsentInputModel.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System.Collections.Generic;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class ConsentInputModel
+ {
+ public string Button { get; set; }
+ public IEnumerable ScopesConsented { get; set; }
+ public bool RememberConsent { get; set; }
+ public string ReturnUrl { get; set; }
+ public string Description { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Consent/ConsentOptions.cs b/Quickstart/Consent/ConsentOptions.cs
new file mode 100644
index 0000000..998c51d
--- /dev/null
+++ b/Quickstart/Consent/ConsentOptions.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class ConsentOptions
+ {
+ public static bool EnableOfflineAccess = true;
+ public static string OfflineAccessDisplayName = "Offline Access";
+ public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline";
+
+ public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission";
+ public static readonly string InvalidSelectionErrorMessage = "Invalid selection";
+ }
+}
diff --git a/Quickstart/Consent/ConsentViewModel.cs b/Quickstart/Consent/ConsentViewModel.cs
new file mode 100644
index 0000000..af4b9c5
--- /dev/null
+++ b/Quickstart/Consent/ConsentViewModel.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System.Collections.Generic;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class ConsentViewModel : ConsentInputModel
+ {
+ public string ClientName { get; set; }
+ public string ClientUrl { get; set; }
+ public string ClientLogoUrl { get; set; }
+ public bool AllowRememberConsent { get; set; }
+
+ public IEnumerable IdentityScopes { get; set; }
+ public IEnumerable ApiScopes { get; set; }
+ }
+}
diff --git a/Quickstart/Consent/ProcessConsentResult.cs b/Quickstart/Consent/ProcessConsentResult.cs
new file mode 100644
index 0000000..1d331df
--- /dev/null
+++ b/Quickstart/Consent/ProcessConsentResult.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using IdentityServer4.Models;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class ProcessConsentResult
+ {
+ public bool IsRedirect => RedirectUri != null;
+ public string RedirectUri { get; set; }
+ public Client Client { get; set; }
+
+ public bool ShowView => ViewModel != null;
+ public ConsentViewModel ViewModel { get; set; }
+
+ public bool HasValidationError => ValidationError != null;
+ public string ValidationError { get; set; }
+ }
+}
diff --git a/Quickstart/Consent/ScopeViewModel.cs b/Quickstart/Consent/ScopeViewModel.cs
new file mode 100644
index 0000000..532d1b1
--- /dev/null
+++ b/Quickstart/Consent/ScopeViewModel.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class ScopeViewModel
+ {
+ public string Value { get; set; }
+ public string DisplayName { get; set; }
+ public string Description { get; set; }
+ public bool Emphasize { get; set; }
+ public bool Required { get; set; }
+ public bool Checked { get; set; }
+ }
+}
diff --git a/Quickstart/Device/DeviceAuthorizationInputModel.cs b/Quickstart/Device/DeviceAuthorizationInputModel.cs
new file mode 100644
index 0000000..a221181
--- /dev/null
+++ b/Quickstart/Device/DeviceAuthorizationInputModel.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class DeviceAuthorizationInputModel : ConsentInputModel
+ {
+ public string UserCode { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Device/DeviceAuthorizationViewModel.cs b/Quickstart/Device/DeviceAuthorizationViewModel.cs
new file mode 100644
index 0000000..3e8857f
--- /dev/null
+++ b/Quickstart/Device/DeviceAuthorizationViewModel.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class DeviceAuthorizationViewModel : ConsentViewModel
+ {
+ public string UserCode { get; set; }
+ public bool ConfirmUserCode { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Device/DeviceController.cs b/Quickstart/Device/DeviceController.cs
new file mode 100644
index 0000000..390afda
--- /dev/null
+++ b/Quickstart/Device/DeviceController.cs
@@ -0,0 +1,231 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using IdentityServer4.Configuration;
+using IdentityServer4.Events;
+using IdentityServer4.Extensions;
+using IdentityServer4.Models;
+using IdentityServer4.Services;
+using IdentityServer4.Validation;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ [Authorize]
+ [SecurityHeaders]
+ public class DeviceController : Controller
+ {
+ private readonly IDeviceFlowInteractionService _interaction;
+ private readonly IEventService _events;
+ private readonly IOptions _options;
+ private readonly ILogger _logger;
+
+ public DeviceController(
+ IDeviceFlowInteractionService interaction,
+ IEventService eventService,
+ IOptions options,
+ ILogger logger)
+ {
+ _interaction = interaction;
+ _events = eventService;
+ _options = options;
+ _logger = logger;
+ }
+
+ [HttpGet]
+ public async Task Index()
+ {
+ string userCodeParamName = _options.Value.UserInteraction.DeviceVerificationUserCodeParameter;
+ string userCode = Request.Query[userCodeParamName];
+ if (string.IsNullOrWhiteSpace(userCode)) return View("UserCodeCapture");
+
+ var vm = await BuildViewModelAsync(userCode);
+ if (vm == null) return View("Error");
+
+ vm.ConfirmUserCode = true;
+ return View("UserCodeConfirmation", vm);
+ }
+
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task UserCodeCapture(string userCode)
+ {
+ var vm = await BuildViewModelAsync(userCode);
+ if (vm == null) return View("Error");
+
+ return View("UserCodeConfirmation", vm);
+ }
+
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task Callback(DeviceAuthorizationInputModel model)
+ {
+ if (model == null) throw new ArgumentNullException(nameof(model));
+
+ var result = await ProcessConsent(model);
+ if (result.HasValidationError) return View("Error");
+
+ return View("Success");
+ }
+
+ private async Task ProcessConsent(DeviceAuthorizationInputModel model)
+ {
+ var result = new ProcessConsentResult();
+
+ var request = await _interaction.GetAuthorizationContextAsync(model.UserCode);
+ if (request == null) return result;
+
+ ConsentResponse grantedConsent = null;
+
+ // user clicked 'no' - send back the standard 'access_denied' response
+ if (model.Button == "no")
+ {
+ grantedConsent = ConsentResponse.Denied;
+
+ // emit event
+ await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested));
+ }
+ // user clicked 'yes' - validate the data
+ else if (model.Button == "yes")
+ {
+ // if the user consented to some scope, build the response model
+ if (model.ScopesConsented != null && model.ScopesConsented.Any())
+ {
+ var scopes = model.ScopesConsented;
+ if (ConsentOptions.EnableOfflineAccess == false)
+ {
+ scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
+ }
+
+ grantedConsent = new ConsentResponse
+ {
+ RememberConsent = model.RememberConsent,
+ ScopesConsented = scopes.ToArray()
+ };
+
+ // emit event
+ await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested, grantedConsent.ScopesConsented, grantedConsent.RememberConsent));
+ }
+ else
+ {
+ result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
+ }
+ }
+ else
+ {
+ result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
+ }
+
+ if (grantedConsent != null)
+ {
+ // communicate outcome of consent back to identityserver
+ await _interaction.HandleRequestAsync(model.UserCode, grantedConsent);
+
+ // indicate that's it ok to redirect back to authorization endpoint
+ result.RedirectUri = model.ReturnUrl;
+ result.Client = request.Client;
+ }
+ else
+ {
+ // we need to redisplay the consent UI
+ result.ViewModel = await BuildViewModelAsync(model.UserCode, model);
+ }
+
+ return result;
+ }
+
+ private async Task BuildViewModelAsync(string userCode, DeviceAuthorizationInputModel model = null)
+ {
+ var request = await _interaction.GetAuthorizationContextAsync(userCode);
+ if (request != null)
+ {
+ return CreateConsentViewModel(userCode, model, request);
+ }
+
+ return null;
+ }
+
+ private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, DeviceAuthorizationInputModel model, DeviceFlowAuthorizationRequest request)
+ {
+ var vm = new DeviceAuthorizationViewModel
+ {
+ UserCode = userCode,
+ Description = model?.Description,
+
+ RememberConsent = model?.RememberConsent ?? true,
+ ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(),
+
+ ClientName = request.Client.ClientName ?? request.Client.ClientId,
+ ClientUrl = request.Client.ClientUri,
+ ClientLogoUrl = request.Client.LogoUri,
+ AllowRememberConsent = request.Client.AllowRememberConsent
+ };
+
+ vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
+
+ var apiScopes = new List();
+ foreach (var parsedScope in request.ValidatedResources.ParsedScopes)
+ {
+ var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);
+ if (apiScope != null)
+ {
+ var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null);
+ apiScopes.Add(scopeVm);
+ }
+ }
+ if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
+ {
+ apiScopes.Add(GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null));
+ }
+ vm.ApiScopes = apiScopes;
+
+ return vm;
+ }
+
+ private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
+ {
+ return new ScopeViewModel
+ {
+ Value = identity.Name,
+ DisplayName = identity.DisplayName ?? identity.Name,
+ Description = identity.Description,
+ Emphasize = identity.Emphasize,
+ Required = identity.Required,
+ Checked = check || identity.Required
+ };
+ }
+
+ public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
+ {
+ return new ScopeViewModel
+ {
+ Value = parsedScopeValue.RawValue,
+ // todo: use the parsed scope value in the display?
+ DisplayName = apiScope.DisplayName ?? apiScope.Name,
+ Description = apiScope.Description,
+ Emphasize = apiScope.Emphasize,
+ Required = apiScope.Required,
+ Checked = check || apiScope.Required
+ };
+ }
+ private ScopeViewModel GetOfflineAccessScope(bool check)
+ {
+ return new ScopeViewModel
+ {
+ Value = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
+ DisplayName = ConsentOptions.OfflineAccessDisplayName,
+ Description = ConsentOptions.OfflineAccessDescription,
+ Emphasize = true,
+ Checked = check
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Diagnostics/DiagnosticsController.cs b/Quickstart/Diagnostics/DiagnosticsController.cs
new file mode 100644
index 0000000..57c2f55
--- /dev/null
+++ b/Quickstart/Diagnostics/DiagnosticsController.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ [SecurityHeaders]
+ [Authorize]
+ public class DiagnosticsController : Controller
+ {
+ public async Task Index()
+ {
+ var localAddresses = new string[] { "127.0.0.1", "::1", HttpContext.Connection.LocalIpAddress.ToString() };
+ if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString()))
+ {
+ return NotFound();
+ }
+
+ var model = new DiagnosticsViewModel(await HttpContext.AuthenticateAsync());
+ return View(model);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Diagnostics/DiagnosticsViewModel.cs b/Quickstart/Diagnostics/DiagnosticsViewModel.cs
new file mode 100644
index 0000000..f43c768
--- /dev/null
+++ b/Quickstart/Diagnostics/DiagnosticsViewModel.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using IdentityModel;
+using Microsoft.AspNetCore.Authentication;
+using Newtonsoft.Json;
+using System.Collections.Generic;
+using System.Text;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class DiagnosticsViewModel
+ {
+ public DiagnosticsViewModel(AuthenticateResult result)
+ {
+ AuthenticateResult = result;
+
+ if (result.Properties.Items.ContainsKey("client_list"))
+ {
+ var encoded = result.Properties.Items["client_list"];
+ var bytes = Base64Url.Decode(encoded);
+ var value = Encoding.UTF8.GetString(bytes);
+
+ Clients = JsonConvert.DeserializeObject(value);
+ }
+ }
+
+ public AuthenticateResult AuthenticateResult { get; }
+ public IEnumerable Clients { get; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Extensions.cs b/Quickstart/Extensions.cs
new file mode 100644
index 0000000..6c720b7
--- /dev/null
+++ b/Quickstart/Extensions.cs
@@ -0,0 +1,27 @@
+using System;
+using IdentityServer4.Models;
+using Microsoft.AspNetCore.Mvc;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public static class Extensions
+ {
+ ///
+ /// Checks if the redirect URI is for a native client.
+ ///
+ ///
+ public static bool IsNativeClient(this AuthorizationRequest context)
+ {
+ return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
+ && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
+ }
+
+ public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectUri)
+ {
+ controller.HttpContext.Response.StatusCode = 200;
+ controller.HttpContext.Response.Headers["Location"] = "";
+
+ return controller.View(viewName, new RedirectViewModel { RedirectUrl = redirectUri });
+ }
+ }
+}
diff --git a/Quickstart/Grants/GrantsController.cs b/Quickstart/Grants/GrantsController.cs
new file mode 100644
index 0000000..2431e0d
--- /dev/null
+++ b/Quickstart/Grants/GrantsController.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using IdentityServer4.Services;
+using IdentityServer4.Stores;
+using Microsoft.AspNetCore.Mvc;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using IdentityServer4.Events;
+using IdentityServer4.Extensions;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ ///
+ /// This sample controller allows a user to revoke grants given to clients
+ ///
+ [SecurityHeaders]
+ [Authorize]
+ public class GrantsController : Controller
+ {
+ private readonly IIdentityServerInteractionService _interaction;
+ private readonly IClientStore _clients;
+ private readonly IResourceStore _resources;
+ private readonly IEventService _events;
+
+ public GrantsController(IIdentityServerInteractionService interaction,
+ IClientStore clients,
+ IResourceStore resources,
+ IEventService events)
+ {
+ _interaction = interaction;
+ _clients = clients;
+ _resources = resources;
+ _events = events;
+ }
+
+ ///
+ /// Show list of grants
+ ///
+ [HttpGet]
+ public async Task Index()
+ {
+ return View("Index", await BuildViewModelAsync());
+ }
+
+ ///
+ /// Handle postback to revoke a client
+ ///
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task Revoke(string clientId)
+ {
+ await _interaction.RevokeUserConsentAsync(clientId);
+ await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), clientId));
+
+ return RedirectToAction("Index");
+ }
+
+ private async Task BuildViewModelAsync()
+ {
+ var grants = await _interaction.GetAllUserConsentsAsync();
+
+ var list = new List();
+ foreach(var grant in grants)
+ {
+ var client = await _clients.FindClientByIdAsync(grant.ClientId);
+ if (client != null)
+ {
+ var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes);
+
+ var item = new GrantViewModel()
+ {
+ ClientId = client.ClientId,
+ ClientName = client.ClientName ?? client.ClientId,
+ ClientLogoUrl = client.LogoUri,
+ ClientUrl = client.ClientUri,
+ Description = client.Description,
+ Created = grant.CreationTime,
+ Expires = grant.Expiration,
+ IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(),
+ ApiGrantNames = resources.ApiResources.Select(x => x.DisplayName ?? x.Name).ToArray()
+ };
+
+ list.Add(item);
+ }
+ }
+
+ return new GrantsViewModel
+ {
+ Grants = list
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Grants/GrantsViewModel.cs b/Quickstart/Grants/GrantsViewModel.cs
new file mode 100644
index 0000000..2114dee
--- /dev/null
+++ b/Quickstart/Grants/GrantsViewModel.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Collections.Generic;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class GrantsViewModel
+ {
+ public IEnumerable Grants { get; set; }
+ }
+
+ public class GrantViewModel
+ {
+ public string ClientId { get; set; }
+ public string ClientName { get; set; }
+ public string ClientUrl { get; set; }
+ public string ClientLogoUrl { get; set; }
+ public string Description { get; set; }
+ public DateTime Created { get; set; }
+ public DateTime? Expires { get; set; }
+ public IEnumerable IdentityGrantNames { get; set; }
+ public IEnumerable ApiGrantNames { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Home/ErrorViewModel.cs b/Quickstart/Home/ErrorViewModel.cs
new file mode 100644
index 0000000..a29a21a
--- /dev/null
+++ b/Quickstart/Home/ErrorViewModel.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using IdentityServer4.Models;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class ErrorViewModel
+ {
+ public ErrorViewModel()
+ {
+ }
+
+ public ErrorViewModel(string error)
+ {
+ Error = new ErrorMessage { Error = error };
+ }
+
+ public ErrorMessage Error { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/Home/HomeController.cs b/Quickstart/Home/HomeController.cs
new file mode 100644
index 0000000..871c637
--- /dev/null
+++ b/Quickstart/Home/HomeController.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using IdentityServer4.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System.Threading.Tasks;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ [SecurityHeaders]
+ [AllowAnonymous]
+ public class HomeController : Controller
+ {
+ private readonly IIdentityServerInteractionService _interaction;
+ private readonly ILogger _logger;
+ IHostingEnvironment _environment;
+
+ public HomeController(IIdentityServerInteractionService interaction, Microsoft.AspNetCore.Hosting.IHostingEnvironment environment, ILogger logger)
+ {
+ _interaction = interaction;
+ _environment = environment;
+ _logger = logger;
+ }
+
+ public IActionResult Index()
+ {
+ if (_environment.IsDevelopment())
+ {
+ // only show in development
+ return View();
+ }
+
+ _logger.LogInformation("Homepage is disabled in production. Returning 404.");
+ return NotFound();
+ }
+
+ ///
+ /// Shows the error page
+ ///
+ public async Task Error(string errorId)
+ {
+ var vm = new ErrorViewModel();
+
+ // retrieve error details from identityserver
+ var message = await _interaction.GetErrorContextAsync(errorId);
+ if (message != null)
+ {
+ vm.Error = message;
+
+ if (!_environment.IsDevelopment())
+ {
+ // only show in development
+ message.ErrorDescription = null;
+ }
+ }
+
+ return View("Error", vm);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Quickstart/SecurityHeadersAttribute.cs b/Quickstart/SecurityHeadersAttribute.cs
new file mode 100644
index 0000000..93ca67f
--- /dev/null
+++ b/Quickstart/SecurityHeadersAttribute.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class SecurityHeadersAttribute : ActionFilterAttribute
+ {
+ public override void OnResultExecuting(ResultExecutingContext context)
+ {
+ var result = context.Result;
+ if (result is ViewResult)
+ {
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
+ if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options"))
+ {
+ context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff");
+ }
+
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
+ if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options"))
+ {
+ context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
+ }
+
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+ var csp = "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';";
+ // also consider adding upgrade-insecure-requests once you have HTTPS in place for production
+ //csp += "upgrade-insecure-requests;";
+ // also an example if you need client images to be displayed from twitter
+ // csp += "img-src 'self' https://pbs.twimg.com;";
+
+ // once for standards compliant browsers
+ if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy"))
+ {
+ context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp);
+ }
+ // and once again for IE
+ if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy"))
+ {
+ context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp);
+ }
+
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
+ var referrer_policy = "no-referrer";
+ if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy"))
+ {
+ context.HttpContext.Response.Headers.Add("Referrer-Policy", referrer_policy);
+ }
+ }
+ }
+ }
+}
diff --git a/Quickstart/TestUsers.cs b/Quickstart/TestUsers.cs
new file mode 100644
index 0000000..8947d19
--- /dev/null
+++ b/Quickstart/TestUsers.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using IdentityModel;
+using IdentityServer4.Test;
+using System.Collections.Generic;
+using System.Security.Claims;
+using IdentityServer4;
+using Newtonsoft.Json;
+
+namespace IdentityServerHost.Quickstart.UI
+{
+ public class TestUsers
+ {
+ public static List Users
+ {
+ get
+ {
+ var address = new
+ {
+ street_address = "One Hacker Way",
+ locality = "Heidelberg",
+ postal_code = 69118,
+ country = "Germany"
+ };
+
+ return new List
+ {
+ new TestUser
+ {
+ SubjectId = "818727",
+ Username = "alice",
+ Password = "alice",
+ Claims =
+ {
+ new Claim(JwtClaimTypes.Name, "Alice Smith"),
+ new Claim(JwtClaimTypes.GivenName, "Alice"),
+ new Claim(JwtClaimTypes.FamilyName, "Smith"),
+ new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"),
+ new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
+ new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
+ new Claim(JwtClaimTypes.Address, JsonConvert.SerializeObject(address), IdentityServerConstants.ClaimValueTypes.Json)
+ }
+ },
+ new TestUser
+ {
+ SubjectId = "88421113",
+ Username = "bob",
+ Password = "bob",
+ Claims =
+ {
+ new Claim(JwtClaimTypes.Name, "Bob Smith"),
+ new Claim(JwtClaimTypes.GivenName, "Bob"),
+ new Claim(JwtClaimTypes.FamilyName, "Smith"),
+ new Claim(JwtClaimTypes.Email, "BobSmith@email.com"),
+ new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
+ new Claim(JwtClaimTypes.WebSite, "http://bob.com"),
+ new Claim(JwtClaimTypes.Address, JsonConvert.SerializeObject(address), IdentityServerConstants.ClaimValueTypes.Json)
+ }
+ }
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Startup.cs b/Startup.cs
index 6f8713e..a245eb5 100644
--- a/Startup.cs
+++ b/Startup.cs
@@ -34,13 +34,22 @@ namespace nuget_host
// if you are using API resources, you can specify the name here
options.Audience = "packages";
-
- });
+ });
services.AddMvc();
services.AddDataProtection();
+ services.AddIdentityServer()
+ .AddInMemoryClients(Config.Clients)
+ .AddInMemoryIdentityResources(Config.IdentityResources)
+ .AddInMemoryApiResources(Config.ApiResources)
+ .AddDeveloperSigningCredential()
+ .AddTestUsers(Config.TestUsers);
+
+
+
+
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -48,13 +57,12 @@ namespace nuget_host
{
if (env.IsDevelopment())
{
- app.UseDeveloperExceptionPage();
+ app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
-
ExternalUrl = Configuration["NuGet:ExternalUrl"];
SourceDir = Configuration["NuGet:SourceDir"];
RootApiKeySecret = Configuration["RootApiKeySecret"];
diff --git a/Views/Account/AccessDenied.cshtml b/Views/Account/AccessDenied.cshtml
new file mode 100644
index 0000000..0745189
--- /dev/null
+++ b/Views/Account/AccessDenied.cshtml
@@ -0,0 +1,7 @@
+
+
+
+
Access Denied
+
You do not have access to that resource.
+
+
\ No newline at end of file
diff --git a/Views/Account/LoggedOut.cshtml b/Views/Account/LoggedOut.cshtml
new file mode 100644
index 0000000..dc9fbf7
--- /dev/null
+++ b/Views/Account/LoggedOut.cshtml
@@ -0,0 +1,34 @@
+@model LoggedOutViewModel
+
+@{
+ // set this so the layout rendering sees an anonymous user
+ ViewData["signed-out"] = true;
+}
+
+
+ Invalid login request
+ There are no login schemes configured for this request.
+
+ }
+
+
\ No newline at end of file
diff --git a/Views/Account/Logout.cshtml b/Views/Account/Logout.cshtml
new file mode 100644
index 0000000..b49dee0
--- /dev/null
+++ b/Views/Account/Logout.cshtml
@@ -0,0 +1,15 @@
+@model LogoutViewModel
+
+
- Swapping to Development environment will display more detailed information about the error that occurred.
-
-
- Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application.
-
+@model ErrorViewModel
+
+@{
+ var error = Model?.Error?.Error;
+ var errorDescription = Model?.Error?.ErrorDescription;
+ var request_id = Model?.Error?.RequestId;
+}
+
+
+
+
Error
+
+
+
+
+
+ Sorry, there was an error
+
+ @if (error != null)
+ {
+
+
+ : @error
+
+
+
+ if (errorDescription != null)
+ {
+
` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `
`-`
` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `
` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `
`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$grays: map-merge(\n (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n ),\n $grays\n);\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$colors: map-merge(\n (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n ),\n $colors\n);\n\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-800 !default;\n\n$theme-colors: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$theme-colors: map-merge(\n (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n ),\n $theme-colors\n);\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n// The yiq lightness value that determines when the lightness of color changes from \"dark\" to \"light\". Acceptable values are between 0 and 255.\n$yiq-contrasted-threshold: 150 !default;\n\n// Customize the light and dark text colors for use in our YIQ color contrast function.\n$yiq-text-dark: $gray-900 !default;\n$yiq-text-light: $white !default;\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\",\"%3c\"),\n (\">\",\"%3e\"),\n (\"#\",\"%23\"),\n) !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-prefers-reduced-motion-media-query: true !default;\n$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS\n$enable-grid-classes: true !default;\n$enable-pointer-cursor-for-buttons: true !default;\n$enable-print-styles: true !default;\n$enable-responsive-font-sizes: false !default;\n$enable-validation-icons: true !default;\n$enable-deprecation-messages: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$spacers: map-merge(\n (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n ),\n $spacers\n);\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$sizes: map-merge(\n (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%,\n auto: auto\n ),\n $sizes\n);\n\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n// Darken percentage for links with `.text-*` class (e.g. `.text-success`)\n$emphasized-link-hover-darken-percentage: 15% !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n$grid-row-columns: 6 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n$border-color: $gray-300 !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$rounded-pill: 50rem !default;\n\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n$embed-responsive-aspect-ratios: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$embed-responsive-aspect-ratios: join(\n (\n (21 9),\n (16 9),\n (4 3),\n (1 1),\n ),\n $embed-responsive-aspect-ratios\n);\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: -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\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n// stylelint-enable value-keyword-case\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: $font-size-base * 1.25 !default;\n$font-size-sm: $font-size-base * .875 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n\n$headings-margin-bottom: $spacer / 2 !default;\n$headings-font-family: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-small-font-size: $small-font-size !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n\n$hr-border-color: rgba($black, .1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n\n$hr-margin-y: $spacer !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-color: $body-color !default;\n$table-bg: null !default;\n$table-accent-bg: rgba($black, .05) !default;\n$table-hover-color: $table-color !default;\n$table-hover-bg: rgba($black, .075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n\n$table-dark-color: $white !default;\n$table-dark-bg: $gray-800 !default;\n$table-dark-accent-bg: rgba($white, .05) !default;\n$table-dark-hover-color: $table-dark-color !default;\n$table-dark-hover-bg: rgba($white, .075) !default;\n$table-dark-border-color: lighten($table-dark-bg, 7.5%) !default;\n\n$table-striped-order: odd !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-level: -9 !default;\n$table-border-level: -6 !default;\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .2rem !default;\n$input-btn-focus-color: rgba($component-active-bg, .25) !default;\n$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n$input-btn-line-height-sm: $line-height-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n$input-btn-line-height-lg: $line-height-lg !default;\n\n$input-btn-border-width: $border-width !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n$btn-line-height-sm: $input-btn-line-height-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n$btn-line-height-lg: $input-btn-line-height-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n\n// Forms\n\n$label-margin-bottom: .5rem !default;\n\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n$input-line-height-sm: $input-btn-line-height-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n$input-line-height-lg: $input-btn-line-height-lg !default;\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten($component-active-bg, 25%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y / 2) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height-lg * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .3rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n$form-check-inline-input-margin-x: .3125rem !default;\n\n$form-grid-gutter-width: 10px !default;\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$custom-control-gutter: .5rem !default;\n$custom-control-spacer-x: 1rem !default;\n$custom-control-cursor: null !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: $input-bg !default;\n\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: $input-box-shadow !default;\n$custom-control-indicator-border-color: $gray-500 !default;\n$custom-control-indicator-border-width: $input-border-width !default;\n\n$custom-control-label-color: null !default;\n\n$custom-control-indicator-disabled-bg: $input-disabled-bg !default;\n$custom-control-label-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $component-active-color !default;\n$custom-control-indicator-checked-bg: $component-active-bg !default;\n$custom-control-indicator-checked-disabled-bg: rgba(theme-color(\"primary\"), .5) !default;\n$custom-control-indicator-checked-box-shadow: none !default;\n$custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg !default;\n\n$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-control-indicator-focus-border-color: $input-focus-border-color !default;\n\n$custom-control-indicator-active-color: $component-active-color !default;\n$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-control-indicator-active-box-shadow: none !default;\n$custom-control-indicator-active-border-color: $custom-control-indicator-active-bg !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: url(\"data:image/svg+xml,\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: none !default;\n$custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-switch-width: $custom-control-indicator-size * 1.75 !default;\n$custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 !default;\n$custom-switch-indicator-size: subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;\n\n$custom-select-padding-y: $input-padding-y !default;\n$custom-select-padding-x: $input-padding-x !default;\n$custom-select-font-family: $input-font-family !default;\n$custom-select-font-size: $input-font-size !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-font-weight: $input-font-weight !default;\n$custom-select-line-height: $input-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $input-bg !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: $gray-800 !default;\n$custom-select-indicator: url(\"data:image/svg+xml,\") !default;\n$custom-select-background: escape-svg($custom-select-indicator) no-repeat right $custom-select-padding-x center / $custom-select-bg-size !default; // Used so we can have multiple background elements (e.g., arrow and feedback icon)\n\n$custom-select-feedback-icon-padding-right: add(1em * .75, (2 * $custom-select-padding-y * .75) + $custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$custom-select-border-width: $input-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;\n\n$custom-select-focus-border-color: $input-focus-border-color !default;\n$custom-select-focus-width: $input-focus-width !default;\n$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width $input-btn-focus-color !default;\n\n$custom-select-padding-y-sm: $input-padding-y-sm !default;\n$custom-select-padding-x-sm: $input-padding-x-sm !default;\n$custom-select-font-size-sm: $input-font-size-sm !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-select-padding-y-lg: $input-padding-y-lg !default;\n$custom-select-padding-x-lg: $input-padding-x-lg !default;\n$custom-select-font-size-lg: $input-font-size-lg !default;\n$custom-select-height-lg: $input-height-lg !default;\n\n$custom-range-track-width: 100% !default;\n$custom-range-track-height: .5rem !default;\n$custom-range-track-cursor: pointer !default;\n$custom-range-track-bg: $gray-300 !default;\n$custom-range-track-border-radius: 1rem !default;\n$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;\n\n$custom-range-thumb-width: 1rem !default;\n$custom-range-thumb-height: $custom-range-thumb-width !default;\n$custom-range-thumb-bg: $component-active-bg !default;\n$custom-range-thumb-border: 0 !default;\n$custom-range-thumb-border-radius: 1rem !default;\n$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$custom-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in IE/Edge\n$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-range-thumb-disabled-bg: $gray-500 !default;\n\n$custom-file-height: $input-height !default;\n$custom-file-height-inner: $input-height-inner !default;\n$custom-file-focus-border-color: $input-focus-border-color !default;\n$custom-file-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-file-disabled-bg: $input-disabled-bg !default;\n\n$custom-file-padding-y: $input-padding-y !default;\n$custom-file-padding-x: $input-padding-x !default;\n$custom-file-line-height: $input-line-height !default;\n$custom-file-font-family: $input-font-family !default;\n$custom-file-font-weight: $input-font-weight !default;\n$custom-file-color: $input-color !default;\n$custom-file-bg: $input-bg !default;\n$custom-file-border-width: $input-border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $input-border-radius !default;\n$custom-file-box-shadow: $input-box-shadow !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $input-group-addon-bg !default;\n$custom-file-text: (\n en: \"Browse\"\n) !default;\n\n\n// Form validation\n\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $small-font-size !default;\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n\n$form-validation-states: () !default;\n// stylelint-disable-next-line scss/dollar-variable-default\n$form-validation-states: map-merge(\n (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n ),\n ),\n $form-validation-states\n);\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-divider-color: $gray-200 !default;\n$nav-divider-margin-y: $spacer / 2 !default;\n\n\n// Navbar\n\n$navbar-padding-y: $spacer / 2 !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white, .5) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .5) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-divider-margin-y: $nav-divider-margin-y !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-color: null !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: $border-width !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black, .125) !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-group-margin: $grid-gutter-width / 2 !default;\n$card-deck-margin: $card-group-margin !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: .25rem !default;\n$tooltip-padding-x: .5rem !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n// Form tooltips must come after regular tooltips\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: $line-height-base !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n\n\n// Popovers\n\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: .75rem !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $popover-header-padding-y !default;\n$popover-body-padding-x: $popover-header-padding-x !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Toasts\n\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .25rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: .25rem !default;\n$toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n\n\n// Badges\n\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n$badge-border-radius: $border-radius !default;\n\n$badge-transition: $btn-transition !default;\n$badge-focus-width: $input-btn-focus-width !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 1rem !default;\n\n// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;\n$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: 1rem !default;\n$modal-header-padding-x: 1rem !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-xl: 1140px !default;\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n$alert-bg-level: -10 !default;\n$alert-border-level: -9 !default;\n$alert-color-level: 6 !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n\n// List group\n\n$list-group-color: null !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-font-size: null !default;\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-margin-bottom: 1rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n\n$breadcrumb-border-radius: $border-radius !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n\n// Spinners\n\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-border-width: .25em !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n\n// Code\n\n$code-font-size: 87.5% !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n\n\n// Utilities\n\n$displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;\n$overflows: auto, hidden !default;\n$positions: static, relative, absolute, fixed, sticky !default;\n\n\n// Printing\n\n$print-page-size: a3 !default;\n$print-body-min-width: map-get($grid-breakpoints, \"lg\") !default;\n","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover() {\n &:hover { @content; }\n}\n\n@mixin hover-focus() {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus() {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active() {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n"]}
\ No newline at end of file
diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css b/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
new file mode 100644
index 0000000..5308df6
--- /dev/null
+++ b/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
@@ -0,0 +1,8 @@
+/*!
+ * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)
+ * Copyright 2011-2019 The Bootstrap Authors
+ * Copyright 2011-2019 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
+ */*,::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]){color:inherit;text-decoration:none}a:not([href]):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}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
+/*# sourceMappingURL=bootstrap-reboot.min.css.map */
\ No newline at end of file
diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map b/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
new file mode 100644
index 0000000..b8551f7
--- /dev/null
+++ b/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../../scss/bootstrap-reboot.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","../../scss/vendor/_rfs.scss","bootstrap-reboot.css","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ACkBA,ECTA,QADA,SDaE,WAAA,WAGF,KACE,YAAA,WACA,YAAA,KACA,yBAAA,KACA,4BAAA,YAMF,QAAA,MAAA,WAAA,OAAA,OAAA,OAAA,OAAA,KAAA,IAAA,QACE,QAAA,MAUF,KACE,OAAA,EACA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBEgFI,UAAA,KF9EJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,KACA,iBAAA,KGlBF,0CH+BE,QAAA,YASF,GACE,WAAA,YACA,OAAA,EACA,SAAA,QAaF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAOF,EACE,WAAA,EACA,cAAA,KC9CF,0BDyDA,YAEE,gBAAA,UACA,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,cAAA,EACA,iCAAA,KAAA,yBAAA,KAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QCnDF,GDsDA,GCvDA,GD0DE,WAAA,EACA,cAAA,KAGF,MCtDA,MACA,MAFA,MD2DE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAGF,ECvDA,ODyDE,YAAA,OAGF,MExFI,UAAA,IFiGJ,IC5DA,ID8DE,SAAA,SEnGE,UAAA,IFqGF,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAON,EACE,MAAA,QACA,gBAAA,KACA,iBAAA,YIhLA,QJmLE,MAAA,QACA,gBAAA,UASJ,cACE,MAAA,QACA,gBAAA,KI/LA,oBJkME,MAAA,QACA,gBAAA,KC7DJ,KACA,IDqEA,ICpEA,KDwEE,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UEpJE,UAAA,IFwJJ,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAQF,OAEE,OAAA,EAAA,EAAA,KAQF,IACE,eAAA,OACA,aAAA,KAGF,IAGE,SAAA,OACA,eAAA,OAQF,MACE,gBAAA,SAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAGF,GAGE,WAAA,QAQF,MAEE,QAAA,aACA,cAAA,MAMF,OAEE,cAAA,EAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBCxGF,OD2GA,MCzGA,SADA,OAEA,SD6GE,OAAA,EACA,YAAA,QErPE,UAAA,QFuPF,YAAA,QAGF,OC3GA,MD6GE,SAAA,QAGF,OC3GA,OD6GE,eAAA,KAMF,OACE,UAAA,OC3GF,cACA,aACA,cDgHA,OAIE,mBAAA,OC/GF,6BACA,4BACA,6BDkHE,sBAKI,OAAA,QClHN,gCACA,+BACA,gCDsHA,yBAIE,QAAA,EACA,aAAA,KCrHF,qBDwHA,kBAEE,WAAA,WACA,QAAA,EAIF,iBCxHA,2BACA,kBAFA,iBDkIE,mBAAA,QAGF,SACE,SAAA,KAEA,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAKF,OACE,QAAA,MACA,MAAA,KACA,UAAA,KACA,QAAA,EACA,cAAA,MEjSI,UAAA,OFmSJ,YAAA,QACA,MAAA,QACA,YAAA,OAGF,SACE,eAAA,SGvIF,yCFGA,yCD0IE,OAAA,KGxIF,cHgJE,eAAA,KACA,mBAAA,KG5IF,yCHoJE,mBAAA,KAQF,6BACE,KAAA,QACA,mBAAA,OAOF,OACE,QAAA,aAGF,QACE,QAAA,UACA,OAAA,QAGF,SACE,QAAA,KGzJF,SH+JE,QAAA","sourcesContent":["/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `
` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `
`-`
` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `
` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `