mirror of
https://gitee.com/dotnetchina/OpenAuth.Net.git
synced 2025-09-19 10:08:04 +08:00
转移.net core 3.1,为.NET 5做准备
This commit is contained in:
383
OpenAuth.Identity/Quickstart/Account/AccountController.cs
Normal file
383
OpenAuth.Identity/Quickstart/Account/AccountController.cs
Normal file
@@ -0,0 +1,383 @@
|
||||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Events;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenAuth.App;
|
||||
using OpenAuth.Repository.Domain;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class AccountController : Controller
|
||||
{
|
||||
private readonly UserManagerApp _userManager;
|
||||
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, UserManagerApp userManager)
|
||||
{
|
||||
|
||||
_interaction = interaction;
|
||||
_clientStore = clientStore;
|
||||
_schemeProvider = schemeProvider;
|
||||
_events = events;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry point into the login workflow
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> 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 { provider = vm.ExternalLoginScheme, returnUrl });
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle postback from username/password login
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> 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.GrantConsentAsync(context, ConsentResponse.Denied);
|
||||
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
if (await _clientStore.IsPkceClientAsync(context.ClientId))
|
||||
{
|
||||
// if the client is PKCE then we assume it's native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return View("Redirect", new RedirectViewModel { RedirectUrl = 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)
|
||||
{
|
||||
User user;
|
||||
if (model.Username == Define.SYSTEM_USERNAME && model.Password == Define.SYSTEM_USERPWD)
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
Account = Define.SYSTEM_USERNAME,
|
||||
Password = Define.SYSTEM_USERPWD,
|
||||
Id = Define.SYSTEM_USERNAME
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
user = _userManager.GetByAccount(model.Username);
|
||||
}
|
||||
|
||||
if (user != null &&(user.Password ==model.Password))
|
||||
{
|
||||
if (user.Status != 0) //判断用户状态
|
||||
{
|
||||
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid user status"));
|
||||
ModelState.AddModelError(string.Empty, "user.status must be 0");
|
||||
var err = await BuildLoginViewModelAsync(model);
|
||||
return View(err);
|
||||
}
|
||||
|
||||
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Account, user.Id, user.Account));
|
||||
|
||||
// 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
|
||||
await HttpContext.SignInAsync(user.Id, user.Account, props);
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
if (await _clientStore.IsPkceClientAsync(context.ClientId))
|
||||
{
|
||||
// if the client is PKCE then we assume it's native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
|
||||
}
|
||||
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
return Redirect(model.ReturnUrl);
|
||||
}
|
||||
|
||||
// request for a local page
|
||||
if (Url.IsLocalUrl(model.ReturnUrl))
|
||||
{
|
||||
return Redirect(model.ReturnUrl);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(model.ReturnUrl))
|
||||
{
|
||||
return Redirect("~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new Exception("invalid return URL");
|
||||
}
|
||||
}
|
||||
|
||||
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.ClientId));
|
||||
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
|
||||
}
|
||||
|
||||
// something went wrong, show form with error
|
||||
var vm = await BuildLoginViewModelAsync(model);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Show logout page
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle logout page postback
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> 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<LoginViewModel> 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 ||
|
||||
(x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
|
||||
)
|
||||
.Select(x => new ExternalProvider
|
||||
{
|
||||
DisplayName = x.DisplayName,
|
||||
AuthenticationScheme = x.Name
|
||||
}).ToList();
|
||||
|
||||
var allowLocal = true;
|
||||
if (context?.ClientId != null)
|
||||
{
|
||||
var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId);
|
||||
if (client != null)
|
||||
{
|
||||
allowLocal = client.EnableLocalLogin;
|
||||
|
||||
if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any())
|
||||
{
|
||||
providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new LoginViewModel
|
||||
{
|
||||
AllowRememberLogin = AccountOptions.AllowRememberLogin,
|
||||
EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin,
|
||||
ReturnUrl = returnUrl,
|
||||
Username = context?.LoginHint,
|
||||
ExternalProviders = providers.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<LoginViewModel> BuildLoginViewModelAsync(LoginInputModel model)
|
||||
{
|
||||
var vm = await BuildLoginViewModelAsync(model.ReturnUrl);
|
||||
vm.Username = model.Username;
|
||||
vm.RememberLogin = model.RememberLogin;
|
||||
return vm;
|
||||
}
|
||||
|
||||
private async Task<LogoutViewModel> 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<LoggedOutViewModel> 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;
|
||||
}
|
||||
}
|
||||
}
|
25
OpenAuth.Identity/Quickstart/Account/AccountOptions.cs
Normal file
25
OpenAuth.Identity/Quickstart/Account/AccountOptions.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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 OpenAuth.IdentityServer.Quickstart.Account
|
||||
{
|
||||
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; //允许注销后跳转
|
||||
|
||||
// specify the Windows authentication scheme being used
|
||||
public static readonly string WindowsAuthenticationSchemeName = Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme;
|
||||
// if user uses windows auth, should we load the groups from windows
|
||||
public static bool IncludeWindowsGroups = false;
|
||||
|
||||
public static string InvalidCredentialsErrorMessage = "Invalid username or password";
|
||||
}
|
||||
}
|
251
OpenAuth.Identity/Quickstart/Account/ExternalController.cs
Normal file
251
OpenAuth.Identity/Quickstart/Account/ExternalController.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityModel;
|
||||
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;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart.Account
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class ExternalController : Controller
|
||||
{
|
||||
private readonly TestUserStore _users;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IClientStore _clientStore;
|
||||
private readonly ILogger<ExternalController> _logger;
|
||||
private readonly IEventService _events;
|
||||
|
||||
public ExternalController(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IClientStore clientStore,
|
||||
IEventService events,
|
||||
ILogger<ExternalController> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// initiate roundtrip to external authentication provider
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Challenge(string provider, 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");
|
||||
}
|
||||
|
||||
if (AccountOptions.WindowsAuthenticationSchemeName == provider)
|
||||
{
|
||||
// windows authentication needs special handling
|
||||
return await ProcessWindowsLoginAsync(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
// start challenge and roundtrip the return URL and scheme
|
||||
var props = new AuthenticationProperties
|
||||
{
|
||||
RedirectUri = Url.Action(nameof(Callback)),
|
||||
Items =
|
||||
{
|
||||
{ "returnUrl", returnUrl },
|
||||
{ "scheme", provider },
|
||||
}
|
||||
};
|
||||
|
||||
return Challenge(props, provider);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Post processing of external authentication
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Callback()
|
||||
{
|
||||
// read external identity from the temporary cookie
|
||||
var result = await HttpContext.AuthenticateAsync(IdentityServer4.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 additonal claims or properties
|
||||
// for the specific prtotocols 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<Claim>();
|
||||
var localSignInProps = new AuthenticationProperties();
|
||||
ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
|
||||
ProcessLoginCallbackForWsFed(result, additionalLocalClaims, localSignInProps);
|
||||
ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps);
|
||||
|
||||
// issue authentication cookie for user
|
||||
await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, localSignInProps, additionalLocalClaims.ToArray());
|
||||
|
||||
// delete temporary cookie used during external authentication
|
||||
await HttpContext.SignOutAsync(IdentityServer4.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?.ClientId));
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
if (await _clientStore.IsPkceClientAsync(context.ClientId))
|
||||
{
|
||||
// if the client is PKCE then we assume it's native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return View("Redirect", new RedirectViewModel { RedirectUrl = returnUrl });
|
||||
}
|
||||
}
|
||||
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
|
||||
{
|
||||
// see if windows auth has already been requested and succeeded
|
||||
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
|
||||
if (result?.Principal is WindowsPrincipal wp)
|
||||
{
|
||||
// we will issue the external cookie and then redirect the
|
||||
// user back to the external callback, in essence, treating windows
|
||||
// auth the same as any other external authentication mechanism
|
||||
var props = new AuthenticationProperties()
|
||||
{
|
||||
RedirectUri = Url.Action("Callback"),
|
||||
Items =
|
||||
{
|
||||
{ "returnUrl", returnUrl },
|
||||
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
|
||||
}
|
||||
};
|
||||
|
||||
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
|
||||
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
|
||||
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
|
||||
|
||||
// add the groups as claims -- be careful if the number of groups is too large
|
||||
if (AccountOptions.IncludeWindowsGroups)
|
||||
{
|
||||
var wi = wp.Identity as WindowsIdentity;
|
||||
var groups = wi.Groups.Translate(typeof(NTAccount));
|
||||
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
|
||||
id.AddClaims(roles);
|
||||
}
|
||||
|
||||
await HttpContext.SignInAsync(
|
||||
IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme,
|
||||
new ClaimsPrincipal(id),
|
||||
props);
|
||||
return Redirect(props.RedirectUri);
|
||||
}
|
||||
else
|
||||
{
|
||||
// trigger windows auth
|
||||
// since windows auth don't support the redirect uri,
|
||||
// this URL is re-triggered when we call challenge
|
||||
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
|
||||
}
|
||||
}
|
||||
|
||||
private (TestUser user, string provider, string providerUserId, IEnumerable<Claim> 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<Claim> claims)
|
||||
{
|
||||
var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList());
|
||||
return user;
|
||||
}
|
||||
|
||||
private void ProcessLoginCallbackForOidc(AuthenticateResult externalResult, List<Claim> 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 id_token = externalResult.Properties.GetTokenValue("id_token");
|
||||
if (id_token != null)
|
||||
{
|
||||
localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } });
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessLoginCallbackForWsFed(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
||||
{
|
||||
}
|
||||
|
||||
private void ProcessLoginCallbackForSaml2p(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
12
OpenAuth.Identity/Quickstart/Account/ExternalProvider.cs
Normal file
12
OpenAuth.Identity/Quickstart/Account/ExternalProvider.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Account
|
||||
{
|
||||
public class ExternalProvider
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public string AuthenticationScheme { get; set; }
|
||||
}
|
||||
}
|
19
OpenAuth.Identity/Quickstart/Account/LoggedOutViewModel.cs
Normal file
19
OpenAuth.Identity/Quickstart/Account/LoggedOutViewModel.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Account
|
||||
{
|
||||
public class LoggedOutViewModel
|
||||
{
|
||||
public string PostLogoutRedirectUri { get; set; }
|
||||
public string ClientName { get; set; }
|
||||
public string SignOutIframeUrl { get; set; }
|
||||
|
||||
public bool AutomaticRedirectAfterSignOut { get; set; } = false;
|
||||
|
||||
public string LogoutId { get; set; }
|
||||
public bool TriggerExternalSignout => ExternalAuthenticationScheme != null;
|
||||
public string ExternalAuthenticationScheme { get; set; }
|
||||
}
|
||||
}
|
18
OpenAuth.Identity/Quickstart/Account/LoginInputModel.cs
Normal file
18
OpenAuth.Identity/Quickstart/Account/LoginInputModel.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Account
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
22
OpenAuth.Identity/Quickstart/Account/LoginViewModel.cs
Normal file
22
OpenAuth.Identity/Quickstart/Account/LoginViewModel.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Account
|
||||
{
|
||||
public class LoginViewModel : LoginInputModel
|
||||
{
|
||||
public bool AllowRememberLogin { get; set; } = true;
|
||||
public bool EnableLocalLogin { get; set; } = true;
|
||||
|
||||
public IEnumerable<ExternalProvider> ExternalProviders { get; set; } = Enumerable.Empty<ExternalProvider>();
|
||||
public IEnumerable<ExternalProvider> VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName));
|
||||
|
||||
public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1;
|
||||
public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null;
|
||||
}
|
||||
}
|
11
OpenAuth.Identity/Quickstart/Account/LogoutInputModel.cs
Normal file
11
OpenAuth.Identity/Quickstart/Account/LogoutInputModel.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Account
|
||||
{
|
||||
public class LogoutInputModel
|
||||
{
|
||||
public string LogoutId { get; set; }
|
||||
}
|
||||
}
|
11
OpenAuth.Identity/Quickstart/Account/LogoutViewModel.cs
Normal file
11
OpenAuth.Identity/Quickstart/Account/LogoutViewModel.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Account
|
||||
{
|
||||
public class LogoutViewModel : LogoutInputModel
|
||||
{
|
||||
public bool ShowLogoutPrompt { get; set; } = true;
|
||||
}
|
||||
}
|
12
OpenAuth.Identity/Quickstart/Account/RedirectViewModel.cs
Normal file
12
OpenAuth.Identity/Quickstart/Account/RedirectViewModel.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Account
|
||||
{
|
||||
public class RedirectViewModel
|
||||
{
|
||||
public string RedirectUrl { get; set; }
|
||||
}
|
||||
}
|
266
OpenAuth.Identity/Quickstart/Consent/ConsentController.cs
Normal file
266
OpenAuth.Identity/Quickstart/Consent/ConsentController.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
// 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 IdentityServer4.Events;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenAuth.IdentityServer.Quickstart.Account;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart.Consent
|
||||
{
|
||||
/// <summary>
|
||||
/// This controller processes the consent UI
|
||||
/// </summary>
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class ConsentController : Controller
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IClientStore _clientStore;
|
||||
private readonly IResourceStore _resourceStore;
|
||||
private readonly IEventService _events;
|
||||
private readonly ILogger<ConsentController> _logger;
|
||||
|
||||
public ConsentController(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IClientStore clientStore,
|
||||
IResourceStore resourceStore,
|
||||
IEventService events,
|
||||
ILogger<ConsentController> logger)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_clientStore = clientStore;
|
||||
_resourceStore = resourceStore;
|
||||
_events = events;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the consent screen
|
||||
/// </summary>
|
||||
/// <param name="returnUrl"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Index(string returnUrl)
|
||||
{
|
||||
var vm = await BuildViewModelAsync(returnUrl);
|
||||
if (vm != null)
|
||||
{
|
||||
return View("Index", vm);
|
||||
}
|
||||
|
||||
return View("Error");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the consent screen postback
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Index(ConsentInputModel model)
|
||||
{
|
||||
var result = await ProcessConsent(model);
|
||||
|
||||
if (result.IsRedirect)
|
||||
{
|
||||
if (await _clientStore.IsPkceClientAsync(result.ClientId))
|
||||
{
|
||||
// if the client is PKCE then we assume it's native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return View("Redirect", new RedirectViewModel { RedirectUrl = 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<ProcessConsentResult> ProcessConsent(ConsentInputModel model)
|
||||
{
|
||||
var result = new ProcessConsentResult();
|
||||
|
||||
// validate return url is still valid
|
||||
var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
|
||||
if (request == null) return result;
|
||||
|
||||
ConsentResponse grantedConsent = null;
|
||||
|
||||
// user clicked 'no' - send back the standard 'access_denied' response
|
||||
if (model?.Button == "no")
|
||||
{
|
||||
grantedConsent = ConsentResponse.Denied;
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested));
|
||||
}
|
||||
// user clicked 'yes' - validate the data
|
||||
else if (model?.Button == "yes")
|
||||
{
|
||||
// if the user consented to some scope, build the response model
|
||||
if (model.ScopesConsented != null && model.ScopesConsented.Any())
|
||||
{
|
||||
var scopes = model.ScopesConsented;
|
||||
if (ConsentOptions.EnableOfflineAccess == false)
|
||||
{
|
||||
scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
RememberConsent = model.RememberConsent,
|
||||
ScopesConsented = scopes.ToArray()
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested, grantedConsent.ScopesConsented, grantedConsent.RememberConsent));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
|
||||
}
|
||||
|
||||
if (grantedConsent != null)
|
||||
{
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.GrantConsentAsync(request, grantedConsent);
|
||||
|
||||
// indicate that's it ok to redirect back to authorization endpoint
|
||||
result.RedirectUri = model.ReturnUrl;
|
||||
result.ClientId = request.ClientId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we need to redisplay the consent UI
|
||||
result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<ConsentViewModel> BuildViewModelAsync(string returnUrl, ConsentInputModel model = null)
|
||||
{
|
||||
var request = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (request != null)
|
||||
{
|
||||
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
|
||||
if (client != null)
|
||||
{
|
||||
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);
|
||||
if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any()))
|
||||
{
|
||||
return CreateConsentViewModel(model, returnUrl, request, client, resources);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("No scopes matching: {0}", request.ScopesRequested.Aggregate((x, y) => x + ", " + y));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Invalid client id: {0}", request.ClientId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("No consent request matching request: {0}", returnUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ConsentViewModel CreateConsentViewModel(
|
||||
ConsentInputModel model, string returnUrl,
|
||||
AuthorizationRequest request,
|
||||
Client client, Resources resources)
|
||||
{
|
||||
var vm = new ConsentViewModel
|
||||
{
|
||||
RememberConsent = model?.RememberConsent ?? true,
|
||||
ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty<string>(),
|
||||
|
||||
ReturnUrl = returnUrl,
|
||||
|
||||
ClientName = client.ClientName ?? client.ClientId,
|
||||
ClientUrl = client.ClientUri,
|
||||
ClientLogoUrl = client.LogoUri,
|
||||
AllowRememberConsent = client.AllowRememberConsent
|
||||
};
|
||||
|
||||
vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
|
||||
vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
|
||||
if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess)
|
||||
{
|
||||
vm.ResourceScopes = vm.ResourceScopes.Union(new ScopeViewModel[] {
|
||||
GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null)
|
||||
});
|
||||
}
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = identity.Name,
|
||||
DisplayName = identity.DisplayName,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
public ScopeViewModel CreateScopeViewModel(Scope scope, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = scope.Name,
|
||||
DisplayName = scope.DisplayName,
|
||||
Description = scope.Description,
|
||||
Emphasize = scope.Emphasize,
|
||||
Required = scope.Required,
|
||||
Checked = check || scope.Required
|
||||
};
|
||||
}
|
||||
|
||||
private ScopeViewModel GetOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = ConsentOptions.OfflineAccessDisplayName,
|
||||
Description = ConsentOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
16
OpenAuth.Identity/Quickstart/Consent/ConsentInputModel.cs
Normal file
16
OpenAuth.Identity/Quickstart/Consent/ConsentInputModel.cs
Normal file
@@ -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.
|
||||
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart.Consent
|
||||
{
|
||||
public class ConsentInputModel
|
||||
{
|
||||
public string Button { get; set; }
|
||||
public IEnumerable<string> ScopesConsented { get; set; }
|
||||
public bool RememberConsent { get; set; }
|
||||
public string ReturnUrl { get; set; }
|
||||
}
|
||||
}
|
16
OpenAuth.Identity/Quickstart/Consent/ConsentOptions.cs
Normal file
16
OpenAuth.Identity/Quickstart/Consent/ConsentOptions.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Consent
|
||||
{
|
||||
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";
|
||||
}
|
||||
}
|
19
OpenAuth.Identity/Quickstart/Consent/ConsentViewModel.cs
Normal file
19
OpenAuth.Identity/Quickstart/Consent/ConsentViewModel.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Consent
|
||||
{
|
||||
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<ScopeViewModel> IdentityScopes { get; set; }
|
||||
public IEnumerable<ScopeViewModel> ResourceScopes { get; set; }
|
||||
}
|
||||
}
|
19
OpenAuth.Identity/Quickstart/Consent/ProcessConsentResult.cs
Normal file
19
OpenAuth.Identity/Quickstart/Consent/ProcessConsentResult.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Consent
|
||||
{
|
||||
public class ProcessConsentResult
|
||||
{
|
||||
public bool IsRedirect => RedirectUri != null;
|
||||
public string RedirectUri { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
|
||||
public bool ShowView => ViewModel != null;
|
||||
public ConsentViewModel ViewModel { get; set; }
|
||||
|
||||
public bool HasValidationError => ValidationError != null;
|
||||
public string ValidationError { get; set; }
|
||||
}
|
||||
}
|
16
OpenAuth.Identity/Quickstart/Consent/ScopeViewModel.cs
Normal file
16
OpenAuth.Identity/Quickstart/Consent/ScopeViewModel.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Consent
|
||||
{
|
||||
public class ScopeViewModel
|
||||
{
|
||||
public string Name { 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; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
// 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 OpenAuth.IdentityServer.Quickstart.Consent;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart.Device
|
||||
{
|
||||
public class DeviceAuthorizationInputModel : ConsentInputModel
|
||||
{
|
||||
public string UserCode { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
// 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 OpenAuth.IdentityServer.Quickstart.Consent;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart.Device
|
||||
{
|
||||
public class DeviceAuthorizationViewModel : ConsentViewModel
|
||||
{
|
||||
public string UserCode { get; set; }
|
||||
public bool ConfirmUserCode { get; set; }
|
||||
}
|
||||
}
|
243
OpenAuth.Identity/Quickstart/Device/DeviceController.cs
Normal file
243
OpenAuth.Identity/Quickstart/Device/DeviceController.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4.Configuration;
|
||||
using IdentityServer4.Events;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenAuth.IdentityServer.Quickstart.Consent;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart.Device
|
||||
{
|
||||
[Authorize]
|
||||
[SecurityHeaders]
|
||||
public class DeviceController : Controller
|
||||
{
|
||||
private readonly IDeviceFlowInteractionService _interaction;
|
||||
private readonly IClientStore _clientStore;
|
||||
private readonly IResourceStore _resourceStore;
|
||||
private readonly IEventService _events;
|
||||
private readonly IOptions<IdentityServerOptions> _options;
|
||||
private readonly ILogger<DeviceController> _logger;
|
||||
|
||||
public DeviceController(
|
||||
IDeviceFlowInteractionService interaction,
|
||||
IClientStore clientStore,
|
||||
IResourceStore resourceStore,
|
||||
IEventService eventService,
|
||||
IOptions<IdentityServerOptions> options,
|
||||
ILogger<DeviceController> logger)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_clientStore = clientStore;
|
||||
_resourceStore = resourceStore;
|
||||
_events = eventService;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> 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<IActionResult> UserCodeCapture(string userCode)
|
||||
{
|
||||
var vm = await BuildViewModelAsync(userCode);
|
||||
if (vm == null) return View("Error");
|
||||
|
||||
return View("UserCodeConfirmation", vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> 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<ProcessConsentResult> ProcessConsent(DeviceAuthorizationInputModel model)
|
||||
{
|
||||
var result = new ProcessConsentResult();
|
||||
|
||||
var request = await _interaction.GetAuthorizationContextAsync(model.UserCode);
|
||||
if (request == null) return result;
|
||||
|
||||
ConsentResponse grantedConsent = null;
|
||||
|
||||
// user clicked 'no' - send back the standard 'access_denied' response
|
||||
if (model.Button == "no")
|
||||
{
|
||||
grantedConsent = ConsentResponse.Denied;
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested));
|
||||
}
|
||||
// user clicked 'yes' - validate the data
|
||||
else if (model.Button == "yes")
|
||||
{
|
||||
// if the user consented to some scope, build the response model
|
||||
if (model.ScopesConsented != null && model.ScopesConsented.Any())
|
||||
{
|
||||
var scopes = model.ScopesConsented;
|
||||
if (ConsentOptions.EnableOfflineAccess == false)
|
||||
{
|
||||
scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
RememberConsent = model.RememberConsent,
|
||||
ScopesConsented = scopes.ToArray()
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested, grantedConsent.ScopesConsented, grantedConsent.RememberConsent));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
|
||||
}
|
||||
|
||||
if (grantedConsent != null)
|
||||
{
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.HandleRequestAsync(model.UserCode, grantedConsent);
|
||||
|
||||
// indicate that's it ok to redirect back to authorization endpoint
|
||||
result.RedirectUri = model.ReturnUrl;
|
||||
result.ClientId = request.ClientId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we need to redisplay the consent UI
|
||||
result.ViewModel = await BuildViewModelAsync(model.UserCode, model);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<DeviceAuthorizationViewModel> BuildViewModelAsync(string userCode, DeviceAuthorizationInputModel model = null)
|
||||
{
|
||||
var request = await _interaction.GetAuthorizationContextAsync(userCode);
|
||||
if (request != null)
|
||||
{
|
||||
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
|
||||
if (client != null)
|
||||
{
|
||||
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);
|
||||
if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any()))
|
||||
{
|
||||
return CreateConsentViewModel(userCode, model, client, resources);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("No scopes matching: {0}", request.ScopesRequested.Aggregate((x, y) => x + ", " + y));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Invalid client id: {0}", request.ClientId);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, DeviceAuthorizationInputModel model, Client client, Resources resources)
|
||||
{
|
||||
var vm = new DeviceAuthorizationViewModel
|
||||
{
|
||||
UserCode = userCode,
|
||||
|
||||
RememberConsent = model?.RememberConsent ?? true,
|
||||
ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty<string>(),
|
||||
|
||||
ClientName = client.ClientName ?? client.ClientId,
|
||||
ClientUrl = client.ClientUri,
|
||||
ClientLogoUrl = client.LogoUri,
|
||||
AllowRememberConsent = client.AllowRememberConsent
|
||||
};
|
||||
|
||||
vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
|
||||
vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
|
||||
if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess)
|
||||
{
|
||||
vm.ResourceScopes = vm.ResourceScopes.Union(new[]
|
||||
{
|
||||
GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null)
|
||||
});
|
||||
}
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = identity.Name,
|
||||
DisplayName = identity.DisplayName,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
public ScopeViewModel CreateScopeViewModel(Scope scope, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = scope.Name,
|
||||
DisplayName = scope.DisplayName,
|
||||
Description = scope.Description,
|
||||
Emphasize = scope.Emphasize,
|
||||
Required = scope.Required,
|
||||
Checked = check || scope.Required
|
||||
};
|
||||
}
|
||||
private ScopeViewModel GetOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = ConsentOptions.OfflineAccessDisplayName,
|
||||
Description = ConsentOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 OpenAuth.IdentityServer.Quickstart.Diagnostics
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class DiagnosticsController : Controller
|
||||
{
|
||||
public async Task<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 System.Collections.Generic;
|
||||
using System.Text;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart.Diagnostics
|
||||
{
|
||||
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<string[]>(value);
|
||||
}
|
||||
}
|
||||
|
||||
public AuthenticateResult AuthenticateResult { get; }
|
||||
public IEnumerable<string> Clients { get; } = new List<string>();
|
||||
}
|
||||
}
|
25
OpenAuth.Identity/Quickstart/Extensions.cs
Normal file
25
OpenAuth.Identity/Quickstart/Extensions.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4.Stores;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the client is configured to use PKCE.
|
||||
/// </summary>
|
||||
/// <param name="store">The store.</param>
|
||||
/// <param name="client_id">The client identifier.</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<bool> IsPkceClientAsync(this IClientStore store, string client_id)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(client_id))
|
||||
{
|
||||
var client = await store.FindEnabledClientByIdAsync(client_id);
|
||||
return client?.RequirePkce == true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
96
OpenAuth.Identity/Quickstart/Grants/GrantsController.cs
Normal file
96
OpenAuth.Identity/Quickstart/Grants/GrantsController.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4.Events;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart.Grants
|
||||
{
|
||||
/// <summary>
|
||||
/// This sample controller allows a user to revoke grants given to clients
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show list of grants
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
return View("Index", await BuildViewModelAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle postback to revoke a client
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Revoke(string clientId)
|
||||
{
|
||||
await _interaction.RevokeUserConsentAsync(clientId);
|
||||
await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), clientId));
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
private async Task<GrantsViewModel> BuildViewModelAsync()
|
||||
{
|
||||
var grants = await _interaction.GetAllUserConsentsAsync();
|
||||
|
||||
var list = new List<GrantViewModel>();
|
||||
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,
|
||||
Created = grant.CreationTime,
|
||||
Expires = grant.Expiration,
|
||||
IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(),
|
||||
ApiGrantNames = resources.ApiResources.Select(x => x.DisplayName ?? x.Name).ToArray()
|
||||
};
|
||||
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return new GrantsViewModel
|
||||
{
|
||||
Grants = list
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
26
OpenAuth.Identity/Quickstart/Grants/GrantsViewModel.cs
Normal file
26
OpenAuth.Identity/Quickstart/Grants/GrantsViewModel.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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 OpenAuth.IdentityServer.Quickstart.Grants
|
||||
{
|
||||
public class GrantsViewModel
|
||||
{
|
||||
public IEnumerable<GrantViewModel> 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 DateTime Created { get; set; }
|
||||
public DateTime? Expires { get; set; }
|
||||
public IEnumerable<string> IdentityGrantNames { get; set; }
|
||||
public IEnumerable<string> ApiGrantNames { get; set; }
|
||||
}
|
||||
}
|
22
OpenAuth.Identity/Quickstart/Home/ErrorViewModel.cs
Normal file
22
OpenAuth.Identity/Quickstart/Home/ErrorViewModel.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart.Home
|
||||
{
|
||||
public class ErrorViewModel
|
||||
{
|
||||
public ErrorViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
public ErrorViewModel(string error)
|
||||
{
|
||||
Error = new ErrorMessage { Error = error };
|
||||
}
|
||||
|
||||
public ErrorMessage Error { get; set; }
|
||||
}
|
||||
}
|
64
OpenAuth.Identity/Quickstart/Home/HomeController.cs
Normal file
64
OpenAuth.Identity/Quickstart/Home/HomeController.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart.Home
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public HomeController(IIdentityServerInteractionService interaction, IHostEnvironment environment, ILogger<HomeController> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the error page
|
||||
/// </summary>
|
||||
public async Task<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
56
OpenAuth.Identity/Quickstart/SecurityHeadersAttribute.cs
Normal file
56
OpenAuth.Identity/Quickstart/SecurityHeadersAttribute.cs
Normal file
@@ -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 OpenAuth.IdentityServer.Quickstart
|
||||
{
|
||||
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);
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
OpenAuth.Identity/Quickstart/TestUsers.cs
Normal file
25
OpenAuth.Identity/Quickstart/TestUsers.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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;
|
||||
using System.Security.Claims;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Test;
|
||||
|
||||
namespace OpenAuth.IdentityServer.Quickstart
|
||||
{
|
||||
public class TestUsers
|
||||
{
|
||||
public static List<TestUser> Users = new List<TestUser>
|
||||
{
|
||||
new TestUser{SubjectId = "System", Username = "System", Password = "123456",
|
||||
Claims =
|
||||
{
|
||||
new Claim(JwtClaimTypes.Name, "System"),
|
||||
new Claim(JwtClaimTypes.GivenName, "yubao"),
|
||||
new Claim(JwtClaimTypes.FamilyName, "lee")}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user