diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Config.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Config.cs new file mode 100644 index 0000000..81c38e2 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Config.cs @@ -0,0 +1,111 @@ +// 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.Models; +using IdentityServer4.Stores; +using System.Collections.Generic; + +namespace OidcSamples.AuthorizationServer +{ + public static class Config + { + public static IEnumerable IdentityResources => + new IdentityResource[] + { + new IdentityResources.OpenId(), + new IdentityResources.Profile(), + new IdentityResources.Email(), + new IdentityResources.Address(), + }; + + private const string TrafficPoliceApi = "traffic-police-api"; + + public static IEnumerable ApiScopes => + new ApiScope[] + { + new ApiScope( + TrafficPoliceApi, + "Traffic Police API scope"), + }; + + public static IEnumerable ApiResources => + new ApiResource[] { + new ApiResource(TrafficPoliceApi, "Traffic Police API") + { + // This will make sure that `traffic-police-api` will be in the + // list of audiences when this scope is requested + Scopes = new List{ TrafficPoliceApi }, + }, + }; + + public static IEnumerable Clients => + new Client[] + { + new Client + { + // IdentityTokenLifetime = + // AuthorizationCodeLifetime = + AccessTokenLifetime = 60 * 60 * 8, + AllowOfflineAccess = true, + UpdateAccessTokenClaimsOnRefresh = true, + ClientName = "Traffic Police React App", + ClientId = "traffic-police-react-app", + AllowedGrantTypes = GrantTypes.Code, + RedirectUris = + { + "https://localhost:3000/signin-oidc" + }, + AllowedScopes = + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Email, + IdentityServerConstants.StandardScopes.Address, + "traffic-police-api", + }, + RequirePkce = true, + PostLogoutRedirectUris = + { + "https://localhost:3000/signout-callback-oidc" + }, + + RequireConsent = false, + }, + new Client + { + AccessTokenLifetime = 60 * 60 * 8, + AllowOfflineAccess = true, + UpdateAccessTokenClaimsOnRefresh = true, + ClientName = "Tax ASP.NET Core Server Side App", + ClientId = "tax-asp-net-core-app", + AllowedGrantTypes = GrantTypes.Code, + RedirectUris = + { + "https://localhost:7001/signin-oidc" + }, + AllowedScopes = + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Address, + IdentityServerConstants.StandardScopes.Email, + "traffic-police-api", + }, + ClientSecrets = + { + new Secret("secret".Sha256()) + }, + RequirePkce = true, + PostLogoutRedirectUris = + { + "https://localhost:7001/signout-callback-oidc" + }, + + RequireConsent = false, + } + }; + } +} \ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/OidcSamples.AuthorizationServer.csproj b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/OidcSamples.AuthorizationServer.csproj new file mode 100644 index 0000000..19aade1 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/OidcSamples.AuthorizationServer.csproj @@ -0,0 +1,14 @@ + + + + netcoreapp3.1 + + + + + + + + + + diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Program.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Program.cs new file mode 100644 index 0000000..eeb934c --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Program.cs @@ -0,0 +1,53 @@ +// 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.Hosting; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; +using System; + +namespace OidcSamples.AuthorizationServer +{ + public class Program + { + public static int Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) + .MinimumLevel.Override("System", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code) + .CreateLogger(); + + try + { + Log.Information("Starting host..."); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly."); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSerilog() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} \ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Properties/launchSettings.json b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Properties/launchSettings.json new file mode 100644 index 0000000..59dcdbe --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "SelfHost": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:10000" + } + } +} \ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/AccountController.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/AccountController.cs new file mode 100644 index 0000000..2d51c3c --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/AccountController.cs @@ -0,0 +1,368 @@ +// 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. + 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?.Client.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?.Client.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 View(); + } + + + /*****************************************/ + /* 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?.Client.ClientId != null) + { + var client = await _clientStore.FindEnabledClientByIdAsync(context.Client.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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/AccountOptions.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/AccountOptions.cs new file mode 100644 index 0000000..a89c229 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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 = true; + + public static string InvalidCredentialsErrorMessage = "Invalid username or password"; + } +} diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/ExternalController.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/ExternalController.cs new file mode 100644 index 0000000..1a7479e --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/ExternalProvider.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/ExternalProvider.cs new file mode 100644 index 0000000..0584aa6 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/LoggedOutViewModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/LoggedOutViewModel.cs new file mode 100644 index 0000000..6368832 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/LoginInputModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/LoginInputModel.cs new file mode 100644 index 0000000..bcb7853 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/LoginViewModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/LoginViewModel.cs new file mode 100644 index 0000000..9bf6c8f --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/LogoutInputModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/LogoutInputModel.cs new file mode 100644 index 0000000..300b5f0 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/LogoutViewModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/LogoutViewModel.cs new file mode 100644 index 0000000..236cd6c --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/RedirectViewModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Account/RedirectViewModel.cs new file mode 100644 index 0000000..904ae20 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ConsentController.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ConsentController.cs new file mode 100644 index 0000000..6c3aa44 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ConsentController.cs @@ -0,0 +1,262 @@ +// 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 = new ConsentResponse { Error = AuthorizationError.AccessDenied }; + + // emit event + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); + } + // 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, + ScopesValuesConsented = scopes.ToArray(), + Description = model.Description + }; + + // emit event + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, 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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ConsentInputModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ConsentInputModel.cs new file mode 100644 index 0000000..ee2d5eb --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ConsentOptions.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ConsentOptions.cs new file mode 100644 index 0000000..3635d0e --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ConsentViewModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ConsentViewModel.cs new file mode 100644 index 0000000..44c41bd --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ProcessConsentResult.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ProcessConsentResult.cs new file mode 100644 index 0000000..1d331df --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ScopeViewModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Consent/ScopeViewModel.cs new file mode 100644 index 0000000..fa57988 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Device/DeviceAuthorizationInputModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Device/DeviceAuthorizationInputModel.cs new file mode 100644 index 0000000..a221181 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Device/DeviceAuthorizationViewModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Device/DeviceAuthorizationViewModel.cs new file mode 100644 index 0000000..3e8857f --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Device/DeviceController.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Device/DeviceController.cs new file mode 100644 index 0000000..d7d07ae --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Device/DeviceController.cs @@ -0,0 +1,232 @@ +// 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 = new ConsentResponse { Error = AuthorizationError.AccessDenied }; + + // emit event + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); + } + // 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, + ScopesValuesConsented = scopes.ToArray(), + Description = model.Description + }; + + // emit event + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, 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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Diagnostics/DiagnosticsController.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Diagnostics/DiagnosticsController.cs new file mode 100644 index 0000000..57c2f55 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Diagnostics/DiagnosticsViewModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Diagnostics/DiagnosticsViewModel.cs new file mode 100644 index 0000000..f43c768 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Extensions.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Extensions.cs new file mode 100644 index 0000000..6c720b7 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Grants/GrantsController.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Grants/GrantsController.cs new file mode 100644 index 0000000..128ce59 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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.GetAllUserGrantsAsync(); + + 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 = grant.Description, + Created = grant.CreationTime, + Expires = grant.Expiration, + IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(), + ApiGrantNames = resources.ApiScopes.Select(x => x.DisplayName ?? x.Name).ToArray() + }; + + list.Add(item); + } + } + + return new GrantsViewModel + { + Grants = list + }; + } + } +} \ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Grants/GrantsViewModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Grants/GrantsViewModel.cs new file mode 100644 index 0000000..f86354f --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Home/ErrorViewModel.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Home/ErrorViewModel.cs new file mode 100644 index 0000000..7d2cbe3 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Home/HomeController.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Home/HomeController.cs new file mode 100644 index 0000000..9cf0678 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/Home/HomeController.cs @@ -0,0 +1,65 @@ +// 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.Hosting; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; + +namespace IdentityServerHost.Quickstart.UI +{ + [SecurityHeaders] + [AllowAnonymous] + public class HomeController : Controller + { + private readonly IIdentityServerInteractionService _interaction; + private readonly IWebHostEnvironment _environment; + private readonly ILogger _logger; + + public HomeController(IIdentityServerInteractionService interaction, IWebHostEnvironment 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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/SecurityHeadersAttribute.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/SecurityHeadersAttribute.cs new file mode 100644 index 0000000..382c340 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/TestUsers.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/TestUsers.cs new file mode 100644 index 0000000..f16672b --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Quickstart/TestUsers.cs @@ -0,0 +1,38 @@ +// 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; + +namespace IdentityServerHost.Quickstart.UI +{ + public class TestUsers + { + public static List Users + { + get + { + return new List + { + new TestUser + { + SubjectId = "3199711031234", + Username = "3199711031234", + Password = "123", + Claims = + { + new Claim(JwtClaimTypes.Name, "Muhammad Azeez"), + new Claim(JwtClaimTypes.GivenName, "Muhammad"), + new Claim(JwtClaimTypes.FamilyName, "Azeez"), + new Claim(JwtClaimTypes.Email, "muhammad-azeez@outlook.com"), + new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), + } + } + }; + } + } + } +} \ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Startup.cs b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Startup.cs new file mode 100644 index 0000000..fa12954 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Startup.cs @@ -0,0 +1,60 @@ +// 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 IdentityServerHost.Quickstart.UI; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace OidcSamples.AuthorizationServer +{ + public class Startup + { + public IWebHostEnvironment Environment { get; } + + public Startup(IWebHostEnvironment environment) + { + Environment = environment; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllersWithViews(); + + var builder = services.AddIdentityServer(options => + { + // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html + options.EmitStaticAudienceClaim = true; + }) + .AddInMemoryIdentityResources(Config.IdentityResources) + .AddInMemoryApiResources(Config.ApiResources) + .AddInMemoryApiScopes(Config.ApiScopes) + .AddInMemoryClients(Config.Clients) + .AddTestUsers(TestUsers.Users); + + // not recommended for production - you need to store your key material somewhere secure + builder.AddDeveloperSigningCredential(); + } + + public void Configure(IApplicationBuilder app) + { + if (Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseStaticFiles(); + app.UseRouting(); + + app.UseIdentityServer(); + + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + } + } +} diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/AccessDenied.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/AccessDenied.cshtml new file mode 100644 index 0000000..32e6c53 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/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/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/LoggedOut.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/LoggedOut.cshtml new file mode 100644 index 0000000..3cc190b --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/LoggedOut.cshtml @@ -0,0 +1,34 @@ +@model LoggedOutViewModel + +@{ + // set this so the layout rendering sees an anonymous user + ViewData["signed-out"] = true; +} + +
+

+ Logout + You are now logged out +

+ + @if (Model.PostLogoutRedirectUri != null) + { +
+ Click here to return to the + @Model.ClientName application. +
+ } + + @if (Model.SignOutIframeUrl != null) + { + + } +
+ +@section scripts +{ + @if (Model.AutomaticRedirectAfterSignOut) + { + + } +} diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/Login.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/Login.cshtml new file mode 100644 index 0000000..e4ccb1d --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/Login.cshtml @@ -0,0 +1,87 @@ +@model LoginViewModel + + \ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/Logout.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/Logout.cshtml new file mode 100644 index 0000000..e74bde6 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Account/Logout.cshtml @@ -0,0 +1,15 @@ +@model LogoutViewModel + +
+
+

Logout

+

Would you like to logut of IdentityServer?

+
+ +
+ +
+ +
+
+
diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Consent/Index.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Consent/Index.cshtml new file mode 100644 index 0000000..f8aa10d --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Consent/Index.cshtml @@ -0,0 +1,104 @@ +@model ConsentViewModel + + diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Device/Success.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Device/Success.cshtml new file mode 100644 index 0000000..050dd91 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Device/Success.cshtml @@ -0,0 +1,7 @@ + +
+
+

Success

+

You have successfully authorized the device

+
+
diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Device/UserCodeCapture.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Device/UserCodeCapture.cshtml new file mode 100644 index 0000000..6d41261 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Device/UserCodeCapture.cshtml @@ -0,0 +1,23 @@ +@model string + +
+
+

User Code

+

Please enter the code displayed on your device.

+
+ + + +
+
+
+
+ + +
+ + +
+
+
+
diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Device/UserCodeConfirmation.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Device/UserCodeConfirmation.cshtml new file mode 100644 index 0000000..e1d3b19 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Device/UserCodeConfirmation.cshtml @@ -0,0 +1,108 @@ +@model DeviceAuthorizationViewModel + +
+
+ @if (Model.ClientLogoUrl != null) + { + + } +

+ @Model.ClientName + is requesting your permission +

+ @if (Model.ConfirmUserCode) + { +

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

+ } +

Uncheck the permissions you do not wish to grant.

+
+ +
+
+ +
+
+ +
+ +
+
+ @if (Model.IdentityScopes.Any()) + { +
+
+
+ + Personal Information +
+
    + @foreach (var scope in Model.IdentityScopes) + { + + } +
+
+
+ } + + @if (Model.ApiScopes.Any()) + { +
+
+
+ + Application Access +
+
    + @foreach (var scope in Model.ApiScopes) + { + + } +
+
+
+ } + +
+
+
+ + Description +
+
+ +
+
+
+ + @if (Model.AllowRememberConsent) + { +
+
+ + +
+
+ } +
+
+ +
+
+ + +
+
+ @if (Model.ClientUrl != null) + { + + + @Model.ClientName + + } +
+
+
+
diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Diagnostics/Index.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Diagnostics/Index.cshtml new file mode 100644 index 0000000..e939c0d --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Diagnostics/Index.cshtml @@ -0,0 +1,64 @@ +@model DiagnosticsViewModel + +
+
+

Authentication Cookie

+
+ +
+
+
+
+

Claims

+
+
+
+ @foreach (var claim in Model.AuthenticateResult.Principal.Claims) + { +
@claim.Type
+
@claim.Value
+ } +
+
+
+
+ +
+
+
+

Properties

+
+
+
+ @foreach (var prop in Model.AuthenticateResult.Properties.Items) + { +
@prop.Key
+
@prop.Value
+ } + @if (Model.Clients.Any()) + { +
Clients
+
+ @{ + var clients = Model.Clients.ToArray(); + for(var i = 0; i < clients.Length; i++) + { + @clients[i] + if (i < clients.Length - 1) + { + , + } + } + } +
+ } +
+
+
+
+
+
+ + + + diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Grants/Index.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Grants/Index.cshtml new file mode 100644 index 0000000..0cfc7ec --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Grants/Index.cshtml @@ -0,0 +1,87 @@ +@model GrantsViewModel + +
+
+

Client Application Permissions

+

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

+
+ + @if (Model.Grants.Any() == false) + { +
+
+
+ You have not given access to any applications +
+
+
+ } + else + { + foreach (var grant in Model.Grants) + { +
+
+
+
+ @if (grant.ClientLogoUrl != null) + { + + } + @grant.ClientName +
+ +
+
+ + +
+
+
+
+ +
    + @if (grant.Description != null) + { +
  • + @grant.Description +
  • + } +
  • + @grant.Created.ToString("yyyy-MM-dd") +
  • + @if (grant.Expires.HasValue) + { +
  • + @grant.Expires.Value.ToString("yyyy-MM-dd") +
  • + } + @if (grant.IdentityGrantNames.Any()) + { +
  • + +
      + @foreach (var name in grant.IdentityGrantNames) + { +
    • @name
    • + } +
    +
  • + } + @if (grant.ApiGrantNames.Any()) + { +
  • + +
      + @foreach (var name in grant.ApiGrantNames) + { +
    • @name
    • + } +
    +
  • + } +
+
+ } + } +
\ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Home/Index.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Home/Index.cshtml new file mode 100644 index 0000000..36b2bfc --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Home/Index.cshtml @@ -0,0 +1,32 @@ +@using System.Diagnostics + +@{ + var version = FileVersionInfo.GetVersionInfo(typeof(IdentityServer4.Hosting.IdentityServerMiddleware).Assembly.Location).ProductVersion.Split('+').First(); +} + +
+

+ + Welcome to IdentityServer4 + (version @version) +

+ + +
diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/Error.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/Error.cshtml new file mode 100644 index 0000000..4c746e7 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/Error.cshtml @@ -0,0 +1,40 @@ +@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) + { +
@errorDescription
+ } + } +
+ + @if (request_id != null) + { +
Request Id: @request_id
+ } +
+
+
diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/Redirect.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/Redirect.cshtml new file mode 100644 index 0000000..ecc31c1 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/Redirect.cshtml @@ -0,0 +1,11 @@ +@model RedirectViewModel + +
+
+

You are now being returned to the application

+

Once complete, you may close this tab.

+
+
+ + + diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_Layout.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..64ba125 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_Layout.cshtml @@ -0,0 +1,28 @@ + + + + + + + + IdentityServer4 + + + + + + + + + + +
+ @RenderBody() +
+ + + + + @RenderSection("scripts", required: false) + + diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_Nav.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_Nav.cshtml new file mode 100644 index 0000000..7ab1f86 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_Nav.cshtml @@ -0,0 +1,33 @@ +@using IdentityServer4.Extensions + +@{ + string name = null; + if (!true.Equals(ViewData["signed-out"])) + { + name = Context.User?.GetDisplayName(); + } +} + + diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_ScopeListItem.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_ScopeListItem.cshtml new file mode 100644 index 0000000..162029e --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_ScopeListItem.cshtml @@ -0,0 +1,34 @@ +@model ScopeViewModel + +
  • + + @if (Model.Required) + { + (required) + } + @if (Model.Description != null) + { + + } +
  • \ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_ValidationSummary.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_ValidationSummary.cshtml new file mode 100644 index 0000000..5fd90ba --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/Shared/_ValidationSummary.cshtml @@ -0,0 +1,7 @@ +@if (ViewContext.ModelState.IsValid == false) +{ +
    + Error +
    +
    +} \ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/_ViewImports.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/_ViewImports.cshtml new file mode 100644 index 0000000..bb20544 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using IdentityServerHost.Quickstart.UI +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/_ViewStart.cshtml b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/_ViewStart.cshtml new file mode 100644 index 0000000..820a2f6 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/tempkey.jwk b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/tempkey.jwk new file mode 100644 index 0000000..7f9ca46 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/tempkey.jwk @@ -0,0 +1 @@ +{"alg":"RS256","d":"K_F5s3MAkSWSl_lLgKeqYwp9UYU_FNjwejyRrs16Pa4vgHDpflUADPninzZFe23MYMcTNsWbbZPYB6nwLv9SMRa3-W940A75gEh48LY48ZrLe-OFta8jFFEdXnRsLyA8gRfM0-Hz9Ud0OyMehzqqfCtKTVTIcoG70J2L1DhYvTkofbd766REInuBBRQCb7ouj9l8K5almO-zPgTHkKPYV5tji5VCs0seEe2vVMk3xPR8hPQloejdYq0cVMbdp9p34CajRx2OxbxmbggNU_M1FtR0125eBIKWf9iFjTiLquhLcnH8BrDs29H0kYZWNmY6kTh4OOfTTfCzukovrEDf3Q","dp":"tIBdNNX4bEvNISiGn7e4_BUudu9Dk5_X3TSeUhd6fouhOXWZPTFF7xccEV6rBGyQB1Gpgi0iB-OPLkUYOJF4Rlr6AnO3tGV-wmPpLH9O5sKNx54RgbogRjxjgRstrdICIo7wwRp_eO3ZyUnpC2uC1TnaGJoGdD0o3cydj6aFbF0","dq":"v6Ok_Z75FPd-vzd2ACxrjgymGapFMkM--hutJAn4X0XuoIMpkO-G4KSVy4M5P0lsIUs7-o-fRZ-xcqW8WyfFz_gh9Jwa_j5AiI05T2oCCB-NCQXZEYRG1Grf1b0RBQADoLALkXeihNw_zke4IkY7vrbGrjKISAs63r88EWKE_3E","e":"AQAB","kid":"4B936CF39842424A431BF2C03131729D","kty":"RSA","n":"uG6oMJrUcTj_2FLQ_wTi4On882bio5WnfTBpX52E3RIBDzy9ktIeOyZ4pg7dYuVr3mpE2rdLMCvl5FNY22pvpzjsNuEy5a7wO5PANkjqxCwWFASNE_dKZ9iMuSfuWiTQ4zuPVPPhwYcAKEEUpXkJLZ9HvHCPmqb3fzoolsRB_0iZiyORmo120RU4uOWz6PPwAd_llyhGvMtCwr93sPxYDkX0XTP2wv18X7cc79RrwKvZfIFIMGnazNM6_JWgx3atmAfap3sPVCvwyqfV3BbZBN8t7IVmQWcxuWdkC2UG-QMMveVaBhs05kn5wUTVh7jjHTmAUkNN9DcTmYEy7c9oMQ","p":"36mokmJSKNBei0pksonfZS5aTMs5fKcQJHV_zhOsbbqNG5fj0vCdDsSMVJTyEL7D-Ijsr8pt5csBLiD24R4oo0Rk73vaSRLwypsUOXcxVP3r2xP09pk2eUe2LxlKCAiEwrzN9j0MPjaNiyy0vaSglUumSxUR3xWkJYxLwrkYg-c","q":"0xj56aR3DMXx_AigySznKFgSEIY-Ju8I-Q7eBhbFsZQKxH3BF-TXAOjuIplWlVYSJv940Dyx2V-fJSw6haVV2Ts_1vXGWjoH8kpKrqyIHZwybnCSDUaRTN9rkqkuYD_s3Tb041cAtTHUVCkyZzELI8abREym_pqRyafsy3_oMCc","qi":"YBBUxFykVD9vV5bI_Kwj6Jc6dOI42_1niMjyg0phBeMXRXtAJtTrOXFCkMuDAmIGYpeKqC5fqkz74Fmmu6appY2Dx6UBXT2H_ytlydWfa6cRAX5Nvr8ttzOlNCDKlIGFwe2gRMw9mU4_nfCjfb1O4O-_VcWlK4t8YBeUaMqyiFM"} \ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/css/site.css b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/css/site.css new file mode 100644 index 0000000..e05e77d --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/css/site.css @@ -0,0 +1,24 @@ +.body-container { + margin-top: 60px; + padding-bottom: 40px; } + +.welcome-page li { + list-style: none; + padding: 4px; } + +.logged-out-page iframe { + display: none; + width: 0; + height: 0; } + +.grants-page .card { + margin-top: 20px; + border-bottom: 1px solid lightgray; } + .grants-page .card .card-title { + font-size: 120%; + font-weight: bold; } + .grants-page .card .card-title img { + width: 100px; + height: 100px; } + .grants-page .card label { + font-weight: bold; } diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/css/site.min.css b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/css/site.min.css new file mode 100644 index 0000000..bfc692e --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/css/site.min.css @@ -0,0 +1 @@ +.body-container{margin-top:60px;padding-bottom:40px;}.welcome-page li{list-style:none;padding:4px;}.logged-out-page iframe{display:none;width:0;height:0;}.grants-page .card{margin-top:20px;border-bottom:1px solid #d3d3d3;}.grants-page .card .card-title{font-size:120%;font-weight:bold;}.grants-page .card .card-title img{width:100px;height:100px;}.grants-page .card label{font-weight:bold;} \ No newline at end of file diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/css/site.scss b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/css/site.scss new file mode 100644 index 0000000..ffab645 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/css/site.scss @@ -0,0 +1,42 @@ +.body-container { + margin-top: 60px; + padding-bottom:40px; +} + +.welcome-page { + li { + list-style: none; + padding: 4px; + } +} + +.logged-out-page { + iframe { + display: none; + width: 0; + height: 0; + } +} + +.grants-page { + .card { + margin-top: 20px; + border-bottom: 1px solid lightgray; + + .card-title { + img { + width: 100px; + height: 100px; + } + + font-size: 120%; + font-weight: bold; + } + + label { + font-weight: bold; + } + } +} + + diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/favicon.ico b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/favicon.ico new file mode 100644 index 0000000..ee470e4 Binary files /dev/null and b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/favicon.ico differ diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/icon.jpg b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/icon.jpg new file mode 100644 index 0000000..e652502 Binary files /dev/null and b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/icon.jpg differ diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/icon.png b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/icon.png new file mode 100644 index 0000000..cd386d5 Binary files /dev/null and b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/icon.png differ diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/js/signin-redirect.js b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/js/signin-redirect.js new file mode 100644 index 0000000..6ebc569 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/js/signin-redirect.js @@ -0,0 +1 @@ +window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/js/signout-redirect.js b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/js/signout-redirect.js new file mode 100644 index 0000000..12f684c --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/js/signout-redirect.js @@ -0,0 +1,6 @@ +window.addEventListener("load", function () { + var a = document.querySelector("a.PostLogoutRedirectUri"); + if (a) { + window.location = a.href; + } +}); diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/README.md b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/README.md new file mode 100644 index 0000000..d70069e --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/README.md @@ -0,0 +1,209 @@ +

    + + Bootstrap logo + +

    + +

    Bootstrap

    + +

    + Sleek, intuitive, and powerful front-end framework for faster and easier web development. +
    + Explore Bootstrap docs » +
    +
    + Report bug + · + Request feature + · + Themes + · + Blog +

    + + +## Table of contents + +- [Quick start](#quick-start) +- [Status](#status) +- [What's included](#whats-included) +- [Bugs and feature requests](#bugs-and-feature-requests) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [Community](#community) +- [Versioning](#versioning) +- [Creators](#creators) +- [Thanks](#thanks) +- [Copyright and license](#copyright-and-license) + + +## Quick start + +Several quick start options are available: + +- [Download the latest release.](https://github.com/twbs/bootstrap/archive/v4.4.1.zip) +- Clone the repo: `git clone https://github.com/twbs/bootstrap.git` +- Install with [npm](https://www.npmjs.com/): `npm install bootstrap` +- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap@4.4.1` +- Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:4.4.1` +- Install with [NuGet](https://www.nuget.org/): CSS: `Install-Package bootstrap` Sass: `Install-Package bootstrap.sass` + +Read the [Getting started page](https://getbootstrap.com/docs/4.4/getting-started/introduction/) for information on the framework contents, templates and examples, and more. + + +## Status + +[![Slack](https://bootstrap-slack.herokuapp.com/badge.svg)](https://bootstrap-slack.herokuapp.com/) +[![Build Status](https://github.com/twbs/bootstrap/workflows/Tests/badge.svg)](https://github.com/twbs/bootstrap/actions?workflow=Tests) +[![npm version](https://img.shields.io/npm/v/bootstrap.svg)](https://www.npmjs.com/package/bootstrap) +[![Gem version](https://img.shields.io/gem/v/bootstrap.svg)](https://rubygems.org/gems/bootstrap) +[![Meteor Atmosphere](https://img.shields.io/badge/meteor-twbs%3Abootstrap-blue.svg)](https://atmospherejs.com/twbs/bootstrap) +[![Packagist Prerelease](https://img.shields.io/packagist/vpre/twbs/bootstrap.svg)](https://packagist.org/packages/twbs/bootstrap) +[![NuGet](https://img.shields.io/nuget/vpre/bootstrap.svg)](https://www.nuget.org/packages/bootstrap/absoluteLatest) +[![peerDependencies Status](https://img.shields.io/david/peer/twbs/bootstrap.svg)](https://david-dm.org/twbs/bootstrap?type=peer) +[![devDependency Status](https://img.shields.io/david/dev/twbs/bootstrap.svg)](https://david-dm.org/twbs/bootstrap?type=dev) +[![Coverage Status](https://img.shields.io/coveralls/github/twbs/bootstrap/v4-dev.svg)](https://coveralls.io/github/twbs/bootstrap?branch=v4-dev) +[![CSS gzip size](https://img.badgesize.io/twbs/bootstrap/v4-dev/dist/css/bootstrap.min.css?compression=gzip&label=CSS+gzip+size)](https://github.com/twbs/bootstrap/tree/v4-dev/dist/css/bootstrap.min.css) +[![JS gzip size](https://img.badgesize.io/twbs/bootstrap/v4-dev/dist/js/bootstrap.min.js?compression=gzip&label=JS+gzip+size)](https://github.com/twbs/bootstrap/tree/v4-dev/dist/js/bootstrap.min.js) +[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229)](https://www.browserstack.com/automate/public-build/SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229) +[![Backers on Open Collective](https://opencollective.com/bootstrap/backers/badge.svg)](#backers) +[![Sponsors on Open Collective](https://opencollective.com/bootstrap/sponsors/badge.svg)](#sponsors) + + +## What's included + +Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this: + +```text +bootstrap/ +└── dist/ + ├── css/ + │ ├── bootstrap-grid.css + │ ├── bootstrap-grid.css.map + │ ├── bootstrap-grid.min.css + │ ├── bootstrap-grid.min.css.map + │ ├── bootstrap-reboot.css + │ ├── bootstrap-reboot.css.map + │ ├── bootstrap-reboot.min.css + │ ├── bootstrap-reboot.min.css.map + │ ├── bootstrap.css + │ ├── bootstrap.css.map + │ ├── bootstrap.min.css + │ └── bootstrap.min.css.map + └── js/ + ├── bootstrap.bundle.js + ├── bootstrap.bundle.js.map + ├── bootstrap.bundle.min.js + ├── bootstrap.bundle.min.js.map + ├── bootstrap.js + ├── bootstrap.js.map + ├── bootstrap.min.js + └── bootstrap.min.js.map +``` + +We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/), but not [jQuery](https://jquery.com/). + + +## Bugs and feature requests + +Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/twbs/bootstrap/blob/master/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/twbs/bootstrap/issues/new). + + +## Documentation + +Bootstrap's documentation, included in this repo in the root directory, is built with [Jekyll](https://jekyllrb.com/) and publicly hosted on GitHub Pages at . The docs may also be run locally. + +Documentation search is powered by [Algolia's DocSearch](https://community.algolia.com/docsearch/). Working on our search? Be sure to set `debug: true` in `site/docs/4.4/assets/js/src/search.js` file. + +### Running documentation locally + +1. Run through the [tooling setup](https://getbootstrap.com/docs/4.4/getting-started/build-tools/#tooling-setup) to install Jekyll (the site builder) and other Ruby dependencies with `bundle install`. +2. Run `npm install` to install Node.js dependencies. +3. Run `npm start` to compile CSS and JavaScript files, generate our docs, and watch for changes. +4. Open `http://localhost:9001` in your browser, and voilà. + +Learn more about using Jekyll by reading its [documentation](https://jekyllrb.com/docs/). + +### Documentation for previous releases + +You can find all our previous releases docs on . + +[Previous releases](https://github.com/twbs/bootstrap/releases) and their documentation are also available for download. + + +## Contributing + +Please read through our [contributing guidelines](https://github.com/twbs/bootstrap/blob/master/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development. + +Moreover, if your pull request contains JavaScript patches or features, you must include [relevant unit tests](https://github.com/twbs/bootstrap/tree/master/js/tests). All HTML and CSS should conform to the [Code Guide](https://github.com/mdo/code-guide), maintained by [Mark Otto](https://github.com/mdo). + +Editor preferences are available in the [editor config](https://github.com/twbs/bootstrap/blob/master/.editorconfig) for easy use in common text editors. Read more and download plugins at . + + +## Community + +Get updates on Bootstrap's development and chat with the project maintainers and community members. + +- Follow [@getbootstrap on Twitter](https://twitter.com/getbootstrap). +- Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.com/). +- Join [the official Slack room](https://bootstrap-slack.herokuapp.com/). +- Chat with fellow Bootstrappers in IRC. On the `irc.freenode.net` server, in the `##bootstrap` channel. +- Implementation help may be found at Stack Overflow (tagged [`bootstrap-4`](https://stackoverflow.com/questions/tagged/bootstrap-4)). +- Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/browse/keyword/bootstrap) or similar delivery mechanisms for maximum discoverability. + + +## Versioning + +For transparency into our release cycle and in striving to maintain backward compatibility, Bootstrap is maintained under [the Semantic Versioning guidelines](https://semver.org/). Sometimes we screw up, but we adhere to those rules whenever possible. + +See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release. + + +## Creators + +**Mark Otto** + +- +- + +**Jacob Thornton** + +- +- + + +## Thanks + + + BrowserStack Logo + + +Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers! + + +## Backers + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/bootstrap#backer)] + +[![Bakers](https://opencollective.com/bootstrap/backers.svg?width=890)](https://opencollective.com/bootstrap#backers) + + +## Sponsors + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/bootstrap#sponsor)] + +[![](https://opencollective.com/bootstrap/sponsor/0/avatar.svg)](https://opencollective.com/bootstrap/sponsor/0/website) +[![](https://opencollective.com/bootstrap/sponsor/1/avatar.svg)](https://opencollective.com/bootstrap/sponsor/1/website) +[![](https://opencollective.com/bootstrap/sponsor/2/avatar.svg)](https://opencollective.com/bootstrap/sponsor/2/website) +[![](https://opencollective.com/bootstrap/sponsor/3/avatar.svg)](https://opencollective.com/bootstrap/sponsor/3/website) +[![](https://opencollective.com/bootstrap/sponsor/4/avatar.svg)](https://opencollective.com/bootstrap/sponsor/4/website) +[![](https://opencollective.com/bootstrap/sponsor/5/avatar.svg)](https://opencollective.com/bootstrap/sponsor/5/website) +[![](https://opencollective.com/bootstrap/sponsor/6/avatar.svg)](https://opencollective.com/bootstrap/sponsor/6/website) +[![](https://opencollective.com/bootstrap/sponsor/7/avatar.svg)](https://opencollective.com/bootstrap/sponsor/7/website) +[![](https://opencollective.com/bootstrap/sponsor/8/avatar.svg)](https://opencollective.com/bootstrap/sponsor/8/website) +[![](https://opencollective.com/bootstrap/sponsor/9/avatar.svg)](https://opencollective.com/bootstrap/sponsor/9/website) + + +## Copyright and license + +Code and documentation copyright 2011-2019 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors) and [Twitter, Inc.](https://twitter.com) Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/master/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/). diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/scss/_alert.scss b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/scss/_alert.scss new file mode 100644 index 0000000..da2a98a --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/scss/_alert.scss @@ -0,0 +1,51 @@ +// +// Base styles +// + +.alert { + position: relative; + padding: $alert-padding-y $alert-padding-x; + margin-bottom: $alert-margin-bottom; + border: $alert-border-width solid transparent; + @include border-radius($alert-border-radius); +} + +// Headings for larger alerts +.alert-heading { + // Specified to prevent conflicts of changing $headings-color + color: inherit; +} + +// Provide class for links that match alerts +.alert-link { + font-weight: $alert-link-font-weight; +} + + +// Dismissible alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissible { + padding-right: $close-font-size + $alert-padding-x * 2; + + // Adjust close link position + .close { + position: absolute; + top: 0; + right: 0; + padding: $alert-padding-y $alert-padding-x; + color: inherit; + } +} + + +// Alternate styles +// +// Generate contextual modifier classes for colorizing the alert. + +@each $color, $value in $theme-colors { + .alert-#{$color} { + @include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), theme-color-level($color, $alert-color-level)); + } +} diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/scss/_badge.scss b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/scss/_badge.scss new file mode 100644 index 0000000..42c5d08 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/scss/_badge.scss @@ -0,0 +1,54 @@ +// Base class +// +// Requires one of the contextual, color modifier classes for `color` and +// `background-color`. + +.badge { + display: inline-block; + padding: $badge-padding-y $badge-padding-x; + @include font-size($badge-font-size); + font-weight: $badge-font-weight; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + @include border-radius($badge-border-radius); + @include transition($badge-transition); + + @at-root a#{&} { + @include hover-focus() { + text-decoration: none; + } + } + + // Empty badges collapse automatically + &:empty { + display: none; + } +} + +// Quick fix for badges in buttons +.btn .badge { + position: relative; + top: -1px; +} + +// Pill badges +// +// Make them extra rounded with a modifier to replace v3's badges. + +.badge-pill { + padding-right: $badge-pill-padding-x; + padding-left: $badge-pill-padding-x; + @include border-radius($badge-pill-border-radius); +} + +// Colors +// +// Contextual variations (linked badges get darker on :hover). + +@each $color, $value in $theme-colors { + .badge-#{$color} { + @include badge-variant($value); + } +} diff --git a/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/scss/_breadcrumb.scss b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/scss/_breadcrumb.scss new file mode 100644 index 0000000..d748894 --- /dev/null +++ b/CSharp/OidcSamples/OidcSamples.AuthorizationServer/wwwroot/lib/bootstrap/scss/_breadcrumb.scss @@ -0,0 +1,42 @@ +.breadcrumb { + display: flex; + flex-wrap: wrap; + padding: $breadcrumb-padding-y $breadcrumb-padding-x; + margin-bottom: $breadcrumb-margin-bottom; + @include font-size($breadcrumb-font-size); + list-style: none; + background-color: $breadcrumb-bg; + @include border-radius($breadcrumb-border-radius); +} + +.breadcrumb-item { + // The separator between breadcrumbs (by default, a forward-slash: "/") + + .breadcrumb-item { + padding-left: $breadcrumb-item-padding; + + &::before { + display: inline-block; // Suppress underlining of the separator in modern browsers + padding-right: $breadcrumb-item-padding; + color: $breadcrumb-divider-color; + content: escape-svg($breadcrumb-divider); + } + } + + // IE9-11 hack to properly handle hyperlink underlines for breadcrumbs built + // without `