@ -1,31 +0,0 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API.Certificates | |||||
{ | |||||
static class Certificate | |||||
{ | |||||
public static X509Certificate2 Get() | |||||
{ | |||||
var assembly = typeof(Certificate).GetTypeInfo().Assembly; | |||||
var names = assembly.GetManifestResourceNames(); | |||||
/*********************************************************************************************** | |||||
* Please note that here we are using a local certificate only for testing purposes. In a | |||||
* real environment the certificate should be created and stored in a secure way, which is out | |||||
* of the scope of this project. | |||||
**********************************************************************************************/ | |||||
using var stream = assembly.GetManifestResourceStream("Identity.API.Certificate.idsrv3test.pfx"); | |||||
return new X509Certificate2(ReadStream(stream), "idsrv3test"); | |||||
} | |||||
private static byte[] ReadStream(Stream input) | |||||
{ | |||||
byte[] buffer = new byte[16 * 1024]; | |||||
using MemoryStream ms = new MemoryStream(); | |||||
int read; | |||||
while ((read = input.Read(buffer, 0, buffer.Length)) > 0) | |||||
{ | |||||
ms.Write(buffer, 0, read); | |||||
} | |||||
return ms.ToArray(); | |||||
} | |||||
} | |||||
} |
@ -1,299 +0,0 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers | |||||
{ | |||||
/// <summary> | |||||
/// This sample controller implements a typical login/logout/provision workflow for local 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> | |||||
public class AccountController : Controller | |||||
{ | |||||
//private readonly InMemoryUserLoginService _loginService; | |||||
private readonly ILoginService<ApplicationUser> _loginService; | |||||
private readonly IIdentityServerInteractionService _interaction; | |||||
private readonly IClientStore _clientStore; | |||||
private readonly ILogger<AccountController> _logger; | |||||
private readonly UserManager<ApplicationUser> _userManager; | |||||
private readonly IConfiguration _configuration; | |||||
public AccountController( | |||||
//InMemoryUserLoginService loginService, | |||||
ILoginService<ApplicationUser> loginService, | |||||
IIdentityServerInteractionService interaction, | |||||
IClientStore clientStore, | |||||
ILogger<AccountController> logger, | |||||
UserManager<ApplicationUser> userManager, | |||||
IConfiguration configuration) | |||||
{ | |||||
_loginService = loginService; | |||||
_interaction = interaction; | |||||
_clientStore = clientStore; | |||||
_logger = logger; | |||||
_userManager = userManager; | |||||
_configuration = configuration; | |||||
} | |||||
/// <summary> | |||||
/// Show login page | |||||
/// </summary> | |||||
[HttpGet] | |||||
public async Task<IActionResult> Login(string returnUrl) | |||||
{ | |||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl); | |||||
if (context?.IdP != null) | |||||
{ | |||||
throw new NotImplementedException("External login is not implemented!"); | |||||
} | |||||
var vm = await BuildLoginViewModelAsync(returnUrl, context); | |||||
ViewData["ReturnUrl"] = returnUrl; | |||||
return View(vm); | |||||
} | |||||
/// <summary> | |||||
/// Handle postback from username/password login | |||||
/// </summary> | |||||
[HttpPost] | |||||
[ValidateAntiForgeryToken] | |||||
public async Task<IActionResult> Login(LoginViewModel model) | |||||
{ | |||||
if (ModelState.IsValid) | |||||
{ | |||||
var user = await _loginService.FindByUsername(model.Email); | |||||
if (await _loginService.ValidateCredentials(user, model.Password)) | |||||
{ | |||||
var tokenLifetime = _configuration.GetValue("TokenLifetimeMinutes", 120); | |||||
var props = new AuthenticationProperties | |||||
{ | |||||
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(tokenLifetime), | |||||
AllowRefresh = true, | |||||
RedirectUri = model.ReturnUrl | |||||
}; | |||||
if (model.RememberMe) | |||||
{ | |||||
var permanentTokenLifetime = _configuration.GetValue("PermanentTokenLifetimeDays", 365); | |||||
props.ExpiresUtc = DateTimeOffset.UtcNow.AddDays(permanentTokenLifetime); | |||||
props.IsPersistent = true; | |||||
}; | |||||
await _loginService.SignInAsync(user, props); | |||||
// make sure the returnUrl is still valid, and if yes - redirect back to authorize endpoint | |||||
if (_interaction.IsValidReturnUrl(model.ReturnUrl)) | |||||
{ | |||||
return Redirect(model.ReturnUrl); | |||||
} | |||||
return Redirect("~/"); | |||||
} | |||||
ModelState.AddModelError("", "Invalid username or password."); | |||||
} | |||||
// something went wrong, show form with error | |||||
var vm = await BuildLoginViewModelAsync(model); | |||||
ViewData["ReturnUrl"] = model.ReturnUrl; | |||||
return View(vm); | |||||
} | |||||
private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl, AuthorizationRequest context) | |||||
{ | |||||
var allowLocal = true; | |||||
if (context?.Client.ClientId != null) | |||||
{ | |||||
var client = await _clientStore.FindEnabledClientByIdAsync(context.Client.ClientId); | |||||
if (client != null) | |||||
{ | |||||
allowLocal = client.EnableLocalLogin; | |||||
} | |||||
} | |||||
return new LoginViewModel | |||||
{ | |||||
ReturnUrl = returnUrl, | |||||
Email = context?.LoginHint, | |||||
}; | |||||
} | |||||
private async Task<LoginViewModel> BuildLoginViewModelAsync(LoginViewModel model) | |||||
{ | |||||
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); | |||||
var vm = await BuildLoginViewModelAsync(model.ReturnUrl, context); | |||||
vm.Email = model.Email; | |||||
vm.RememberMe = model.RememberMe; | |||||
return vm; | |||||
} | |||||
/// <summary> | |||||
/// Show logout page | |||||
/// </summary> | |||||
[HttpGet] | |||||
public async Task<IActionResult> Logout(string logoutId) | |||||
{ | |||||
if (User.Identity.IsAuthenticated == false) | |||||
{ | |||||
// if the user is not authenticated, then just show logged out page | |||||
return await Logout(new LogoutViewModel { LogoutId = logoutId }); | |||||
} | |||||
//Test for Xamarin. | |||||
var context = await _interaction.GetLogoutContextAsync(logoutId); | |||||
if (context?.ShowSignoutPrompt == false) | |||||
{ | |||||
//it's safe to automatically sign-out | |||||
return await Logout(new LogoutViewModel { LogoutId = logoutId }); | |||||
} | |||||
// show the logout prompt. this prevents attacks where the user | |||||
// is automatically signed out by another malicious web page. | |||||
var vm = new LogoutViewModel | |||||
{ | |||||
LogoutId = logoutId | |||||
}; | |||||
return View(vm); | |||||
} | |||||
/// <summary> | |||||
/// Handle logout page postback | |||||
/// </summary> | |||||
[HttpPost] | |||||
[ValidateAntiForgeryToken] | |||||
public async Task<IActionResult> Logout(LogoutViewModel model) | |||||
{ | |||||
var idp = User?.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; | |||||
if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider) | |||||
{ | |||||
if (model.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 | |||||
model.LogoutId = await _interaction.CreateLogoutContextAsync(); | |||||
} | |||||
string url = "/Account/Logout?logoutId=" + model.LogoutId; | |||||
try | |||||
{ | |||||
// hack: try/catch to handle social providers that throw | |||||
await HttpContext.SignOutAsync(idp, new AuthenticationProperties | |||||
{ | |||||
RedirectUri = url | |||||
}); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogError(ex, "LOGOUT ERROR: {ExceptionMessage}", ex.Message); | |||||
} | |||||
} | |||||
// delete authentication cookie | |||||
await HttpContext.SignOutAsync(); | |||||
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme); | |||||
// set this so UI rendering sees an anonymous user | |||||
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); | |||||
// get context information (client name, post logout redirect URI and iframe for federated signout) | |||||
var logout = await _interaction.GetLogoutContextAsync(model.LogoutId); | |||||
return Redirect(logout?.PostLogoutRedirectUri); | |||||
} | |||||
public async Task<IActionResult> DeviceLogOut(string redirectUrl) | |||||
{ | |||||
// delete authentication cookie | |||||
await HttpContext.SignOutAsync(); | |||||
// set this so UI rendering sees an anonymous user | |||||
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); | |||||
return Redirect(redirectUrl); | |||||
} | |||||
// GET: /Account/Register | |||||
[HttpGet] | |||||
[AllowAnonymous] | |||||
public IActionResult Register(string returnUrl = null) | |||||
{ | |||||
ViewData["ReturnUrl"] = returnUrl; | |||||
return View(); | |||||
} | |||||
// | |||||
// POST: /Account/Register | |||||
[HttpPost] | |||||
[AllowAnonymous] | |||||
[ValidateAntiForgeryToken] | |||||
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null) | |||||
{ | |||||
ViewData["ReturnUrl"] = returnUrl; | |||||
if (ModelState.IsValid) | |||||
{ | |||||
var user = new ApplicationUser | |||||
{ | |||||
UserName = model.Email, | |||||
Email = model.Email, | |||||
CardHolderName = model.User.CardHolderName, | |||||
CardNumber = model.User.CardNumber, | |||||
CardType = model.User.CardType, | |||||
City = model.User.City, | |||||
Country = model.User.Country, | |||||
Expiration = model.User.Expiration, | |||||
LastName = model.User.LastName, | |||||
Name = model.User.Name, | |||||
Street = model.User.Street, | |||||
State = model.User.State, | |||||
ZipCode = model.User.ZipCode, | |||||
PhoneNumber = model.User.PhoneNumber, | |||||
SecurityNumber = model.User.SecurityNumber | |||||
}; | |||||
var result = await _userManager.CreateAsync(user, model.Password); | |||||
if (result.Errors.Count() > 0) | |||||
{ | |||||
AddErrors(result); | |||||
// If we got this far, something failed, redisplay form | |||||
return View(model); | |||||
} | |||||
} | |||||
if (returnUrl != null) | |||||
{ | |||||
if (HttpContext.User.Identity.IsAuthenticated) | |||||
return Redirect(returnUrl); | |||||
else | |||||
if (ModelState.IsValid) | |||||
return RedirectToAction("login", "account", new { returnUrl = returnUrl }); | |||||
else | |||||
return View(model); | |||||
} | |||||
return RedirectToAction("index", "home"); | |||||
} | |||||
[HttpGet] | |||||
public IActionResult Redirecting() | |||||
{ | |||||
return View(); | |||||
} | |||||
private void AddErrors(IdentityResult result) | |||||
{ | |||||
foreach (var error in result.Errors) | |||||
{ | |||||
ModelState.AddModelError(string.Empty, error.Description); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -1,249 +0,0 @@ | |||||
using Duende.IdentityServer.Events; | |||||
using Duende.IdentityServer.Extensions; | |||||
using Identity.API.Extensions; | |||||
using Microsoft.eShopOnContainers.Services.Identity.API.Models.ConsentViewModels; | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers | |||||
{ | |||||
/// <summary> | |||||
/// This controller implements the consent logic | |||||
/// </summary> | |||||
public class ConsentController : Controller | |||||
{ | |||||
private readonly ILogger<ConsentController> _logger; | |||||
private readonly IIdentityServerInteractionService _interaction; | |||||
private readonly IEventService _events; | |||||
public ConsentController( | |||||
ILogger<ConsentController> logger, | |||||
IIdentityServerInteractionService interaction, | |||||
IEventService events) | |||||
{ | |||||
_interaction = interaction; | |||||
_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) | |||||
{ | |||||
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<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 = 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 != Duende.IdentityServer.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<ConsentViewModel> 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<string>(), | |||||
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<ScopeViewModel>(); | |||||
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(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 = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess, | |||||
DisplayName = ConsentOptions.OfflineAccessDisplayName, | |||||
Description = ConsentOptions.OfflineAccessDescription, | |||||
Emphasize = true, | |||||
Checked = check | |||||
}; | |||||
} | |||||
} | |||||
} |
@ -1,46 +0,0 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API.Controllers | |||||
{ | |||||
public class HomeController : Controller | |||||
{ | |||||
private readonly IIdentityServerInteractionService _interaction; | |||||
private readonly IOptionsSnapshot<AppSettings> _settings; | |||||
private readonly IRedirectService _redirectSvc; | |||||
public HomeController(IIdentityServerInteractionService interaction, IOptionsSnapshot<AppSettings> settings, IRedirectService redirectSvc) | |||||
{ | |||||
_interaction = interaction; | |||||
_settings = settings; | |||||
_redirectSvc = redirectSvc; | |||||
} | |||||
public IActionResult Index(string returnUrl) | |||||
{ | |||||
return View(); | |||||
} | |||||
public IActionResult ReturnToOriginalApplication(string returnUrl) | |||||
{ | |||||
if (returnUrl != null) | |||||
return Redirect(_redirectSvc.ExtractRedirectUriFromReturnUrl(returnUrl)); | |||||
else | |||||
return RedirectToAction("Index", "Home"); | |||||
} | |||||
/// <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; | |||||
} | |||||
return View("Error", vm); | |||||
} | |||||
} | |||||
} |
@ -1,18 +1,17 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API.Data | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API.Data; | |||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> | |||||
{ | { | ||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> | |||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) | |||||
: base(options) | |||||
{ | { | ||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) | |||||
: base(options) | |||||
{ | |||||
} | |||||
} | |||||
protected override void OnModelCreating(ModelBuilder builder) | |||||
{ | |||||
base.OnModelCreating(builder); | |||||
// Customize the ASP.NET Identity model and override the defaults if needed. | |||||
// For example, you can rename the ASP.NET Identity table names and more. | |||||
// Add your customizations after calling base.OnModelCreating(builder); | |||||
} | |||||
protected override void OnModelCreating(ModelBuilder builder) | |||||
{ | |||||
base.OnModelCreating(builder); | |||||
// Customize the ASP.NET Identity model and override the defaults if needed. | |||||
// For example, you can rename the ASP.NET Identity table names and more. | |||||
// Add your customizations after calling base.OnModelCreating(builder); | |||||
} | } | ||||
} | } |
@ -1,218 +0,0 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API.Data | |||||
{ | |||||
using Microsoft.Extensions.Logging; | |||||
public class ApplicationDbContextSeed | |||||
{ | |||||
private readonly IPasswordHasher<ApplicationUser> _passwordHasher = new PasswordHasher<ApplicationUser>(); | |||||
public async Task SeedAsync(ApplicationDbContext context, IWebHostEnvironment env, | |||||
ILogger<ApplicationDbContextSeed> logger, IOptions<AppSettings> settings, int? retry = 0) | |||||
{ | |||||
int retryForAvaiability = retry.Value; | |||||
try | |||||
{ | |||||
var useCustomizationData = settings.Value.UseCustomizationData; | |||||
var contentRootPath = env.ContentRootPath; | |||||
var webroot = env.WebRootPath; | |||||
if (!context.Users.Any()) | |||||
{ | |||||
context.Users.AddRange(useCustomizationData | |||||
? GetUsersFromFile(contentRootPath, logger) | |||||
: GetDefaultUser()); | |||||
await context.SaveChangesAsync(); | |||||
} | |||||
if (useCustomizationData) | |||||
{ | |||||
GetPreconfiguredImages(contentRootPath, webroot, logger); | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
if (retryForAvaiability < 10) | |||||
{ | |||||
retryForAvaiability++; | |||||
logger.LogError(ex, "EXCEPTION ERROR while migrating {DbContextName}", nameof(ApplicationDbContext)); | |||||
await SeedAsync(context, env, logger, settings, retryForAvaiability); | |||||
} | |||||
} | |||||
} | |||||
private IEnumerable<ApplicationUser> GetUsersFromFile(string contentRootPath, ILogger logger) | |||||
{ | |||||
string csvFileUsers = Path.Combine(contentRootPath, "Setup", "Users.csv"); | |||||
if (!File.Exists(csvFileUsers)) | |||||
{ | |||||
return GetDefaultUser(); | |||||
} | |||||
string[] csvheaders; | |||||
try | |||||
{ | |||||
string[] requiredHeaders = { | |||||
"cardholdername", "cardnumber", "cardtype", "city", "country", | |||||
"email", "expiration", "lastname", "name", "phonenumber", | |||||
"username", "zipcode", "state", "street", "securitynumber", | |||||
"normalizedemail", "normalizedusername", "password" | |||||
}; | |||||
csvheaders = GetHeaders(requiredHeaders, csvFileUsers); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | |||||
return GetDefaultUser(); | |||||
} | |||||
List<ApplicationUser> users = File.ReadAllLines(csvFileUsers) | |||||
.Skip(1) // skip header column | |||||
.Select(row => Regex.Split(row, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) | |||||
.SelectTry(column => CreateApplicationUser(column, csvheaders)) | |||||
.OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) | |||||
.Where(x => x != null) | |||||
.ToList(); | |||||
return users; | |||||
} | |||||
private ApplicationUser CreateApplicationUser(string[] column, string[] headers) | |||||
{ | |||||
if (column.Count() != headers.Count()) | |||||
{ | |||||
throw new Exception($"column count '{column.Count()}' not the same as headers count'{headers.Count()}'"); | |||||
} | |||||
string cardtypeString = column[Array.IndexOf(headers, "cardtype")].Trim('"').Trim(); | |||||
if (!int.TryParse(cardtypeString, out int cardtype)) | |||||
{ | |||||
throw new Exception($"cardtype='{cardtypeString}' is not a number"); | |||||
} | |||||
var user = new ApplicationUser | |||||
{ | |||||
CardHolderName = column[Array.IndexOf(headers, "cardholdername")].Trim('"').Trim(), | |||||
CardNumber = column[Array.IndexOf(headers, "cardnumber")].Trim('"').Trim(), | |||||
CardType = cardtype, | |||||
City = column[Array.IndexOf(headers, "city")].Trim('"').Trim(), | |||||
Country = column[Array.IndexOf(headers, "country")].Trim('"').Trim(), | |||||
Email = column[Array.IndexOf(headers, "email")].Trim('"').Trim(), | |||||
Expiration = column[Array.IndexOf(headers, "expiration")].Trim('"').Trim(), | |||||
Id = Guid.NewGuid().ToString(), | |||||
LastName = column[Array.IndexOf(headers, "lastname")].Trim('"').Trim(), | |||||
Name = column[Array.IndexOf(headers, "name")].Trim('"').Trim(), | |||||
PhoneNumber = column[Array.IndexOf(headers, "phonenumber")].Trim('"').Trim(), | |||||
UserName = column[Array.IndexOf(headers, "username")].Trim('"').Trim(), | |||||
ZipCode = column[Array.IndexOf(headers, "zipcode")].Trim('"').Trim(), | |||||
State = column[Array.IndexOf(headers, "state")].Trim('"').Trim(), | |||||
Street = column[Array.IndexOf(headers, "street")].Trim('"').Trim(), | |||||
SecurityNumber = column[Array.IndexOf(headers, "securitynumber")].Trim('"').Trim(), | |||||
NormalizedEmail = column[Array.IndexOf(headers, "normalizedemail")].Trim('"').Trim(), | |||||
NormalizedUserName = column[Array.IndexOf(headers, "normalizedusername")].Trim('"').Trim(), | |||||
SecurityStamp = Guid.NewGuid().ToString("D"), | |||||
PasswordHash = column[Array.IndexOf(headers, "password")].Trim('"').Trim(), // Note: This is the password | |||||
}; | |||||
user.PasswordHash = _passwordHasher.HashPassword(user, user.PasswordHash); | |||||
return user; | |||||
} | |||||
private IEnumerable<ApplicationUser> GetDefaultUser() | |||||
{ | |||||
var user = | |||||
new ApplicationUser() | |||||
{ | |||||
CardHolderName = "DemoUser", | |||||
CardNumber = "4012888888881881", | |||||
CardType = 1, | |||||
City = "Redmond", | |||||
Country = "U.S.", | |||||
Email = "demouser@microsoft.com", | |||||
Expiration = "12/25", | |||||
Id = Guid.NewGuid().ToString(), | |||||
LastName = "DemoLastName", | |||||
Name = "DemoUser", | |||||
PhoneNumber = "1234567890", | |||||
UserName = "demouser@microsoft.com", | |||||
ZipCode = "98052", | |||||
State = "WA", | |||||
Street = "15703 NE 61st Ct", | |||||
SecurityNumber = "535", | |||||
NormalizedEmail = "DEMOUSER@MICROSOFT.COM", | |||||
NormalizedUserName = "DEMOUSER@MICROSOFT.COM", | |||||
SecurityStamp = Guid.NewGuid().ToString("D"), | |||||
}; | |||||
user.PasswordHash = _passwordHasher.HashPassword(user, "Pass@word1"); | |||||
return new List<ApplicationUser>() | |||||
{ | |||||
user | |||||
}; | |||||
} | |||||
static string[] GetHeaders(string[] requiredHeaders, string csvfile) | |||||
{ | |||||
string[] csvheaders = File.ReadLines(csvfile).First().ToLowerInvariant().Split(','); | |||||
if (csvheaders.Count() != requiredHeaders.Count()) | |||||
{ | |||||
throw new Exception($"requiredHeader count '{ requiredHeaders.Count()}' is different then read header '{csvheaders.Count()}'"); | |||||
} | |||||
foreach (var requiredHeader in requiredHeaders) | |||||
{ | |||||
if (!csvheaders.Contains(requiredHeader)) | |||||
{ | |||||
throw new Exception($"does not contain required header '{requiredHeader}'"); | |||||
} | |||||
} | |||||
return csvheaders; | |||||
} | |||||
static void GetPreconfiguredImages(string contentRootPath, string webroot, ILogger logger) | |||||
{ | |||||
try | |||||
{ | |||||
string imagesZipFile = Path.Combine(contentRootPath, "Setup", "images.zip"); | |||||
if (!File.Exists(imagesZipFile)) | |||||
{ | |||||
logger.LogError("Zip file '{ZipFileName}' does not exists.", imagesZipFile); | |||||
return; | |||||
} | |||||
string imagePath = Path.Combine(webroot, "images"); | |||||
string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray(); | |||||
using ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read); | |||||
foreach (ZipArchiveEntry entry in zip.Entries) | |||||
{ | |||||
if (imageFiles.Contains(entry.Name)) | |||||
{ | |||||
string destinationFilename = Path.Combine(imagePath, entry.Name); | |||||
if (File.Exists(destinationFilename)) | |||||
{ | |||||
File.Delete(destinationFilename); | |||||
} | |||||
entry.ExtractToFile(destinationFilename); | |||||
} | |||||
else | |||||
{ | |||||
logger.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile); | |||||
} | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); ; | |||||
} | |||||
} | |||||
} | |||||
} |
@ -1,83 +0,0 @@ | |||||
using Duende.IdentityServer.EntityFramework.Entities; | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API.Data | |||||
{ | |||||
public class ConfigurationDbContextSeed | |||||
{ | |||||
public async Task SeedAsync(ConfigurationDbContext context, IConfiguration configuration) | |||||
{ | |||||
//callbacks urls from config: | |||||
var clientUrls = new Dictionary<string, string>(); | |||||
clientUrls.Add("Mvc", configuration.GetValue<string>("MvcClient")); | |||||
clientUrls.Add("Spa", configuration.GetValue<string>("SpaClient")); | |||||
clientUrls.Add("Xamarin", configuration.GetValue<string>("XamarinCallback")); | |||||
clientUrls.Add("BasketApi", configuration.GetValue<string>("BasketApiClient")); | |||||
clientUrls.Add("OrderingApi", configuration.GetValue<string>("OrderingApiClient")); | |||||
clientUrls.Add("MobileShoppingAgg", configuration.GetValue<string>("MobileShoppingAggClient")); | |||||
clientUrls.Add("WebShoppingAgg", configuration.GetValue<string>("WebShoppingAggClient")); | |||||
clientUrls.Add("WebhooksApi", configuration.GetValue<string>("WebhooksApiClient")); | |||||
clientUrls.Add("WebhooksWeb", configuration.GetValue<string>("WebhooksWebClient")); | |||||
if (!context.Clients.Any()) | |||||
{ | |||||
foreach (var client in Config.GetClients(clientUrls)) | |||||
{ | |||||
context.Clients.Add(client.ToEntity()); | |||||
} | |||||
await context.SaveChangesAsync(); | |||||
} | |||||
// Checking always for old redirects to fix existing deployments | |||||
// to use new swagger-ui redirect uri as of v3.0.0 | |||||
// There should be no problem for new ones | |||||
// ref: https://github.com/dotnet-architecture/eShopOnContainers/issues/586 | |||||
else | |||||
{ | |||||
List<ClientRedirectUri> oldRedirects = (await context.Clients.Include(c => c.RedirectUris).ToListAsync()) | |||||
.SelectMany(c => c.RedirectUris) | |||||
.Where(ru => ru.RedirectUri.EndsWith("/o2c.html")) | |||||
.ToList(); | |||||
if (oldRedirects.Any()) | |||||
{ | |||||
foreach (var ru in oldRedirects) | |||||
{ | |||||
ru.RedirectUri = ru.RedirectUri.Replace("/o2c.html", "/oauth2-redirect.html"); | |||||
context.Update(ru.Client); | |||||
} | |||||
await context.SaveChangesAsync(); | |||||
} | |||||
} | |||||
if (!context.IdentityResources.Any()) | |||||
{ | |||||
foreach (var resource in Config.GetResources()) | |||||
{ | |||||
context.IdentityResources.Add(resource.ToEntity()); | |||||
} | |||||
await context.SaveChangesAsync(); | |||||
} | |||||
if (!context.ApiResources.Any()) | |||||
{ | |||||
foreach (var api in Config.GetApis()) | |||||
{ | |||||
context.ApiResources.Add(api.ToEntity()); | |||||
} | |||||
await context.SaveChangesAsync(); | |||||
} | |||||
if (!context.ApiScopes.Any()) | |||||
{ | |||||
foreach (var apiScope in Config.GetApiScopes()) | |||||
{ | |||||
context.ApiScopes.Add(apiScope.ToEntity()); | |||||
} | |||||
await context.SaveChangesAsync(); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -1,7 +1,7 @@ | |||||
using System; | using System; | ||||
using Microsoft.EntityFrameworkCore.Migrations; | using Microsoft.EntityFrameworkCore.Migrations; | ||||
namespace Identity.API.Migrations | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API.Data.Migrations | |||||
{ | { | ||||
public partial class InitialMigration : Migration | public partial class InitialMigration : Migration | ||||
{ | { |
@ -1,23 +0,0 @@ | |||||
namespace Identity.API.Extensions | |||||
{ | |||||
public static class Extensions | |||||
{ | |||||
/// <summary> | |||||
/// Checks if the redirect URI is for a native client. | |||||
/// </summary> | |||||
/// <returns></returns> | |||||
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 }); | |||||
} | |||||
} | |||||
} |
@ -1,46 +0,0 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API.Extensions | |||||
{ | |||||
public static class LinqSelectExtensions | |||||
{ | |||||
public static IEnumerable<SelectTryResult<TSource, TResult>> SelectTry<TSource, TResult>(this IEnumerable<TSource> enumerable, Func<TSource, TResult> selector) | |||||
{ | |||||
foreach (TSource element in enumerable) | |||||
{ | |||||
SelectTryResult<TSource, TResult> returnedValue; | |||||
try | |||||
{ | |||||
returnedValue = new SelectTryResult<TSource, TResult>(element, selector(element), null); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
returnedValue = new SelectTryResult<TSource, TResult>(element, default(TResult), ex); | |||||
} | |||||
yield return returnedValue; | |||||
} | |||||
} | |||||
public static IEnumerable<TResult> OnCaughtException<TSource, TResult>(this IEnumerable<SelectTryResult<TSource, TResult>> enumerable, Func<Exception, TResult> exceptionHandler) | |||||
{ | |||||
return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.CaughtException)); | |||||
} | |||||
public static IEnumerable<TResult> OnCaughtException<TSource, TResult>(this IEnumerable<SelectTryResult<TSource, TResult>> enumerable, Func<TSource, Exception, TResult> exceptionHandler) | |||||
{ | |||||
return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.Source, x.CaughtException)); | |||||
} | |||||
public class SelectTryResult<TSource, TResult> | |||||
{ | |||||
internal SelectTryResult(TSource source, TResult result, Exception exception) | |||||
{ | |||||
Source = source; | |||||
Result = result; | |||||
CaughtException = exception; | |||||
} | |||||
public TSource Source { get; private set; } | |||||
public TResult Result { get; private set; } | |||||
public Exception CaughtException { get; private set; } | |||||
} | |||||
} | |||||
} |
@ -1,20 +0,0 @@ | |||||
namespace Identity.API.Factories | |||||
{ | |||||
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext> | |||||
{ | |||||
public ApplicationDbContext CreateDbContext(string[] args) | |||||
{ | |||||
var config = new ConfigurationBuilder() | |||||
.SetBasePath(Path.Combine(Directory.GetCurrentDirectory())) | |||||
.AddJsonFile("appsettings.json") | |||||
.AddEnvironmentVariables() | |||||
.Build(); | |||||
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>(); | |||||
optionsBuilder.UseSqlServer(config["ConnectionString"], sqlServerOptionsAction: o => o.MigrationsAssembly("Identity.API")); | |||||
return new ApplicationDbContext(optionsBuilder.Options); | |||||
} | |||||
} | |||||
} |
@ -1,21 +0,0 @@ | |||||
namespace Identity.API.Factories | |||||
{ | |||||
public class ConfigurationDbContextFactory : IDesignTimeDbContextFactory<ConfigurationDbContext> | |||||
{ | |||||
public ConfigurationDbContext CreateDbContext(string[] args) | |||||
{ | |||||
var config = new ConfigurationBuilder() | |||||
.SetBasePath(Path.Combine(Directory.GetCurrentDirectory())) | |||||
.AddJsonFile("appsettings.json") | |||||
.AddEnvironmentVariables() | |||||
.Build(); | |||||
var optionsBuilder = new DbContextOptionsBuilder<ConfigurationDbContext>(); | |||||
var storeOptions = new ConfigurationStoreOptions(); | |||||
optionsBuilder.UseSqlServer(config["ConnectionString"], sqlServerOptionsAction: o => o.MigrationsAssembly("Identity.API")); | |||||
return new ConfigurationDbContext(optionsBuilder.Options); | |||||
} | |||||
} | |||||
} |
@ -1,21 +0,0 @@ | |||||
namespace Identity.API.Factories | |||||
{ | |||||
public class PersistedGrantDbContextFactory : IDesignTimeDbContextFactory<PersistedGrantDbContext> | |||||
{ | |||||
public PersistedGrantDbContext CreateDbContext(string[] args) | |||||
{ | |||||
var config = new ConfigurationBuilder() | |||||
.SetBasePath(Path.Combine(Directory.GetCurrentDirectory())) | |||||
.AddJsonFile("appsettings.json") | |||||
.AddEnvironmentVariables() | |||||
.Build(); | |||||
var optionsBuilder = new DbContextOptionsBuilder<PersistedGrantDbContext>(); | |||||
var operationOptions = new OperationalStoreOptions(); | |||||
optionsBuilder.UseSqlServer(config["ConnectionString"], sqlServerOptionsAction: o => o.MigrationsAssembly("Identity.API")); | |||||
return new PersistedGrantDbContext(optionsBuilder.Options); | |||||
} | |||||
} | |||||
} |
@ -1,712 +0,0 @@ | |||||
using System; | |||||
using Microsoft.EntityFrameworkCore.Migrations; | |||||
#nullable disable | |||||
namespace Identity.API.Migrations.ConfigurationDb | |||||
{ | |||||
public partial class Configuration : Migration | |||||
{ | |||||
protected override void Up(MigrationBuilder migrationBuilder) | |||||
{ | |||||
migrationBuilder.CreateTable( | |||||
name: "ApiResources", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Enabled = table.Column<bool>(type: "bit", nullable: false), | |||||
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
DisplayName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true), | |||||
AllowedAccessTokenSigningAlgorithms = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true), | |||||
ShowInDiscoveryDocument = table.Column<bool>(type: "bit", nullable: false), | |||||
RequireResourceIndicator = table.Column<bool>(type: "bit", nullable: false), | |||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Updated = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
LastAccessed = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
NonEditable = table.Column<bool>(type: "bit", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ApiResources", x => x.Id); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ApiScopes", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Enabled = table.Column<bool>(type: "bit", nullable: false), | |||||
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
DisplayName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true), | |||||
Required = table.Column<bool>(type: "bit", nullable: false), | |||||
Emphasize = table.Column<bool>(type: "bit", nullable: false), | |||||
ShowInDiscoveryDocument = table.Column<bool>(type: "bit", nullable: false), | |||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Updated = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
LastAccessed = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
NonEditable = table.Column<bool>(type: "bit", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ApiScopes", x => x.Id); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "Clients", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Enabled = table.Column<bool>(type: "bit", nullable: false), | |||||
ClientId = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
ProtocolType = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
RequireClientSecret = table.Column<bool>(type: "bit", nullable: false), | |||||
ClientName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true), | |||||
ClientUri = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true), | |||||
LogoUri = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true), | |||||
RequireConsent = table.Column<bool>(type: "bit", nullable: false), | |||||
AllowRememberConsent = table.Column<bool>(type: "bit", nullable: false), | |||||
AlwaysIncludeUserClaimsInIdToken = table.Column<bool>(type: "bit", nullable: false), | |||||
RequirePkce = table.Column<bool>(type: "bit", nullable: false), | |||||
AllowPlainTextPkce = table.Column<bool>(type: "bit", nullable: false), | |||||
RequireRequestObject = table.Column<bool>(type: "bit", nullable: false), | |||||
AllowAccessTokensViaBrowser = table.Column<bool>(type: "bit", nullable: false), | |||||
FrontChannelLogoutUri = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true), | |||||
FrontChannelLogoutSessionRequired = table.Column<bool>(type: "bit", nullable: false), | |||||
BackChannelLogoutUri = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true), | |||||
BackChannelLogoutSessionRequired = table.Column<bool>(type: "bit", nullable: false), | |||||
AllowOfflineAccess = table.Column<bool>(type: "bit", nullable: false), | |||||
IdentityTokenLifetime = table.Column<int>(type: "int", nullable: false), | |||||
AllowedIdentityTokenSigningAlgorithms = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true), | |||||
AccessTokenLifetime = table.Column<int>(type: "int", nullable: false), | |||||
AuthorizationCodeLifetime = table.Column<int>(type: "int", nullable: false), | |||||
ConsentLifetime = table.Column<int>(type: "int", nullable: true), | |||||
AbsoluteRefreshTokenLifetime = table.Column<int>(type: "int", nullable: false), | |||||
SlidingRefreshTokenLifetime = table.Column<int>(type: "int", nullable: false), | |||||
RefreshTokenUsage = table.Column<int>(type: "int", nullable: false), | |||||
UpdateAccessTokenClaimsOnRefresh = table.Column<bool>(type: "bit", nullable: false), | |||||
RefreshTokenExpiration = table.Column<int>(type: "int", nullable: false), | |||||
AccessTokenType = table.Column<int>(type: "int", nullable: false), | |||||
EnableLocalLogin = table.Column<bool>(type: "bit", nullable: false), | |||||
IncludeJwtId = table.Column<bool>(type: "bit", nullable: false), | |||||
AlwaysSendClientClaims = table.Column<bool>(type: "bit", nullable: false), | |||||
ClientClaimsPrefix = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
PairWiseSubjectSalt = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
UserSsoLifetime = table.Column<int>(type: "int", nullable: true), | |||||
UserCodeType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true), | |||||
DeviceCodeLifetime = table.Column<int>(type: "int", nullable: false), | |||||
CibaLifetime = table.Column<int>(type: "int", nullable: true), | |||||
PollingInterval = table.Column<int>(type: "int", nullable: true), | |||||
CoordinateLifetimeWithUserSession = table.Column<bool>(type: "bit", nullable: true), | |||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Updated = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
LastAccessed = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
NonEditable = table.Column<bool>(type: "bit", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_Clients", x => x.Id); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "IdentityProviders", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Scheme = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
DisplayName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
Enabled = table.Column<bool>(type: "bit", nullable: false), | |||||
Type = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false), | |||||
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true), | |||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Updated = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
LastAccessed = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
NonEditable = table.Column<bool>(type: "bit", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_IdentityProviders", x => x.Id); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "IdentityResources", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Enabled = table.Column<bool>(type: "bit", nullable: false), | |||||
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
DisplayName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true), | |||||
Required = table.Column<bool>(type: "bit", nullable: false), | |||||
Emphasize = table.Column<bool>(type: "bit", nullable: false), | |||||
ShowInDiscoveryDocument = table.Column<bool>(type: "bit", nullable: false), | |||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Updated = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
NonEditable = table.Column<bool>(type: "bit", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_IdentityResources", x => x.Id); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ApiResourceClaims", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
ApiResourceId = table.Column<int>(type: "int", nullable: false), | |||||
Type = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ApiResourceClaims", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ApiResourceClaims_ApiResources_ApiResourceId", | |||||
column: x => x.ApiResourceId, | |||||
principalTable: "ApiResources", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ApiResourceProperties", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
ApiResourceId = table.Column<int>(type: "int", nullable: false), | |||||
Key = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false), | |||||
Value = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ApiResourceProperties", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ApiResourceProperties_ApiResources_ApiResourceId", | |||||
column: x => x.ApiResourceId, | |||||
principalTable: "ApiResources", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ApiResourceScopes", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Scope = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
ApiResourceId = table.Column<int>(type: "int", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ApiResourceScopes", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ApiResourceScopes_ApiResources_ApiResourceId", | |||||
column: x => x.ApiResourceId, | |||||
principalTable: "ApiResources", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ApiResourceSecrets", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
ApiResourceId = table.Column<int>(type: "int", nullable: false), | |||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true), | |||||
Value = table.Column<string>(type: "nvarchar(4000)", maxLength: 4000, nullable: false), | |||||
Expiration = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
Type = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false), | |||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ApiResourceSecrets", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ApiResourceSecrets_ApiResources_ApiResourceId", | |||||
column: x => x.ApiResourceId, | |||||
principalTable: "ApiResources", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ApiScopeClaims", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
ScopeId = table.Column<int>(type: "int", nullable: false), | |||||
Type = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ApiScopeClaims", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ApiScopeClaims_ApiScopes_ScopeId", | |||||
column: x => x.ScopeId, | |||||
principalTable: "ApiScopes", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ApiScopeProperties", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
ScopeId = table.Column<int>(type: "int", nullable: false), | |||||
Key = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false), | |||||
Value = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ApiScopeProperties", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ApiScopeProperties_ApiScopes_ScopeId", | |||||
column: x => x.ScopeId, | |||||
principalTable: "ApiScopes", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ClientClaims", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Type = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false), | |||||
Value = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false), | |||||
ClientId = table.Column<int>(type: "int", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ClientClaims", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ClientClaims_Clients_ClientId", | |||||
column: x => x.ClientId, | |||||
principalTable: "Clients", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ClientCorsOrigins", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Origin = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false), | |||||
ClientId = table.Column<int>(type: "int", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ClientCorsOrigins", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ClientCorsOrigins_Clients_ClientId", | |||||
column: x => x.ClientId, | |||||
principalTable: "Clients", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ClientGrantTypes", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
GrantType = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false), | |||||
ClientId = table.Column<int>(type: "int", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ClientGrantTypes", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ClientGrantTypes_Clients_ClientId", | |||||
column: x => x.ClientId, | |||||
principalTable: "Clients", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ClientIdPRestrictions", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Provider = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
ClientId = table.Column<int>(type: "int", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ClientIdPRestrictions", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ClientIdPRestrictions_Clients_ClientId", | |||||
column: x => x.ClientId, | |||||
principalTable: "Clients", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ClientPostLogoutRedirectUris", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
PostLogoutRedirectUri = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: false), | |||||
ClientId = table.Column<int>(type: "int", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ClientPostLogoutRedirectUris", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ClientPostLogoutRedirectUris_Clients_ClientId", | |||||
column: x => x.ClientId, | |||||
principalTable: "Clients", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ClientProperties", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
ClientId = table.Column<int>(type: "int", nullable: false), | |||||
Key = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false), | |||||
Value = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ClientProperties", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ClientProperties_Clients_ClientId", | |||||
column: x => x.ClientId, | |||||
principalTable: "Clients", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ClientRedirectUris", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
RedirectUri = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: false), | |||||
ClientId = table.Column<int>(type: "int", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ClientRedirectUris", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ClientRedirectUris_Clients_ClientId", | |||||
column: x => x.ClientId, | |||||
principalTable: "Clients", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ClientScopes", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Scope = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
ClientId = table.Column<int>(type: "int", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ClientScopes", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ClientScopes_Clients_ClientId", | |||||
column: x => x.ClientId, | |||||
principalTable: "Clients", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ClientSecrets", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
ClientId = table.Column<int>(type: "int", nullable: false), | |||||
Description = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true), | |||||
Value = table.Column<string>(type: "nvarchar(4000)", maxLength: 4000, nullable: false), | |||||
Expiration = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
Type = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false), | |||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ClientSecrets", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_ClientSecrets_Clients_ClientId", | |||||
column: x => x.ClientId, | |||||
principalTable: "Clients", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "IdentityResourceClaims", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
IdentityResourceId = table.Column<int>(type: "int", nullable: false), | |||||
Type = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_IdentityResourceClaims", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId", | |||||
column: x => x.IdentityResourceId, | |||||
principalTable: "IdentityResources", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "IdentityResourceProperties", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
IdentityResourceId = table.Column<int>(type: "int", nullable: false), | |||||
Key = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false), | |||||
Value = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_IdentityResourceProperties", x => x.Id); | |||||
table.ForeignKey( | |||||
name: "FK_IdentityResourceProperties_IdentityResources_IdentityResourceId", | |||||
column: x => x.IdentityResourceId, | |||||
principalTable: "IdentityResources", | |||||
principalColumn: "Id", | |||||
onDelete: ReferentialAction.Cascade); | |||||
}); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ApiResourceClaims_ApiResourceId_Type", | |||||
table: "ApiResourceClaims", | |||||
columns: new[] { "ApiResourceId", "Type" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ApiResourceProperties_ApiResourceId_Key", | |||||
table: "ApiResourceProperties", | |||||
columns: new[] { "ApiResourceId", "Key" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ApiResources_Name", | |||||
table: "ApiResources", | |||||
column: "Name", | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ApiResourceScopes_ApiResourceId_Scope", | |||||
table: "ApiResourceScopes", | |||||
columns: new[] { "ApiResourceId", "Scope" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ApiResourceSecrets_ApiResourceId", | |||||
table: "ApiResourceSecrets", | |||||
column: "ApiResourceId"); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ApiScopeClaims_ScopeId_Type", | |||||
table: "ApiScopeClaims", | |||||
columns: new[] { "ScopeId", "Type" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ApiScopeProperties_ScopeId_Key", | |||||
table: "ApiScopeProperties", | |||||
columns: new[] { "ScopeId", "Key" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ApiScopes_Name", | |||||
table: "ApiScopes", | |||||
column: "Name", | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ClientClaims_ClientId_Type_Value", | |||||
table: "ClientClaims", | |||||
columns: new[] { "ClientId", "Type", "Value" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ClientCorsOrigins_ClientId_Origin", | |||||
table: "ClientCorsOrigins", | |||||
columns: new[] { "ClientId", "Origin" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ClientGrantTypes_ClientId_GrantType", | |||||
table: "ClientGrantTypes", | |||||
columns: new[] { "ClientId", "GrantType" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ClientIdPRestrictions_ClientId_Provider", | |||||
table: "ClientIdPRestrictions", | |||||
columns: new[] { "ClientId", "Provider" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ClientPostLogoutRedirectUris_ClientId_PostLogoutRedirectUri", | |||||
table: "ClientPostLogoutRedirectUris", | |||||
columns: new[] { "ClientId", "PostLogoutRedirectUri" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ClientProperties_ClientId_Key", | |||||
table: "ClientProperties", | |||||
columns: new[] { "ClientId", "Key" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ClientRedirectUris_ClientId_RedirectUri", | |||||
table: "ClientRedirectUris", | |||||
columns: new[] { "ClientId", "RedirectUri" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_Clients_ClientId", | |||||
table: "Clients", | |||||
column: "ClientId", | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ClientScopes_ClientId_Scope", | |||||
table: "ClientScopes", | |||||
columns: new[] { "ClientId", "Scope" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ClientSecrets_ClientId", | |||||
table: "ClientSecrets", | |||||
column: "ClientId"); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_IdentityProviders_Scheme", | |||||
table: "IdentityProviders", | |||||
column: "Scheme", | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_IdentityResourceClaims_IdentityResourceId_Type", | |||||
table: "IdentityResourceClaims", | |||||
columns: new[] { "IdentityResourceId", "Type" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_IdentityResourceProperties_IdentityResourceId_Key", | |||||
table: "IdentityResourceProperties", | |||||
columns: new[] { "IdentityResourceId", "Key" }, | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_IdentityResources_Name", | |||||
table: "IdentityResources", | |||||
column: "Name", | |||||
unique: true); | |||||
} | |||||
protected override void Down(MigrationBuilder migrationBuilder) | |||||
{ | |||||
migrationBuilder.DropTable( | |||||
name: "ApiResourceClaims"); | |||||
migrationBuilder.DropTable( | |||||
name: "ApiResourceProperties"); | |||||
migrationBuilder.DropTable( | |||||
name: "ApiResourceScopes"); | |||||
migrationBuilder.DropTable( | |||||
name: "ApiResourceSecrets"); | |||||
migrationBuilder.DropTable( | |||||
name: "ApiScopeClaims"); | |||||
migrationBuilder.DropTable( | |||||
name: "ApiScopeProperties"); | |||||
migrationBuilder.DropTable( | |||||
name: "ClientClaims"); | |||||
migrationBuilder.DropTable( | |||||
name: "ClientCorsOrigins"); | |||||
migrationBuilder.DropTable( | |||||
name: "ClientGrantTypes"); | |||||
migrationBuilder.DropTable( | |||||
name: "ClientIdPRestrictions"); | |||||
migrationBuilder.DropTable( | |||||
name: "ClientPostLogoutRedirectUris"); | |||||
migrationBuilder.DropTable( | |||||
name: "ClientProperties"); | |||||
migrationBuilder.DropTable( | |||||
name: "ClientRedirectUris"); | |||||
migrationBuilder.DropTable( | |||||
name: "ClientScopes"); | |||||
migrationBuilder.DropTable( | |||||
name: "ClientSecrets"); | |||||
migrationBuilder.DropTable( | |||||
name: "IdentityProviders"); | |||||
migrationBuilder.DropTable( | |||||
name: "IdentityResourceClaims"); | |||||
migrationBuilder.DropTable( | |||||
name: "IdentityResourceProperties"); | |||||
migrationBuilder.DropTable( | |||||
name: "ApiResources"); | |||||
migrationBuilder.DropTable( | |||||
name: "ApiScopes"); | |||||
migrationBuilder.DropTable( | |||||
name: "Clients"); | |||||
migrationBuilder.DropTable( | |||||
name: "IdentityResources"); | |||||
} | |||||
} | |||||
} |
@ -1,240 +0,0 @@ | |||||
// <auto-generated /> | |||||
using System; | |||||
using Duende.IdentityServer.EntityFramework.DbContexts; | |||||
using Microsoft.EntityFrameworkCore; | |||||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||||
using Microsoft.EntityFrameworkCore.Metadata; | |||||
using Microsoft.EntityFrameworkCore.Migrations; | |||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | |||||
#nullable disable | |||||
namespace Identity.API.Migrations.PersistedGrantDb | |||||
{ | |||||
[DbContext(typeof(PersistedGrantDbContext))] | |||||
[Migration("20220324152905_Grants")] | |||||
partial class Grants | |||||
{ | |||||
protected override void BuildTargetModel(ModelBuilder modelBuilder) | |||||
{ | |||||
#pragma warning disable 612, 618 | |||||
modelBuilder | |||||
.HasAnnotation("ProductVersion", "6.0.0") | |||||
.HasAnnotation("Relational:MaxIdentifierLength", 128); | |||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); | |||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b => | |||||
{ | |||||
b.Property<string>("UserCode") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<string>("ClientId") | |||||
.IsRequired() | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<DateTime>("CreationTime") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Data") | |||||
.IsRequired() | |||||
.HasMaxLength(50000) | |||||
.HasColumnType("nvarchar(max)"); | |||||
b.Property<string>("Description") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<string>("DeviceCode") | |||||
.IsRequired() | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<DateTime?>("Expiration") | |||||
.IsRequired() | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("SessionId") | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<string>("SubjectId") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.HasKey("UserCode"); | |||||
b.HasIndex("DeviceCode") | |||||
.IsUnique(); | |||||
b.HasIndex("Expiration"); | |||||
b.ToTable("DeviceCodes", (string)null); | |||||
}); | |||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b => | |||||
{ | |||||
b.Property<string>("Id") | |||||
.HasColumnType("nvarchar(450)"); | |||||
b.Property<string>("Algorithm") | |||||
.IsRequired() | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<DateTime>("Created") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Data") | |||||
.IsRequired() | |||||
.HasColumnType("nvarchar(max)"); | |||||
b.Property<bool>("DataProtected") | |||||
.HasColumnType("bit"); | |||||
b.Property<bool>("IsX509Certificate") | |||||
.HasColumnType("bit"); | |||||
b.Property<string>("Use") | |||||
.HasColumnType("nvarchar(450)"); | |||||
b.Property<int>("Version") | |||||
.HasColumnType("int"); | |||||
b.HasKey("Id"); | |||||
b.HasIndex("Use"); | |||||
b.ToTable("Keys", (string)null); | |||||
}); | |||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b => | |||||
{ | |||||
b.Property<long>("Id") | |||||
.ValueGeneratedOnAdd() | |||||
.HasColumnType("bigint"); | |||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||||
b.Property<string>("ClientId") | |||||
.IsRequired() | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<DateTime?>("ConsumedTime") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<DateTime>("CreationTime") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Data") | |||||
.IsRequired() | |||||
.HasMaxLength(50000) | |||||
.HasColumnType("nvarchar(max)"); | |||||
b.Property<string>("Description") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<DateTime?>("Expiration") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Key") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<string>("SessionId") | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<string>("SubjectId") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<string>("Type") | |||||
.IsRequired() | |||||
.HasMaxLength(50) | |||||
.HasColumnType("nvarchar(50)"); | |||||
b.HasKey("Id"); | |||||
b.HasIndex("ConsumedTime"); | |||||
b.HasIndex("Expiration"); | |||||
b.HasIndex("Key") | |||||
.IsUnique() | |||||
.HasFilter("[Key] IS NOT NULL"); | |||||
b.HasIndex("SubjectId", "ClientId", "Type"); | |||||
b.HasIndex("SubjectId", "SessionId", "Type"); | |||||
b.ToTable("PersistedGrants", (string)null); | |||||
}); | |||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.ServerSideSession", b => | |||||
{ | |||||
b.Property<int>("Id") | |||||
.ValueGeneratedOnAdd() | |||||
.HasColumnType("int"); | |||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||||
b.Property<DateTime>("Created") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Data") | |||||
.IsRequired() | |||||
.HasColumnType("nvarchar(max)"); | |||||
b.Property<string>("DisplayName") | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<DateTime?>("Expires") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Key") | |||||
.IsRequired() | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<DateTime>("Renewed") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Scheme") | |||||
.IsRequired() | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<string>("SessionId") | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<string>("SubjectId") | |||||
.IsRequired() | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.HasKey("Id"); | |||||
b.HasIndex("DisplayName"); | |||||
b.HasIndex("Expires"); | |||||
b.HasIndex("Key") | |||||
.IsUnique(); | |||||
b.HasIndex("SessionId"); | |||||
b.HasIndex("SubjectId"); | |||||
b.ToTable("ServerSideSessions", (string)null); | |||||
}); | |||||
#pragma warning restore 612, 618 | |||||
} | |||||
} | |||||
} |
@ -1,177 +0,0 @@ | |||||
using System; | |||||
using Microsoft.EntityFrameworkCore.Migrations; | |||||
#nullable disable | |||||
namespace Identity.API.Migrations.PersistedGrantDb | |||||
{ | |||||
public partial class Grants : Migration | |||||
{ | |||||
protected override void Up(MigrationBuilder migrationBuilder) | |||||
{ | |||||
migrationBuilder.CreateTable( | |||||
name: "DeviceCodes", | |||||
columns: table => new | |||||
{ | |||||
UserCode = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
DeviceCode = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
SubjectId = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
SessionId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true), | |||||
ClientId = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
Description = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Expiration = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Data = table.Column<string>(type: "nvarchar(max)", maxLength: 50000, nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_DeviceCodes", x => x.UserCode); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "Keys", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false), | |||||
Version = table.Column<int>(type: "int", nullable: false), | |||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Use = table.Column<string>(type: "nvarchar(450)", nullable: true), | |||||
Algorithm = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false), | |||||
IsX509Certificate = table.Column<bool>(type: "bit", nullable: false), | |||||
DataProtected = table.Column<bool>(type: "bit", nullable: false), | |||||
Data = table.Column<string>(type: "nvarchar(max)", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_Keys", x => x.Id); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "PersistedGrants", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<long>(type: "bigint", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Key = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
Type = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false), | |||||
SubjectId = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
SessionId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true), | |||||
ClientId = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false), | |||||
Description = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true), | |||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Expiration = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
ConsumedTime = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
Data = table.Column<string>(type: "nvarchar(max)", maxLength: 50000, nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_PersistedGrants", x => x.Id); | |||||
}); | |||||
migrationBuilder.CreateTable( | |||||
name: "ServerSideSessions", | |||||
columns: table => new | |||||
{ | |||||
Id = table.Column<int>(type: "int", nullable: false) | |||||
.Annotation("SqlServer:Identity", "1, 1"), | |||||
Key = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false), | |||||
Scheme = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false), | |||||
SubjectId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false), | |||||
SessionId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true), | |||||
DisplayName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true), | |||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Renewed = table.Column<DateTime>(type: "datetime2", nullable: false), | |||||
Expires = table.Column<DateTime>(type: "datetime2", nullable: true), | |||||
Data = table.Column<string>(type: "nvarchar(max)", nullable: false) | |||||
}, | |||||
constraints: table => | |||||
{ | |||||
table.PrimaryKey("PK_ServerSideSessions", x => x.Id); | |||||
}); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_DeviceCodes_DeviceCode", | |||||
table: "DeviceCodes", | |||||
column: "DeviceCode", | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_DeviceCodes_Expiration", | |||||
table: "DeviceCodes", | |||||
column: "Expiration"); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_Keys_Use", | |||||
table: "Keys", | |||||
column: "Use"); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_PersistedGrants_ConsumedTime", | |||||
table: "PersistedGrants", | |||||
column: "ConsumedTime"); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_PersistedGrants_Expiration", | |||||
table: "PersistedGrants", | |||||
column: "Expiration"); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_PersistedGrants_Key", | |||||
table: "PersistedGrants", | |||||
column: "Key", | |||||
unique: true, | |||||
filter: "[Key] IS NOT NULL"); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_PersistedGrants_SubjectId_ClientId_Type", | |||||
table: "PersistedGrants", | |||||
columns: new[] { "SubjectId", "ClientId", "Type" }); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_PersistedGrants_SubjectId_SessionId_Type", | |||||
table: "PersistedGrants", | |||||
columns: new[] { "SubjectId", "SessionId", "Type" }); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ServerSideSessions_DisplayName", | |||||
table: "ServerSideSessions", | |||||
column: "DisplayName"); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ServerSideSessions_Expires", | |||||
table: "ServerSideSessions", | |||||
column: "Expires"); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ServerSideSessions_Key", | |||||
table: "ServerSideSessions", | |||||
column: "Key", | |||||
unique: true); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ServerSideSessions_SessionId", | |||||
table: "ServerSideSessions", | |||||
column: "SessionId"); | |||||
migrationBuilder.CreateIndex( | |||||
name: "IX_ServerSideSessions_SubjectId", | |||||
table: "ServerSideSessions", | |||||
column: "SubjectId"); | |||||
} | |||||
protected override void Down(MigrationBuilder migrationBuilder) | |||||
{ | |||||
migrationBuilder.DropTable( | |||||
name: "DeviceCodes"); | |||||
migrationBuilder.DropTable( | |||||
name: "Keys"); | |||||
migrationBuilder.DropTable( | |||||
name: "PersistedGrants"); | |||||
migrationBuilder.DropTable( | |||||
name: "ServerSideSessions"); | |||||
} | |||||
} | |||||
} |
@ -1,238 +0,0 @@ | |||||
// <auto-generated /> | |||||
using System; | |||||
using Duende.IdentityServer.EntityFramework.DbContexts; | |||||
using Microsoft.EntityFrameworkCore; | |||||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||||
using Microsoft.EntityFrameworkCore.Metadata; | |||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | |||||
#nullable disable | |||||
namespace Identity.API.Migrations.PersistedGrantDb | |||||
{ | |||||
[DbContext(typeof(PersistedGrantDbContext))] | |||||
partial class PersistedGrantDbContextModelSnapshot : ModelSnapshot | |||||
{ | |||||
protected override void BuildModel(ModelBuilder modelBuilder) | |||||
{ | |||||
#pragma warning disable 612, 618 | |||||
modelBuilder | |||||
.HasAnnotation("ProductVersion", "6.2.0") | |||||
.HasAnnotation("Relational:MaxIdentifierLength", 128); | |||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); | |||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b => | |||||
{ | |||||
b.Property<string>("UserCode") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<string>("ClientId") | |||||
.IsRequired() | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<DateTime>("CreationTime") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Data") | |||||
.IsRequired() | |||||
.HasMaxLength(50000) | |||||
.HasColumnType("nvarchar(max)"); | |||||
b.Property<string>("Description") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<string>("DeviceCode") | |||||
.IsRequired() | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<DateTime?>("Expiration") | |||||
.IsRequired() | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("SessionId") | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<string>("SubjectId") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.HasKey("UserCode"); | |||||
b.HasIndex("DeviceCode") | |||||
.IsUnique(); | |||||
b.HasIndex("Expiration"); | |||||
b.ToTable("DeviceCodes", (string)null); | |||||
}); | |||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b => | |||||
{ | |||||
b.Property<string>("Id") | |||||
.HasColumnType("nvarchar(450)"); | |||||
b.Property<string>("Algorithm") | |||||
.IsRequired() | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<DateTime>("Created") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Data") | |||||
.IsRequired() | |||||
.HasColumnType("nvarchar(max)"); | |||||
b.Property<bool>("DataProtected") | |||||
.HasColumnType("bit"); | |||||
b.Property<bool>("IsX509Certificate") | |||||
.HasColumnType("bit"); | |||||
b.Property<string>("Use") | |||||
.HasColumnType("nvarchar(450)"); | |||||
b.Property<int>("Version") | |||||
.HasColumnType("int"); | |||||
b.HasKey("Id"); | |||||
b.HasIndex("Use"); | |||||
b.ToTable("Keys", (string)null); | |||||
}); | |||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b => | |||||
{ | |||||
b.Property<long>("Id") | |||||
.ValueGeneratedOnAdd() | |||||
.HasColumnType("bigint"); | |||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||||
b.Property<string>("ClientId") | |||||
.IsRequired() | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<DateTime?>("ConsumedTime") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<DateTime>("CreationTime") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Data") | |||||
.IsRequired() | |||||
.HasMaxLength(50000) | |||||
.HasColumnType("nvarchar(max)"); | |||||
b.Property<string>("Description") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<DateTime?>("Expiration") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Key") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<string>("SessionId") | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<string>("SubjectId") | |||||
.HasMaxLength(200) | |||||
.HasColumnType("nvarchar(200)"); | |||||
b.Property<string>("Type") | |||||
.IsRequired() | |||||
.HasMaxLength(50) | |||||
.HasColumnType("nvarchar(50)"); | |||||
b.HasKey("Id"); | |||||
b.HasIndex("ConsumedTime"); | |||||
b.HasIndex("Expiration"); | |||||
b.HasIndex("Key") | |||||
.IsUnique() | |||||
.HasFilter("[Key] IS NOT NULL"); | |||||
b.HasIndex("SubjectId", "ClientId", "Type"); | |||||
b.HasIndex("SubjectId", "SessionId", "Type"); | |||||
b.ToTable("PersistedGrants", (string)null); | |||||
}); | |||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.ServerSideSession", b => | |||||
{ | |||||
b.Property<int>("Id") | |||||
.ValueGeneratedOnAdd() | |||||
.HasColumnType("int"); | |||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||||
b.Property<DateTime>("Created") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Data") | |||||
.IsRequired() | |||||
.HasColumnType("nvarchar(max)"); | |||||
b.Property<string>("DisplayName") | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<DateTime?>("Expires") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Key") | |||||
.IsRequired() | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<DateTime>("Renewed") | |||||
.HasColumnType("datetime2"); | |||||
b.Property<string>("Scheme") | |||||
.IsRequired() | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<string>("SessionId") | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.Property<string>("SubjectId") | |||||
.IsRequired() | |||||
.HasMaxLength(100) | |||||
.HasColumnType("nvarchar(100)"); | |||||
b.HasKey("Id"); | |||||
b.HasIndex("DisplayName"); | |||||
b.HasIndex("Expires"); | |||||
b.HasIndex("Key") | |||||
.IsUnique(); | |||||
b.HasIndex("SessionId"); | |||||
b.HasIndex("SubjectId"); | |||||
b.ToTable("ServerSideSessions", (string)null); | |||||
}); | |||||
#pragma warning restore 612, 618 | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,117 @@ | |||||
using Serilog; | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API; | |||||
public static class ProgramExtensions | |||||
{ | |||||
private const string AppName = "Identity API"; | |||||
public static void AddCustomConfiguration(this WebApplicationBuilder builder) | |||||
{ | |||||
builder.Configuration.AddConfiguration(GetConfiguration()).Build(); | |||||
} | |||||
public static void AddCustomSerilog(this WebApplicationBuilder builder) | |||||
{ | |||||
var seqServerUrl = builder.Configuration["SeqServerUrl"]; | |||||
var logstashUrl = builder.Configuration["LogstashgUrl"]; | |||||
Log.Logger = new LoggerConfiguration() | |||||
.MinimumLevel.Verbose() | |||||
.Enrich.WithProperty("ApplicationContext", AppName) | |||||
.Enrich.FromLogContext() | |||||
.WriteTo.Console() | |||||
.WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) | |||||
.WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://localhost:8080" : logstashUrl, null) | |||||
.ReadFrom.Configuration(builder.Configuration) | |||||
.CreateLogger(); | |||||
builder.Host.UseSerilog(); | |||||
} | |||||
public static void AddCustomMvc(this WebApplicationBuilder builder) | |||||
{ | |||||
builder.Services.AddControllersWithViews(); | |||||
builder.Services.AddControllers(); | |||||
builder.Services.AddRazorPages(); | |||||
} | |||||
public static void AddCustomDatabase(this WebApplicationBuilder builder) => | |||||
builder.Services.AddDbContext<ApplicationDbContext>( | |||||
options => options.UseSqlServer(builder.Configuration["ConnectionString"])); | |||||
public static void AddCustomIdentity(this WebApplicationBuilder builder) | |||||
{ | |||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole>() | |||||
.AddEntityFrameworkStores<ApplicationDbContext>() | |||||
.AddDefaultTokenProviders(); | |||||
} | |||||
public static void AddCustomIdentityServer(this WebApplicationBuilder builder) | |||||
{ | |||||
var identityServerBuilder = builder.Services.AddIdentityServer(options => | |||||
{ | |||||
options.IssuerUri = builder.Configuration["IssuerUrl"]; | |||||
options.Authentication.CookieLifetime = TimeSpan.FromHours(2); | |||||
options.Events.RaiseErrorEvents = true; | |||||
options.Events.RaiseInformationEvents = true; | |||||
options.Events.RaiseFailureEvents = true; | |||||
options.Events.RaiseSuccessEvents = true; | |||||
}) | |||||
.AddInMemoryIdentityResources(Config.GetResources()) | |||||
.AddInMemoryApiScopes(Config.GetApiScopes()) | |||||
.AddInMemoryApiResources(Config.GetApis()) | |||||
.AddInMemoryClients(Config.GetClients(builder.Configuration)) | |||||
.AddAspNetIdentity<ApplicationUser>(); | |||||
// not recommended for production - you need to store your key material somewhere secure | |||||
identityServerBuilder.AddDeveloperSigningCredential(); | |||||
} | |||||
public static void AddCustomAuthentication(this WebApplicationBuilder builder) | |||||
{ | |||||
builder.Services.AddAuthentication(); | |||||
} | |||||
public static void AddCustomHealthChecks(this WebApplicationBuilder builder) | |||||
{ | |||||
builder.Services.AddHealthChecks() | |||||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||||
.AddSqlServer(builder.Configuration["ConnectionString"], | |||||
name: "IdentityDB-check", | |||||
tags: new string[] { "IdentityDB" }); | |||||
} | |||||
public static void AddCustomApplicationServices(this WebApplicationBuilder builder) | |||||
{ | |||||
builder.Services.AddTransient<IProfileService, ProfileService>(); | |||||
builder.Services.AddTransient<ILoginService<ApplicationUser>, EFLoginService>(); | |||||
builder.Services.AddTransient<IRedirectService, RedirectService>(); | |||||
} | |||||
static IConfiguration GetConfiguration() | |||||
{ | |||||
var builder = new ConfigurationBuilder() | |||||
.SetBasePath(Directory.GetCurrentDirectory()) | |||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) | |||||
.AddEnvironmentVariables(); | |||||
var config = builder.Build(); | |||||
if (config.GetValue<bool>("UseVault", false)) | |||||
{ | |||||
TokenCredential credential = new ClientSecretCredential( | |||||
config["Vault:TenantId"], | |||||
config["Vault:ClientId"], | |||||
config["Vault:ClientSecret"]); | |||||
builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential); | |||||
} | |||||
return builder.Build(); | |||||
} | |||||
} |
@ -0,0 +1,341 @@ | |||||
// 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 Duende.IdentityServer.Events; | |||||
using Duende.IdentityServer.Extensions; | |||||
using Duende.IdentityServer.Stores; | |||||
using Microsoft.AspNetCore.Authentication; | |||||
using Microsoft.AspNetCore.Authorization; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
namespace IdentityServerHost.Quickstart.UI | |||||
{ | |||||
[SecurityHeaders] | |||||
[AllowAnonymous] | |||||
public class AccountController : Controller | |||||
{ | |||||
private readonly UserManager<ApplicationUser> _userManager; | |||||
private readonly SignInManager<ApplicationUser> _signInManager; | |||||
private readonly IIdentityServerInteractionService _interaction; | |||||
private readonly IClientStore _clientStore; | |||||
private readonly IAuthenticationSchemeProvider _schemeProvider; | |||||
private readonly IAuthenticationHandlerProvider _handlerProvider; | |||||
private readonly IEventService _events; | |||||
public AccountController( | |||||
UserManager<ApplicationUser> userManager, | |||||
SignInManager<ApplicationUser> signInManager, | |||||
IIdentityServerInteractionService interaction, | |||||
IClientStore clientStore, | |||||
IAuthenticationSchemeProvider schemeProvider, | |||||
IAuthenticationHandlerProvider handlerProvider, | |||||
IEventService events) | |||||
{ | |||||
_userManager = userManager; | |||||
_signInManager = signInManager; | |||||
_interaction = interaction; | |||||
_clientStore = clientStore; | |||||
_schemeProvider = schemeProvider; | |||||
_handlerProvider = handlerProvider; | |||||
_events = events; | |||||
} | |||||
/// <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); | |||||
ViewData["ReturnUrl"] = 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); | |||||
} | |||||
/// <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.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) | |||||
{ | |||||
var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberLogin, lockoutOnFailure: true); | |||||
if (result.Succeeded) | |||||
{ | |||||
var user = await _userManager.FindByNameAsync(model.Username); | |||||
await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: 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", 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); | |||||
ViewData["ReturnUrl"] = model.ReturnUrl; | |||||
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 _signInManager.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 == 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<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 != IdentityServerConstants.LocalIdentityProvider) | |||||
{ | |||||
var handler = await _handlerProvider.GetHandlerAsync(HttpContext, idp); | |||||
if (handler is IAuthenticationSignOutHandler) | |||||
{ | |||||
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; | |||||
} | |||||
} | |||||
} |
@ -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 AccountOptions | |||||
{ | |||||
public static bool AllowLocalLogin = true; | |||||
public static bool AllowRememberLogin = true; | |||||
public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); | |||||
public static bool ShowLogoutPrompt = false; | |||||
public static bool AutomaticRedirectAfterSignOut = true; | |||||
public static string InvalidCredentialsErrorMessage = "Invalid username or password"; | |||||
} |
@ -0,0 +1,238 @@ | |||||
namespace IdentityServerHost.Quickstart.UI; | |||||
[SecurityHeaders] | |||||
[AllowAnonymous] | |||||
public class ExternalController : Controller | |||||
{ | |||||
private readonly UserManager<ApplicationUser> _userManager; | |||||
private readonly SignInManager<ApplicationUser> _signInManager; | |||||
private readonly IIdentityServerInteractionService _interaction; | |||||
private readonly IClientStore _clientStore; | |||||
private readonly IEventService _events; | |||||
private readonly ILogger<ExternalController> _logger; | |||||
public ExternalController( | |||||
UserManager<ApplicationUser> userManager, | |||||
SignInManager<ApplicationUser> signInManager, | |||||
IIdentityServerInteractionService interaction, | |||||
IClientStore clientStore, | |||||
IEventService events, | |||||
ILogger<ExternalController> logger) | |||||
{ | |||||
_userManager = userManager; | |||||
_signInManager = signInManager; | |||||
_interaction = interaction; | |||||
_clientStore = clientStore; | |||||
_events = events; | |||||
_logger = logger; | |||||
} | |||||
/// <summary> | |||||
/// initiate roundtrip to external authentication provider | |||||
/// </summary> | |||||
[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); | |||||
} | |||||
/// <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(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) = await FindUserFromExternalProviderAsync(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 = await AutoProvisionUserAsync(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<Claim>(); | |||||
var localSignInProps = new AuthenticationProperties(); | |||||
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps); | |||||
// issue authentication cookie for user | |||||
// we must issue the cookie maually, and can't use the SignInManager because | |||||
// it doesn't expose an API to issue additional claims from the login workflow | |||||
var principal = await _signInManager.CreateUserPrincipalAsync(user); | |||||
additionalLocalClaims.AddRange(principal.Claims); | |||||
var name = principal.FindFirst(JwtClaimTypes.Name)?.Value ?? user.Id; | |||||
var isuser = new IdentityServerUser(user.Id) | |||||
{ | |||||
DisplayName = name, | |||||
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.Id, name, 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 async Task<(ApplicationUser user, string provider, string providerUserId, IEnumerable<Claim> claims)> | |||||
FindUserFromExternalProviderAsync(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 = await _userManager.FindByLoginAsync(provider, providerUserId); | |||||
return (user, provider, providerUserId, claims); | |||||
} | |||||
private async Task<ApplicationUser> AutoProvisionUserAsync(string provider, string providerUserId, IEnumerable<Claim> claims) | |||||
{ | |||||
// create a list of claims that we want to transfer into our store | |||||
var filtered = new List<Claim>(); | |||||
// user's display name | |||||
var name = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Name)?.Value ?? | |||||
claims.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value; | |||||
if (name != null) | |||||
{ | |||||
filtered.Add(new Claim(JwtClaimTypes.Name, name)); | |||||
} | |||||
else | |||||
{ | |||||
var first = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.GivenName)?.Value ?? | |||||
claims.FirstOrDefault(x => x.Type == ClaimTypes.GivenName)?.Value; | |||||
var last = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.FamilyName)?.Value ?? | |||||
claims.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value; | |||||
if (first != null && last != null) | |||||
{ | |||||
filtered.Add(new Claim(JwtClaimTypes.Name, first + " " + last)); | |||||
} | |||||
else if (first != null) | |||||
{ | |||||
filtered.Add(new Claim(JwtClaimTypes.Name, first)); | |||||
} | |||||
else if (last != null) | |||||
{ | |||||
filtered.Add(new Claim(JwtClaimTypes.Name, last)); | |||||
} | |||||
} | |||||
var email = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Email)?.Value ?? | |||||
claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value; | |||||
if (email != null) | |||||
{ | |||||
filtered.Add(new Claim(JwtClaimTypes.Email, email)); | |||||
} | |||||
var user = new ApplicationUser | |||||
{ | |||||
UserName = Guid.NewGuid().ToString(), | |||||
}; | |||||
var identityResult = await _userManager.CreateAsync(user); | |||||
if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description); | |||||
if (filtered.Any()) | |||||
{ | |||||
identityResult = await _userManager.AddClaimsAsync(user, filtered); | |||||
if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description); | |||||
} | |||||
identityResult = await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerUserId, provider)); | |||||
if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description); | |||||
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<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 idToken = externalResult.Properties.GetTokenValue("id_token"); | |||||
if (idToken != null) | |||||
{ | |||||
localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } }); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,10 @@ | |||||
// 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; } | |||||
} |
@ -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. | |||||
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; } | |||||
} |
@ -0,0 +1,15 @@ | |||||
// 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 LoginInputModel | |||||
{ | |||||
[Required] | |||||
public string Username { get; set; } | |||||
[Required] | |||||
public string Password { get; set; } | |||||
public bool RememberLogin { get; set; } | |||||
public string ReturnUrl { get; set; } | |||||
} |
@ -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. | |||||
namespace IdentityServerHost.Quickstart.UI; | |||||
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; | |||||
} |
@ -0,0 +1,10 @@ | |||||
// 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; } | |||||
} |
@ -0,0 +1,10 @@ | |||||
// 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; | |||||
} |
@ -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 RedirectViewModel | |||||
{ | |||||
public string RedirectUrl { get; set; } | |||||
} |
@ -0,0 +1,247 @@ | |||||
// 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; | |||||
/// <summary> | |||||
/// This controller processes the consent UI | |||||
/// </summary> | |||||
[SecurityHeaders] | |||||
[Authorize] | |||||
public class ConsentController : Controller | |||||
{ | |||||
private readonly IIdentityServerInteractionService _interaction; | |||||
private readonly IEventService _events; | |||||
private readonly ILogger<ConsentController> _logger; | |||||
public ConsentController( | |||||
IIdentityServerInteractionService interaction, | |||||
IEventService events, | |||||
ILogger<ConsentController> logger) | |||||
{ | |||||
_interaction = interaction; | |||||
_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) | |||||
{ | |||||
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<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 = 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 != 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<ConsentViewModel> 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<string>(), | |||||
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<ScopeViewModel>(); | |||||
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(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 = IdentityServerConstants.StandardScopes.OfflineAccess, | |||||
DisplayName = ConsentOptions.OfflineAccessDisplayName, | |||||
Description = ConsentOptions.OfflineAccessDescription, | |||||
Emphasize = true, | |||||
Checked = check | |||||
}; | |||||
} | |||||
} |
@ -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. | |||||
namespace IdentityServerHost.Quickstart.UI; | |||||
public class ConsentInputModel | |||||
{ | |||||
public string Button { get; set; } | |||||
public IEnumerable<string> ScopesConsented { get; set; } | |||||
public bool RememberConsent { get; set; } | |||||
public string ReturnUrl { get; set; } | |||||
public string Description { get; set; } | |||||
} |
@ -0,0 +1,15 @@ | |||||
// 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"; | |||||
} |
@ -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 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> ApiScopes { get; set; } | |||||
} |
@ -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. | |||||
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; } | |||||
} |
@ -0,0 +1,15 @@ | |||||
// 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; } | |||||
} |
@ -0,0 +1,10 @@ | |||||
// 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; } | |||||
} |
@ -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 DeviceAuthorizationViewModel : ConsentViewModel | |||||
{ | |||||
public string UserCode { get; set; } | |||||
public bool ConfirmUserCode { get; set; } | |||||
} |
@ -0,0 +1,214 @@ | |||||
// 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; | |||||
[Authorize] | |||||
[SecurityHeaders] | |||||
public class DeviceController : Controller | |||||
{ | |||||
private readonly IDeviceFlowInteractionService _interaction; | |||||
private readonly IEventService _events; | |||||
private readonly IOptions<IdentityServerOptions> _options; | |||||
private readonly ILogger<DeviceController> _logger; | |||||
public DeviceController( | |||||
IDeviceFlowInteractionService interaction, | |||||
IEventService eventService, | |||||
IOptions<IdentityServerOptions> options, | |||||
ILogger<DeviceController> logger) | |||||
{ | |||||
_interaction = interaction; | |||||
_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 = 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 != 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<DeviceAuthorizationViewModel> 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<string>(), | |||||
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<ScopeViewModel>(); | |||||
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(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, | |||||
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 = IdentityServerConstants.StandardScopes.OfflineAccess, | |||||
DisplayName = ConsentOptions.OfflineAccessDisplayName, | |||||
Description = ConsentOptions.OfflineAccessDescription, | |||||
Emphasize = true, | |||||
Checked = check | |||||
}; | |||||
} | |||||
} |
@ -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. | |||||
namespace IdentityServerHost.Quickstart.UI; | |||||
[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,28 @@ | |||||
// 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.Text; | |||||
using System.Text.Json; | |||||
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 = JsonSerializer.Deserialize<string[]>(value); | |||||
} | |||||
} | |||||
public AuthenticateResult AuthenticateResult { get; } | |||||
public IEnumerable<string> Clients { get; } = new List<string>(); | |||||
} |
@ -0,0 +1,22 @@ | |||||
namespace IdentityServerHost.Quickstart.UI; | |||||
public static class Extensions | |||||
{ | |||||
/// <summary> | |||||
/// Checks if the redirect URI is for a native client. | |||||
/// </summary> | |||||
/// <returns></returns> | |||||
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 }); | |||||
} | |||||
} |
@ -0,0 +1,85 @@ | |||||
// 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; | |||||
/// <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.GetAllUserGrantsAsync(); | |||||
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, | |||||
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 | |||||
}; | |||||
} | |||||
} |
@ -0,0 +1,23 @@ | |||||
// 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 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 string Description { get; set; } | |||||
public DateTime Created { get; set; } | |||||
public DateTime? Expires { get; set; } | |||||
public IEnumerable<string> IdentityGrantNames { get; set; } | |||||
public IEnumerable<string> ApiGrantNames { get; set; } | |||||
} |
@ -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. | |||||
namespace IdentityServerHost.Quickstart.UI; | |||||
public class ErrorViewModel | |||||
{ | |||||
public ErrorViewModel() | |||||
{ | |||||
} | |||||
public ErrorViewModel(string error) | |||||
{ | |||||
Error = new ErrorMessage { Error = error }; | |||||
} | |||||
public ErrorMessage Error { get; set; } | |||||
} |
@ -0,0 +1,63 @@ | |||||
// 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.Authorization; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
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<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); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,52 @@ | |||||
// 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 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); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,124 @@ | |||||
using System.Threading.Tasks; | |||||
using System; | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API; | |||||
public class SeedData | |||||
{ | |||||
public static async Task EnsureSeedData(IServiceScope scope, IConfiguration configuration, Microsoft.Extensions.Logging.ILogger logger) | |||||
{ | |||||
var retryPolicy = CreateRetryPolicy(configuration, logger); | |||||
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); | |||||
await retryPolicy.ExecuteAsync(async () => | |||||
{ | |||||
await context.Database.MigrateAsync(); | |||||
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); | |||||
var alice = await userMgr.FindByNameAsync("alice"); | |||||
if (alice == null) | |||||
{ | |||||
alice = new ApplicationUser | |||||
{ | |||||
UserName = "alice", | |||||
Email = "AliceSmith@email.com", | |||||
EmailConfirmed = true, | |||||
CardHolderName = "Alice Smith", | |||||
CardNumber = "4012888888881881", | |||||
CardType = 1, | |||||
City = "Redmond", | |||||
Country = "U.S.", | |||||
Expiration = "12/20", | |||||
Id = Guid.NewGuid().ToString(), | |||||
LastName = "Smith", | |||||
Name = "Alice", | |||||
PhoneNumber = "1234567890", | |||||
ZipCode = "98052", | |||||
State = "WA", | |||||
Street = "15703 NE 61st Ct", | |||||
SecurityNumber = "123" | |||||
}; | |||||
var result = userMgr.CreateAsync(alice, "Pass123$").Result; | |||||
if (!result.Succeeded) | |||||
{ | |||||
throw new Exception(result.Errors.First().Description); | |||||
} | |||||
logger.LogDebug("alice created"); | |||||
} | |||||
else | |||||
{ | |||||
logger.LogDebug("alice already exists"); | |||||
} | |||||
var bob = await userMgr.FindByNameAsync("bob"); | |||||
if (bob == null) | |||||
{ | |||||
bob = new ApplicationUser | |||||
{ | |||||
UserName = "bob", | |||||
Email = "BobSmith@email.com", | |||||
EmailConfirmed = true, | |||||
CardHolderName = "Bob Smith", | |||||
CardNumber = "4012888888881881", | |||||
CardType = 1, | |||||
City = "Redmond", | |||||
Country = "U.S.", | |||||
Expiration = "12/20", | |||||
Id = Guid.NewGuid().ToString(), | |||||
LastName = "Smith", | |||||
Name = "Bob", | |||||
PhoneNumber = "1234567890", | |||||
ZipCode = "98052", | |||||
State = "WA", | |||||
Street = "15703 NE 61st Ct", | |||||
SecurityNumber = "456" | |||||
}; | |||||
var result = await userMgr.CreateAsync(bob, "Pass123$"); | |||||
if (!result.Succeeded) | |||||
{ | |||||
throw new Exception(result.Errors.First().Description); | |||||
} | |||||
logger.LogDebug("bob created"); | |||||
} | |||||
else | |||||
{ | |||||
logger.LogDebug("bob already exists"); | |||||
} | |||||
}); | |||||
} | |||||
private static AsyncPolicy CreateRetryPolicy(IConfiguration configuration, Microsoft.Extensions.Logging.ILogger logger) | |||||
{ | |||||
var retryMigrations = false; | |||||
bool.TryParse(configuration["RetryMigrations"], out retryMigrations); | |||||
// Only use a retry policy if configured to do so. | |||||
// When running in an orchestrator/K8s, it will take care of restarting failed services. | |||||
if (retryMigrations) | |||||
{ | |||||
return Policy.Handle<Exception>(). | |||||
WaitAndRetryForeverAsync( | |||||
sleepDurationProvider: retry => TimeSpan.FromSeconds(5), | |||||
onRetry: (exception, retry, timeSpan) => | |||||
{ | |||||
logger.LogWarning( | |||||
exception, | |||||
"Exception {ExceptionType} with message {Message} detected during database migration (retry attempt {retry})", | |||||
exception.GetType().Name, | |||||
exception.Message, | |||||
retry); | |||||
} | |||||
); | |||||
} | |||||
return Policy.NoOpAsync(); | |||||
} | |||||
} |
@ -1,2 +0,0 @@ | |||||
CardHolderName,CardNumber,CardType,City,Country,Email,Expiration,LastName,Name,PhoneNumber,UserName,ZipCode,State,Street,SecurityNumber,NormalizedEmail,NormalizedUserName,Password | |||||
DemoUser,4012888888881881,1,Redmond,U.S.,demouser@microsoft.com,12/25,DemoLastName,DemoUser,1234567890,demouser@microsoft.com,98052,WA,15703 NE 61st Ct,535,DEMOUSER@MICROSOFT.COM,DEMOUSER@MICROSOFT.COM,Pass@word1 |
@ -1,167 +0,0 @@ | |||||
using Microsoft.AspNetCore.DataProtection; | |||||
namespace Microsoft.eShopOnContainers.Services.Identity.API | |||||
{ | |||||
public class Startup | |||||
{ | |||||
public Startup(IConfiguration configuration) | |||||
{ | |||||
Configuration = configuration; | |||||
} | |||||
public IConfiguration Configuration { get; } | |||||
// This method gets called by the runtime. Use this method to add services to the container. | |||||
public IServiceProvider ConfigureServices(IServiceCollection services) | |||||
{ | |||||
RegisterAppInsights(services); | |||||
// Add framework services. | |||||
services.AddDbContext<ApplicationDbContext>(options => | |||||
options.UseSqlServer(Configuration["ConnectionString"], | |||||
sqlServerOptionsAction: sqlOptions => | |||||
{ | |||||
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); | |||||
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency | |||||
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); | |||||
})); | |||||
services.AddIdentity<ApplicationUser, IdentityRole>() | |||||
.AddEntityFrameworkStores<ApplicationDbContext>() | |||||
.AddDefaultTokenProviders(); | |||||
services.Configure<AppSettings>(Configuration); | |||||
if (Configuration.GetValue<string>("IsClusterEnv") == bool.TrueString) | |||||
{ | |||||
services.AddDataProtection(opts => | |||||
{ | |||||
opts.ApplicationDiscriminator = "eshop.identity"; | |||||
}) | |||||
.PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(Configuration["DPConnectionString"]), "DataProtection-Keys"); | |||||
} | |||||
services.AddHealthChecks() | |||||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||||
.AddSqlServer(Configuration["ConnectionString"], | |||||
name: "IdentityDB-check", | |||||
tags: new string[] { "IdentityDB" }); | |||||
services.AddTransient<ILoginService<ApplicationUser>, EFLoginService>(); | |||||
services.AddTransient<IRedirectService, RedirectService>(); | |||||
var connectionString = Configuration["ConnectionString"]; | |||||
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; | |||||
// Adds IdentityServer | |||||
services.AddIdentityServer(x => | |||||
{ | |||||
x.IssuerUri = "null"; | |||||
x.Authentication.CookieLifetime = TimeSpan.FromHours(2); | |||||
}) | |||||
.AddServerSideSessions() | |||||
.AddDevspacesIfNeeded(Configuration.GetValue("EnableDevspaces", false)) | |||||
.AddSigningCredential(Certificate.Get()) | |||||
.AddAspNetIdentity<ApplicationUser>() | |||||
.AddConfigurationStore(options => | |||||
{ | |||||
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, | |||||
sqlServerOptionsAction: sqlOptions => | |||||
{ | |||||
sqlOptions.MigrationsAssembly(migrationsAssembly); | |||||
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency | |||||
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); | |||||
}); | |||||
}) | |||||
.AddOperationalStore(options => | |||||
{ | |||||
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, | |||||
sqlServerOptionsAction: sqlOptions => | |||||
{ | |||||
sqlOptions.MigrationsAssembly(migrationsAssembly); | |||||
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency | |||||
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); | |||||
}); | |||||
}) | |||||
.Services.AddTransient<IProfileService, ProfileService>(); | |||||
//services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0); | |||||
services.AddControllers(); | |||||
services.AddControllersWithViews(); | |||||
services.AddRazorPages(); | |||||
var container = new ContainerBuilder(); | |||||
container.Populate(services); | |||||
return new AutofacServiceProvider(container.Build()); | |||||
} | |||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | |||||
{ | |||||
//loggerFactory.AddConsole(Configuration.GetSection("Logging")); | |||||
//loggerFactory.AddDebug(); | |||||
//loggerFactory.AddAzureWebAppDiagnostics(); | |||||
//loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); | |||||
if (env.IsDevelopment()) | |||||
{ | |||||
app.UseDeveloperExceptionPage(); | |||||
} | |||||
else | |||||
{ | |||||
app.UseExceptionHandler("/Home/Error"); | |||||
} | |||||
var pathBase = Configuration["PATH_BASE"]; | |||||
if (!string.IsNullOrEmpty(pathBase)) | |||||
{ | |||||
//loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | |||||
app.UsePathBase(pathBase); | |||||
} | |||||
app.UseStaticFiles(); | |||||
// Make work identity server redirections in Edge and lastest versions of browsers. WARN: Not valid in a production environment. | |||||
app.Use(async (context, next) => | |||||
{ | |||||
context.Response.Headers.Add("Content-Security-Policy", "script-src 'unsafe-inline'"); | |||||
context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); | |||||
context.Response.Headers.Add("Access-Control-Allow-Headers", "*"); | |||||
context.Response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS"); | |||||
await next(); | |||||
}); | |||||
app.UseForwardedHeaders(); | |||||
// Adds IdentityServer | |||||
app.UseIdentityServer(); | |||||
// Fix a problem with chrome. Chrome enabled a new feature "Cookies without SameSite must be secure", | |||||
// the cookies should be expired from https, but in eShop, the internal communication in aks and docker compose is http. | |||||
// To avoid this problem, the policy of cookies should be in Lax mode. | |||||
app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = AspNetCore.Http.SameSiteMode.Lax }); | |||||
app.UseRouting(); | |||||
app.UseEndpoints(endpoints => | |||||
{ | |||||
endpoints.MapDefaultControllerRoute(); | |||||
endpoints.MapControllers(); | |||||
endpoints.MapHealthChecks("/hc", new HealthCheckOptions() | |||||
{ | |||||
Predicate = _ => true, | |||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | |||||
}); | |||||
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | |||||
{ | |||||
Predicate = r => r.Name.Contains("self") | |||||
}); | |||||
}); | |||||
} | |||||
private void RegisterAppInsights(IServiceCollection services) | |||||
{ | |||||
services.AddApplicationInsightsTelemetry(Configuration); | |||||
services.AddApplicationInsightsKubernetesEnricher(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,7 @@ | |||||
<div class="container"> | |||||
<div class="lead"> | |||||
<h1>Access Denied</h1> | |||||
<p>You do not have access to that resource.</p> | |||||
</div> | |||||
</div> |
@ -1,28 +1,58 @@ | |||||
@model Microsoft.eShopOnContainers.Services.Identity.API.Models.AccountViewModels.LoginViewModel | |||||
@{ | |||||
ViewData["Title"] = "Log in"; | |||||
var requestQuery = ViewContext.HttpContext.Request.Query; | |||||
requestQuery.TryGetValue("ReturnUrl", out var returnUrl); | |||||
string partialView; | |||||
if (returnUrl[0].Contains("client_id=js")) | |||||
{ | |||||
Layout = "_Layout-SPA"; | |||||
partialView = "_LoginPartial-SPA.cshtml"; | |||||
} | |||||
else | |||||
{ | |||||
partialView = "_LoginPartial-MVC.cshtml"; | |||||
} | |||||
} | |||||
<partial name=@partialView model=@Model /> | |||||
@section Scripts { | |||||
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } | |||||
} | |||||
@model LoginViewModel | |||||
<div class="login-page"> | |||||
<partial name="_ValidationSummary" /> | |||||
<div class="container"> | |||||
<div class="row top-buffer"> | |||||
@if (Model.EnableLocalLogin) | |||||
{ | |||||
<div class="col"></div> | |||||
<div class="col-sm-6"> | |||||
<div class="card"> | |||||
<div class="card-header"> | |||||
<h2>Login</h2> | |||||
</div> | |||||
<div class="card-body"> | |||||
<form asp-route="Login"> | |||||
<input type="hidden" asp-for="ReturnUrl" /> | |||||
<div class="form-group"> | |||||
<label asp-for="Username"></label> | |||||
<input class="form-control" placeholder="Username" asp-for="Username" autofocus> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label asp-for="Password"></label> | |||||
<input type="password" class="form-control" placeholder="Password" asp-for="Password" autocomplete="off"> | |||||
</div> | |||||
@if (Model.AllowRememberLogin) | |||||
{ | |||||
<div class="form-group"> | |||||
<div class="form-check"> | |||||
<input class="form-check-input" asp-for="RememberLogin"> | |||||
<label class="form-check-label" asp-for="RememberLogin"> | |||||
Remember My Login | |||||
</label> | |||||
</div> | |||||
</div> | |||||
} | |||||
<div> | |||||
<p>The default users are alice/bob, password: Pass123$</p> | |||||
</div> | |||||
<button class="btn btn-primary" name="button" value="login">Login</button> | |||||
<button class="btn btn-secondary" name="button" value="cancel">Cancel</button> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="col"></div> | |||||
} | |||||
</div> | |||||
</div> | |||||
</div> |
@ -1,21 +1,15 @@ | |||||
@model Microsoft.eShopOnContainers.Services.Identity.API.Models.AccountViewModels.LogoutViewModel | |||||
@model LogoutViewModel | |||||
<div class="container logout-page"> | |||||
<div class="page-header"> | |||||
<div class="logout-page"> | |||||
<div class="lead"> | |||||
<h1>Logout</h1> | <h1>Logout</h1> | ||||
<p>Would you like to logut of IdentityServer?</p> | |||||
</div> | </div> | ||||
<div class="row"> | |||||
<div class="col-sm-6"> | |||||
<p>Would you like to logout of IdentityServer?</p> | |||||
<form asp-action="Logout"> | |||||
<input type="hidden" name="logoutId" value="@Model.LogoutId" /> | |||||
<fieldset> | |||||
<div class="form-group"> | |||||
<button class="btn btn-primary">Yes</button> | |||||
</div> | |||||
</fieldset> | |||||
</form> | |||||
<form asp-action="Logout"> | |||||
<input type="hidden" name="logoutId" value="@Model.LogoutId" /> | |||||
<div class="form-group"> | |||||
<button class="btn btn-primary">Yes</button> | |||||
</div> | </div> | ||||
</div> | |||||
</div> | |||||
</form> | |||||
</div> |
@ -1,3 +0,0 @@ | |||||
<br /> | |||||
<br /> | |||||
<p style="width:80%;"><center><span>Redirecting...</span></center></p> |
@ -1,28 +0,0 @@ | |||||
@model Microsoft.eShopOnContainers.Services.Identity.API.Models.AccountViewModels.RegisterViewModel | |||||
@{ | |||||
ViewData["Title"] = "Register"; | |||||
var requestQuery = ViewContext.HttpContext.Request.Query; | |||||
requestQuery.TryGetValue("ReturnUrl", out var returnUrl); | |||||
string partialView; | |||||
if (returnUrl[0].Contains("client_id=js")) | |||||
{ | |||||
Layout = "_Layout-SPA"; | |||||
partialView = "_RegisterPartial-SPA.cshtml"; | |||||
} | |||||
else | |||||
{ | |||||
partialView = "_RegisterPartial-MVC.cshtml"; | |||||
} | |||||
} | |||||
<partial name=@partialView model=@Model /> | |||||
@section Scripts { | |||||
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } | |||||
} |
@ -1,54 +0,0 @@ | |||||
@model Microsoft.eShopOnContainers.Services.Identity.API.Models.AccountViewModels.LoginViewModel | |||||
<div class="brand-header-block"> | |||||
<ul class="container"> | |||||
<li><a asp-area="" asp-controller="Account" asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]">REGISTER</a></li> | |||||
<li class="active" style="margin-right: 65px;">LOGIN</li> | |||||
</ul> | |||||
</div> | |||||
<div class="container account-login-container"> | |||||
<div class="row"> | |||||
<div class="col-md-12"> | |||||
<section> | |||||
<form asp-controller="Account" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal"> | |||||
<input type="hidden" asp-for="ReturnUrl" /> | |||||
<h4>ARE YOU REGISTERED?</h4> | |||||
<div asp-validation-summary="All" class="text-danger"></div> | |||||
<div class="form-group"> | |||||
<label asp-for="Email" class="control-label form-label"></label> | |||||
<input asp-for="Email" class="form-control form-input form-input-center" /> | |||||
<span asp-validation-for="Email" class="text-danger"></span> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label asp-for="Password" class="control-label form-label"></label> | |||||
<input asp-for="Password" class="form-control form-input form-input-center" /> | |||||
<span asp-validation-for="Password" class="text-danger"></span> | |||||
</div> | |||||
<div class="form-group"> | |||||
<div class="checkbox"> | |||||
<label asp-for="RememberMe"> | |||||
<input asp-for="RememberMe" /> | |||||
@Html.DisplayNameFor(m => m.RememberMe) | |||||
</label> | |||||
</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<button type="submit" class="btn btn-default btn-brand btn-brand-big"> LOG IN </button> | |||||
</div> | |||||
<p> | |||||
<a asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" class="text">Register as a new user?</a> | |||||
</p> | |||||
<p> | |||||
Note that for demo purposes you don't need to register and can login with these credentials: | |||||
</p> | |||||
<p> | |||||
User: <b>demouser@microsoft.com</b> | |||||
</p> | |||||
<p> | |||||
Password: <b>Pass@word1</b> | |||||
</p> | |||||
</form> | |||||
</section> | |||||
</div> | |||||
</div> | |||||
</div> |
@ -1,47 +0,0 @@ | |||||
@model Microsoft.eShopOnContainers.Services.Identity.API.Models.AccountViewModels.LoginViewModel | |||||
<div class="container"> | |||||
<div class="row"> | |||||
<div class="col-md-12"> | |||||
<section> | |||||
<form asp-controller="Account" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-login"> | |||||
<input type="hidden" asp-for="ReturnUrl" /> | |||||
<h4 class="text-left mb-4">ARE YOU REGISTERED?</h4> | |||||
<div asp-validation-summary="All" class="text-danger"></div> | |||||
<div class="form-group"> | |||||
<label asp-for="Email" class="control-label form-label"></label> | |||||
<input asp-for="Email" class="form-control form-input w-100" /> | |||||
<span asp-validation-for="Email" class="text-danger"></span> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label asp-for="Password" class="control-label form-label"></label> | |||||
<input asp-for="Password" class="form-control form-input w-100" /> | |||||
<span asp-validation-for="Password" class="text-danger"></span> | |||||
</div> | |||||
<div class="d-flex align-items-center justify-content-between"> | |||||
<div class="checkbox"> | |||||
<label asp-for="RememberMe"> | |||||
<input asp-for="RememberMe" class="mr-1" /> | |||||
@Html.DisplayNameFor(m => m.RememberMe) | |||||
</label> | |||||
</div> | |||||
<button type="submit" class="btn btn-primary">LOG IN</button> | |||||
</div> | |||||
<div class="form-login-register-link text-center mt-3"> | |||||
<a class="text-link" asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]">Register as a new user?</a> | |||||
</div> | |||||
</form> | |||||
<div class="form-login-details"> | |||||
<div> | |||||
Note that for demo purposes you don't need to register and can login with these credentials: | |||||
</div> | |||||
<div> | |||||
User: <strong>demouser@microsoft.com</strong> Password: <strong>Pass@word1</strong> | |||||
</div> | |||||
</div> | |||||
</section> | |||||
</div> | |||||
</div> | |||||
</div> |
@ -1,100 +0,0 @@ | |||||
@model Microsoft.eShopOnContainers.Services.Identity.API.Models.AccountViewModels.RegisterViewModel | |||||
<div class="brand-header-block"> | |||||
<ul class="container"> | |||||
<li class="active">REGISTER</li> | |||||
<li style="margin-right: 65px;"><a asp-area="" asp-controller="Account" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]">LOGIN</a></li> | |||||
</ul> | |||||
</div> | |||||
<div class="container register-container"> | |||||
<form asp-controller="Account" asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal"> | |||||
<h4 class="order-create-section-title">CREATE NEW ACCOUNT</h4> | |||||
<div asp-validation-summary="All" class="text-danger"></div> | |||||
<div class="row"> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.Name" class="control-label form-label">NAME</label> | |||||
<input asp-for="User.Name" class="form-control form-input" /> | |||||
<span asp-validation-for="User.Name" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.LastName" class="control-label form-label">LAST NAME</label> | |||||
<input asp-for="User.LastName" class="form-control form-input" /> | |||||
<span asp-validation-for="User.LastName" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.Street" class="control-label form-label">ADDRESS</label> | |||||
<input asp-for="User.Street" class="form-control form-input" /> | |||||
<span asp-validation-for="User.Street" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.City" class="control-label form-label"></label> | |||||
<input asp-for="User.City" class="form-control form-input" /> | |||||
<span asp-validation-for="User.City" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.State" class="control-label form-label"></label> | |||||
<input asp-for="User.State" class="form-control form-input" /> | |||||
<span asp-validation-for="User.State" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.Country" class="control-label form-label"></label> | |||||
<input asp-for="User.Country" class="form-control form-input" /> | |||||
<span asp-validation-for="User.Country" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.ZipCode" class="control-label form-label">POSTCODE</label> | |||||
<input asp-for="User.ZipCode" class="form-control form-input" /> | |||||
<span asp-validation-for="User.ZipCode" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.PhoneNumber" class="control-label form-label">PHONE NUMBER</label> | |||||
<input asp-for="User.PhoneNumber" class="form-control form-input" /> | |||||
<span asp-validation-for="User.PhoneNumber" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.CardNumber" class="control-label form-label">Card Number</label> | |||||
<input asp-for="User.CardNumber" class="form-control form-input" /> | |||||
<span asp-validation-for="User.CardNumber" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.CardHolderName" class="control-label form-label">Cardholder Name</label> | |||||
<input asp-for="User.CardHolderName" class="form-control form-input" /> | |||||
<span asp-validation-for="User.CardHolderName" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-3"> | |||||
<label asp-for="User.Expiration" class="control-label form-label">Expiration Date</label> | |||||
<input asp-for="User.Expiration" placeholder="MM/YY" class="form-control form-input form-input-small" /> | |||||
<span asp-validation-for="User.Expiration" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-3"> | |||||
<label asp-for="User.SecurityNumber" class="control-label form-label">Security Code</label> | |||||
<input asp-for="User.SecurityNumber" class="form-control form-input form-input-small" /> | |||||
<span asp-validation-for="User.SecurityNumber" class="text-danger" /> | |||||
</div> | |||||
</div> | |||||
<br /><br /> | |||||
<div class="row"> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="Email" class="control-label form-label"></label> | |||||
<input asp-for="Email" class="form-control form-input" /> | |||||
<span asp-validation-for="Email" class="text-danger"></span> | |||||
</div> | |||||
<div class="form-group col-sm-offset-6"></div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="Password" class="control-label form-label"></label> | |||||
<input asp-for="Password" class="form-control form-input" /> | |||||
<span asp-validation-for="Password" class="text-danger"></span> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="ConfirmPassword" class="control-label form-label"></label> | |||||
<input asp-for="ConfirmPassword" class="form-control form-input" /> | |||||
<span asp-validation-for="ConfirmPassword" class="text-danger"></span> | |||||
</div> | |||||
</div> | |||||
<br /><br /> | |||||
<div class="form-group"> | |||||
<button type="submit" class="btn btn-default btn-brand"> Register </button> | |||||
</div> | |||||
<br /><br /> | |||||
</form> | |||||
</div> |
@ -1,100 +0,0 @@ | |||||
@model Microsoft.eShopOnContainers.Services.Identity.API.Models.AccountViewModels.RegisterViewModel | |||||
<div class="container"> | |||||
<h1 class="mb-4 mt-5">[ New Account ]</h1> | |||||
<form asp-controller="Account" asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-register"> | |||||
<h4 class="order-create-section-title">Information</h4> | |||||
<div asp-validation-summary="All" class="text-danger"></div> | |||||
<div class="row"> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.Name" class="control-label form-label">Name</label> | |||||
<input asp-for="User.Name" class="form-control form-input" /> | |||||
<span asp-validation-for="User.Name" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.LastName" class="control-label form-label">Last Name</label> | |||||
<input asp-for="User.LastName" class="form-control form-input" /> | |||||
<span asp-validation-for="User.LastName" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.Street" class="control-label form-label">Address</label> | |||||
<input asp-for="User.Street" class="form-control form-input" /> | |||||
<span asp-validation-for="User.Street" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.City" class="control-label form-label"></label> | |||||
<input asp-for="User.City" class="form-control form-input" /> | |||||
<span asp-validation-for="User.City" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.State" class="control-label form-label"></label> | |||||
<input asp-for="User.State" class="form-control form-input" /> | |||||
<span asp-validation-for="User.State" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.Country" class="control-label form-label"></label> | |||||
<input asp-for="User.Country" class="form-control form-input" /> | |||||
<span asp-validation-for="User.Country" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.ZipCode" class="control-label form-label">Postcode</label> | |||||
<input asp-for="User.ZipCode" class="form-control form-input" /> | |||||
<span asp-validation-for="User.ZipCode" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.PhoneNumber" class="control-label form-label">Phone Number</label> | |||||
<input asp-for="User.PhoneNumber" class="form-control form-input" /> | |||||
<span asp-validation-for="User.PhoneNumber" class="text-danger" /> | |||||
</div> | |||||
</div> | |||||
<div class="mt-4"> | |||||
<h4 class="order-create-section-title">Credit Card</h4> | |||||
</div> | |||||
<div class="row"> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.CardNumber" class="control-label form-label">Card Number</label> | |||||
<input asp-for="User.CardNumber" class="form-control form-input" /> | |||||
<span asp-validation-for="User.CardNumber" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="User.CardHolderName" class="control-label form-label">Cardholder Name</label> | |||||
<input asp-for="User.CardHolderName" class="form-control form-input" /> | |||||
<span asp-validation-for="User.CardHolderName" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-3"> | |||||
<label asp-for="User.Expiration" class="control-label form-label">Expiration Date</label> | |||||
<input asp-for="User.Expiration" placeholder="MM/YY" class="form-control form-input form-input-small" /> | |||||
<span asp-validation-for="User.Expiration" class="text-danger" /> | |||||
</div> | |||||
<div class="form-group col-sm-3"> | |||||
<label asp-for="User.SecurityNumber" class="control-label form-label">Security Code</label> | |||||
<input asp-for="User.SecurityNumber" class="form-control form-input form-input-small" /> | |||||
<span asp-validation-for="User.SecurityNumber" class="text-danger" /> | |||||
</div> | |||||
</div> | |||||
<br /><br /> | |||||
<div class="row"> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="Email" class="control-label form-label"></label> | |||||
<input asp-for="Email" class="form-control form-input" /> | |||||
<span asp-validation-for="Email" class="text-danger"></span> | |||||
</div> | |||||
<div class="form-group col-sm-offset-6"></div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="Password" class="control-label form-label"></label> | |||||
<input asp-for="Password" class="form-control form-input" /> | |||||
<span asp-validation-for="Password" class="text-danger"></span> | |||||
</div> | |||||
<div class="form-group col-sm-6"> | |||||
<label asp-for="ConfirmPassword" class="control-label form-label"></label> | |||||
<input asp-for="ConfirmPassword" class="form-control form-input" /> | |||||
<span asp-validation-for="ConfirmPassword" class="text-danger"></span> | |||||
</div> | |||||
</div> | |||||
<br /><br /> | |||||
<div class="d-flex mt-3 justify-content-end"> | |||||
<button type="submit" class="btn btn-primary">Register</button> | |||||
</div> | |||||
<br /><br /> | |||||
</form> | |||||
</div> |
@ -0,0 +1,7 @@ | |||||
<div class="page-device-success"> | |||||
<div class="lead"> | |||||
<h1>Success</h1> | |||||
<p>You have successfully authorized the device</p> | |||||
</div> | |||||
</div> |
@ -0,0 +1,23 @@ | |||||
@model string | |||||
<div class="page-device-code"> | |||||
<div class="lead"> | |||||
<h1>User Code</h1> | |||||
<p>Please enter the code displayed on your device.</p> | |||||
</div> | |||||
<partial name="_ValidationSummary" /> | |||||
<div class="row"> | |||||
<div class="col-sm-6"> | |||||
<form asp-action="UserCodeCapture"> | |||||
<div class="form-group"> | |||||
<label for="userCode">User Code:</label> | |||||
<input class="form-control" for="userCode" name="userCode" autofocus /> | |||||
</div> | |||||
<button class="btn btn-primary" name="button">Submit</button> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
</div> |
@ -0,0 +1,108 @@ | |||||
@model DeviceAuthorizationViewModel | |||||
<div class="page-device-confirmation"> | |||||
<div class="lead"> | |||||
@if (Model.ClientLogoUrl != null) | |||||
{ | |||||
<div class="client-logo"><img src="@Model.ClientLogoUrl"></div> | |||||
} | |||||
<h1> | |||||
@Model.ClientName | |||||
<small class="text-muted">is requesting your permission</small> | |||||
</h1> | |||||
@if (Model.ConfirmUserCode) | |||||
{ | |||||
<p>Please confirm that the authorization request quotes the code: <strong>@Model.UserCode</strong>.</p> | |||||
} | |||||
<p>Uncheck the permissions you do not wish to grant.</p> | |||||
</div> | |||||
<div class="row"> | |||||
<div class="col-sm-8"> | |||||
<partial name="_ValidationSummary" /> | |||||
</div> | |||||
</div> | |||||
<form asp-action="Callback"> | |||||
<input asp-for="UserCode" type="hidden" value="@Model.UserCode" /> | |||||
<div class="row"> | |||||
<div class="col-sm-8"> | |||||
@if (Model.IdentityScopes.Any()) | |||||
{ | |||||
<div class="form-group"> | |||||
<div class="card"> | |||||
<div class="card-header"> | |||||
<span class="glyphicon glyphicon-user"></span> | |||||
Personal Information | |||||
</div> | |||||
<ul class="list-group list-group-flush"> | |||||
@foreach (var scope in Model.IdentityScopes) | |||||
{ | |||||
<partial name="_ScopeListItem" model="@scope" /> | |||||
} | |||||
</ul> | |||||
</div> | |||||
</div> | |||||
} | |||||
@if (Model.ApiScopes.Any()) | |||||
{ | |||||
<div class="form-group"> | |||||
<div class="card"> | |||||
<div class="card-header"> | |||||
<span class="glyphicon glyphicon-tasks"></span> | |||||
Application Access | |||||
</div> | |||||
<ul class="list-group list-group-flush"> | |||||
@foreach (var scope in Model.ApiScopes) | |||||
{ | |||||
<partial name="_ScopeListItem" model="scope" /> | |||||
} | |||||
</ul> | |||||
</div> | |||||
</div> | |||||
} | |||||
<div class="form-group"> | |||||
<div class="card"> | |||||
<div class="card-header"> | |||||
<span class="glyphicon glyphicon-tasks"></span> | |||||
Description | |||||
</div> | |||||
<div class="card-body"> | |||||
<input class="form-control" placeholder="Description or name of device" asp-for="Description" autofocus> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
@if (Model.AllowRememberConsent) | |||||
{ | |||||
<div class="form-group"> | |||||
<div class="form-check"> | |||||
<input class="form-check-input" asp-for="RememberConsent"> | |||||
<label class="form-check-label" asp-for="RememberConsent"> | |||||
<strong>Remember My Decision</strong> | |||||
</label> | |||||
</div> | |||||
</div> | |||||
} | |||||
</div> | |||||
</div> | |||||
<div class="row"> | |||||
<div class="col-sm-4"> | |||||
<button name="button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button> | |||||
<button name="button" value="no" class="btn btn-secondary">No, Do Not Allow</button> | |||||
</div> | |||||
<div class="col-sm-4 col-lg-auto"> | |||||
@if (Model.ClientUrl != null) | |||||
{ | |||||
<a class="btn btn-outline-info" href="@Model.ClientUrl"> | |||||
<span class="glyphicon glyphicon-info-sign"></span> | |||||
<strong>@Model.ClientName</strong> | |||||
</a> | |||||
} | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</div> |
@ -0,0 +1,64 @@ | |||||
@model DiagnosticsViewModel | |||||
<div class="diagnostics-page"> | |||||
<div class="lead"> | |||||
<h1>Authentication Cookie</h1> | |||||
</div> | |||||
<div class="row"> | |||||
<div class="col"> | |||||
<div class="card"> | |||||
<div class="card-header"> | |||||
<h2>Claims</h2> | |||||
</div> | |||||
<div class="card-body"> | |||||
<dl> | |||||
@foreach (var claim in Model.AuthenticateResult.Principal.Claims) | |||||
{ | |||||
<dt>@claim.Type</dt> | |||||
<dd>@claim.Value</dd> | |||||
} | |||||
</dl> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="col"> | |||||
<div class="card"> | |||||
<div class="card-header"> | |||||
<h2>Properties</h2> | |||||
</div> | |||||
<div class="card-body"> | |||||
<dl> | |||||
@foreach (var prop in Model.AuthenticateResult.Properties.Items) | |||||
{ | |||||
<dt>@prop.Key</dt> | |||||
<dd>@prop.Value</dd> | |||||
} | |||||
@if (Model.Clients.Any()) | |||||
{ | |||||
<dt>Clients</dt> | |||||
<dd> | |||||
@{ | |||||
var clients = Model.Clients.ToArray(); | |||||
for(var i = 0; i < clients.Length; i++) | |||||
{ | |||||
<text>@clients[i]</text> | |||||
if (i < clients.Length - 1) | |||||
{ | |||||
<text>, </text> | |||||
} | |||||
} | |||||
} | |||||
</dd> | |||||
} | |||||
</dl> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
@ -0,0 +1,87 @@ | |||||
@model GrantsViewModel | |||||
<div class="grants-page"> | |||||
<div class="lead"> | |||||
<h1>Client Application Permissions</h1> | |||||
<p>Below is the list of applications you have given permission to and the resources they have access to.</p> | |||||
</div> | |||||
@if (Model.Grants.Any() == false) | |||||
{ | |||||
<div class="row"> | |||||
<div class="col-sm-8"> | |||||
<div class="alert alert-info"> | |||||
You have not given access to any applications | |||||
</div> | |||||
</div> | |||||
</div> | |||||
} | |||||
else | |||||
{ | |||||
foreach (var grant in Model.Grants) | |||||
{ | |||||
<div class="card"> | |||||
<div class="card-header"> | |||||
<div class="row"> | |||||
<div class="col-sm-8 card-title"> | |||||
@if (grant.ClientLogoUrl != null) | |||||
{ | |||||
<img src="@grant.ClientLogoUrl"> | |||||
} | |||||
<strong>@grant.ClientName</strong> | |||||
</div> | |||||
<div class="col-sm-2"> | |||||
<form asp-action="Revoke"> | |||||
<input type="hidden" name="clientId" value="@grant.ClientId"> | |||||
<button class="btn btn-danger">Revoke Access</button> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<ul class="list-group list-group-flush"> | |||||
@if (grant.Description != null) | |||||
{ | |||||
<li class="list-group-item"> | |||||
<label>Description:</label> @grant.Description | |||||
</li> | |||||
} | |||||
<li class="list-group-item"> | |||||
<label>Created:</label> @grant.Created.ToString("yyyy-MM-dd") | |||||
</li> | |||||
@if (grant.Expires.HasValue) | |||||
{ | |||||
<li class="list-group-item"> | |||||
<label>Expires:</label> @grant.Expires.Value.ToString("yyyy-MM-dd") | |||||
</li> | |||||
} | |||||
@if (grant.IdentityGrantNames.Any()) | |||||
{ | |||||
<li class="list-group-item"> | |||||
<label>Identity Grants</label> | |||||
<ul> | |||||
@foreach (var name in grant.IdentityGrantNames) | |||||
{ | |||||
<li>@name</li> | |||||
} | |||||
</ul> | |||||
</li> | |||||
} | |||||
@if (grant.ApiGrantNames.Any()) | |||||
{ | |||||
<li class="list-group-item"> | |||||
<label>API Grants</label> | |||||
<ul> | |||||
@foreach (var name in grant.ApiGrantNames) | |||||
{ | |||||
<li>@name</li> | |||||
} | |||||
</ul> | |||||
</li> | |||||
} | |||||
</ul> | |||||
</div> | |||||
} | |||||
} | |||||
</div> |
@ -1,30 +1,32 @@ | |||||
<div class="welcome-page container"> | |||||
<div class="row page-header"> | |||||
<div class="col-sm-10"> | |||||
<h1> | |||||
<img class="icon" src="~/icon.jpg"> | |||||
Welcome to Duende.IdentityServer | |||||
@*<small>(build {version})</small>*@ | |||||
</h1> | |||||
</div> | |||||
</div> | |||||
@using System.Diagnostics | |||||
<div class="row"> | |||||
<div class="col-sm-8"> | |||||
<p> | |||||
IdentityServer publishes a | |||||
<a href="~/.well-known/openid-configuration">discovery document</a> | |||||
where you can find metadata and links to all the endpoints, key material, etc. | |||||
</p> | |||||
</div> | |||||
</div> | |||||
<div class="row"> | |||||
<div class="col-sm-8"> | |||||
<p> | |||||
Here are links to the | |||||
<a href="https://github.com/identityserver/Duende.IdentityServer">source code repository</a>, | |||||
and <a href="https://github.com/identityserver/Duende.IdentityServer.Samples">ready to use samples</a>. | |||||
</p> | |||||
</div> | |||||
</div> | |||||
@{ | |||||
var version = FileVersionInfo.GetVersionInfo(typeof(Duende.IdentityServer.Hosting.IdentityServerMiddleware).Assembly.Location).ProductVersion.Split('+').First(); | |||||
} | |||||
<div class="welcome-page"> | |||||
<h1> | |||||
<img src="~/icon.jpg"> | |||||
Welcome to IdentityServer4 | |||||
<small class="text-muted">(version @version)</small> | |||||
</h1> | |||||
<ul> | |||||
<li> | |||||
IdentityServer publishes a | |||||
<a href="~/.well-known/openid-configuration">discovery document</a> | |||||
where you can find metadata and links to all the endpoints, key material, etc. | |||||
</li> | |||||
<li> | |||||
Click <a href="~/diagnostics">here</a> to see the claims for your current session. | |||||
</li> | |||||
<li> | |||||
Click <a href="~/grants">here</a> to manage your stored grants. | |||||
</li> | |||||
<li> | |||||
Here are links to the | |||||
<a href="https://github.com/identityserver/IdentityServer4">source code repository</a>, | |||||
and <a href="https://github.com/IdentityServer/IdentityServer4/tree/main/samples">ready to use samples</a>. | |||||
</li> | |||||
</ul> | |||||
</div> | </div> |
@ -0,0 +1,11 @@ | |||||
@model RedirectViewModel | |||||
<div class="redirect-page"> | |||||
<div class="lead"> | |||||
<h1>You are now being returned to the application</h1> | |||||
<p>Once complete, you may close this tab.</p> | |||||
</div> | |||||
</div> | |||||
<meta http-equiv="refresh" content="0;url=@Model.RedirectUrl" data-url="@Model.RedirectUrl"> | |||||
<script src="~/js/signin-redirect.js"></script> |
@ -1,54 +0,0 @@ | |||||
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
<meta charset="utf-8" /> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||||
<meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline'; script-src-elem 'unsafe-inline'"> | |||||
<title>eShopOnContainers - Identity</title> | |||||
<link rel="icon" type="image/x-icon" href="~/favicon.ico" /> | |||||
<link rel="shortcut icon" type="image/x-icon" href="~/favicon.ico" /> | |||||
<environment names="Development"> | |||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" /> | |||||
<link rel="stylesheet" href="~/css/site-spa.css" /> | |||||
</environment> | |||||
<environment names="Staging,Production"> | |||||
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css" | |||||
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css" | |||||
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" /> | |||||
<link rel="stylesheet" href="~/css/site-spa.min.css" asp-append-version="true" /> | |||||
</environment> | |||||
</head> | |||||
<body> | |||||
<div class="es-header"> | |||||
<div class="container"> | |||||
<article class="d-flex align-content-center justify-content-between"> | |||||
<section> | |||||
<a asp-controller="home" asp-action="ReturnToOriginalApplication" asp-route-returnUrl="@ViewData["ReturnUrl"]"> | |||||
<img class="es-header-brand" src="~/images/logo_color.svg"> | |||||
</a> | |||||
</section> | |||||
</article> | |||||
</div> | |||||
</div> | |||||
<div class="content"> | |||||
@RenderBody() | |||||
</div> | |||||
<footer class="footer"> | |||||
<div class="container"> | |||||
<article class="d-flex w-100 h-100 justify-content-between align-items-center"> | |||||
<section> | |||||
<img class="footer-brand" src="~/images/logo.svg"> | |||||
</section> | |||||
<section> © e-Shoponcontainers. All rights reserved </section> | |||||
</article> | |||||
</div> | |||||
</footer> | |||||
<script src="~/lib/jquery/jquery.js"></script> | |||||
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script> | |||||
@RenderSection("scripts", required: false) | |||||
</body> | |||||
</html> |
@ -1,56 +1,48 @@ | |||||
<!DOCTYPE html> | <!DOCTYPE html> | ||||
<html> | |||||
<html lang="en"> | |||||
<head> | <head> | ||||
<meta charset="utf-8" /> | <meta charset="utf-8" /> | ||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||||
<meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline'; script-src-elem 'unsafe-inline'"> | |||||
<title>eShopOnContainers Identity</title> | |||||
<link rel="icon" type="image/x-icon" href="~/favicon.ico" /> | |||||
<link rel="shortcut icon" type="image/x-icon" href="~/favicon.ico" /> | |||||
<environment names="Development"> | |||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" /> | |||||
<link rel="stylesheet" href="~/css/site.css" /> | |||||
</environment> | |||||
<environment names="Staging,Production"> | |||||
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css" | |||||
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css" | |||||
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" /> | |||||
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" /> | |||||
</environment> | |||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" /> | |||||
<title>eShopOnContainers - Identity</title> | |||||
<link rel="icon" type="image/x-icon" href="favicon.png"> | |||||
<link rel="shortcut icon" type="image/x-icon" href="~/favicon.png" /> | |||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> | |||||
<link rel="stylesheet" href="~/css/site.css" /> | |||||
</head> | </head> | ||||
<body> | <body> | ||||
<div class="navbar navbar-inverse fixed-top es-header"> | |||||
<div class="es-header"> | |||||
<div class="container"> | <div class="container"> | ||||
<div class="row"> | |||||
<div class="navbar-header col-sm-6 col-xs-8"> | |||||
<a asp-controller="home" asp-action="ReturnToOriginalApplication" asp-route-returnUrl="@ViewData["ReturnUrl"]"> | |||||
<div class="navbar-brand"></div> | |||||
</a> | |||||
</div> | |||||
</div> | |||||
<article class="d-flex align-content-center justify-content-between"> | |||||
<section> | |||||
<img class="es-header-brand" src="~/images/logo_color.svg"> | |||||
</section> | |||||
</article> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div> | |||||
<div class="content"> | |||||
@RenderBody() | @RenderBody() | ||||
<br /><br /> | |||||
<footer> | |||||
<div class="container"> | |||||
<div class="row"> | |||||
<div class="col-sm-6"> | |||||
<br><div class="brand"></div> | |||||
</div> | |||||
<div class="col-sm-6"> | |||||
<img class="text hidden-xs" src="~/images/main_footer_text.PNG" width="335" height="26" alt="footer text image" /> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</footer> | |||||
</div> | </div> | ||||
<footer class="footer"> | |||||
<div class="container"> | |||||
<article class="d-flex w-100 h-100 justify-content-between align-items-center"> | |||||
<section> | |||||
<img class="footer-brand" src="~/images/logo.svg"> | |||||
</section> | |||||
<section> © eShopOnContainers. All rights reserved </section> | |||||
</article> | |||||
</div> | |||||
</footer> | |||||
<script src="~/lib/jquery/dist/jquery.slim.min.js"></script> | |||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> | |||||
<script src="~/lib/jquery/jquery.js"></script> | |||||
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script> | |||||
@RenderSection("scripts", required: false) | @RenderSection("scripts", required: false) | ||||
</body> | </body> | ||||
</html> | </html> |
@ -1,27 +0,0 @@ | |||||
@using Microsoft.AspNetCore.Identity | |||||
@using Microsoft.eShopOnContainers.Services.Identity.API.Models | |||||
@inject SignInManager<ApplicationUser> SignInManager | |||||
@inject UserManager<ApplicationUser> UserManager | |||||
@if (SignInManager.IsSignedIn(User)) | |||||
{ | |||||
<form asp-area="" asp-controller="Account" asp-action="Logout" method="post" id="logoutForm" class="navbar-right"> | |||||
<ul class="nav navbar-nav navbar-right"> | |||||
<li> | |||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello @UserManager.GetUserName(User)!</a> | |||||
</li> | |||||
<li> | |||||
<button type="submit" class="btn btn-link navbar-btn navbar-link">Log off</button> | |||||
</li> | |||||
</ul> | |||||
</form> | |||||
} | |||||
else | |||||
{ | |||||
<ul class="nav navbar-nav navbar-right"> | |||||
<li><a asp-area="" asp-controller="Account" asp-action="Register">Register</a></li> | |||||
<li><a asp-area="" asp-controller="Account" asp-action="Login">Log in</a></li> | |||||
</ul> | |||||
} |
@ -1,14 +0,0 @@ | |||||
<environment names="Development"> | |||||
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script> | |||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script> | |||||
</environment> | |||||
<environment names="Staging,Production"> | |||||
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js" | |||||
asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js" | |||||
asp-fallback-test="window.jQuery && window.jQuery.validator"> | |||||
</script> | |||||
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js" | |||||
asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js" | |||||
asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"> | |||||
</script> | |||||
</environment> |
@ -1 +1,2 @@ | |||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers | |||||
@using IdentityServerHost.Quickstart.UI | |||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
@ -1,3 +1,3 @@ | |||||
@{ | |||||
@{ | |||||
Layout = "_Layout"; | Layout = "_Layout"; | ||||
} | } |
@ -0,0 +1 @@ | |||||
{"Version":1,"Id":"10C452043E09C5A22FC8D97669868B8F","Created":"2022-11-30T12:03:40.1630635Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8JmnWj_BpdBDujWYr1NmSawi1hoBHaFG7R1_Hu0pSObE9SiTNg1Wm83zVA3-oCwbntipelQt_gpZ3KH-NMBYdleYt4Gzhpvf4uchFNFzvdnx1X9jpWQi9WCmUm3cVNfzNG2eIKzrVLAoiaWLuDDG9XT-u2ojWIuKRKH7zEBMGrqCDQiVHZlBuqH_qzpWUQn-qKYEkFRcm05OmMYXJA0AquXimdOl1V_BcagZOpNVzG3C5t7lZTsTrm7FpD5zMdyZxL2VJJjRy5fUZbIQIjbQdTGYJaROhj-Sc4h2MgyrMNYJ-TQZKci_5ZoN-GJfjdaB-3UHDk_uFrVk9WpAYEWw0rw-I05tMu8vVxlE2jAnEGaVWEqS0f_Bz53MkDQb2YEleekTwaLJM1JZF791NTaG7oDXHSetSeo7UngHt3D9Ls2FHnZwV-B5mvzZju_-U7inwsoKXjZeqzQfQMs2IsIgiXJJkS20PObmosEbq7LkT97qRrinGicY0pA2zIcDmLiywYzWpsP6Rjw7epDaDlPu2J9BZ93Ni-slfTkNdn2SvAfE2dsJmV7Pz5tMEE-0mBlSMbcrs8TuVQayYPm2dm-sZLCv-iQu0rrvGcJ-lLPliby69du5oFU6WKpo52X4Du9tcM-5prBLWRVt_gcq16zBY_f8ieNnq6b0C_9chaztCevTQAouiSk7ni3zjJo3RuG1wPUts2tLT4sjY6IK2Zbhsvi-negegn91cFA0MzdOaD2Ca2GWN2L2kRrDWioVz_P4ztVFIU3SrgjW6stOGNreRvOB8txUCexJuUP0rb85pOa8rxPCkk1Iw8BjjRdirhvU3bvqhml6i2_iqNTAxpx89A6LRUuOYvYiwJersfhOF9F-FfOVGNxxfm1AKKnUBDfgLWDAw1r8PBeV6huAqUSIwaN5PcQJcdsfER9FeP9_CJqDgNGaL-psm2NQDaVV2Vx4m3GqWQhKMlk90zee5XNe1wobBS0XW_GQYieWWhH5t0pyzJi0QvpFyMAz1mcGXQs-TiHoU142FlFQojIu3_Ynf2RHOOByWDM9krMKMn6ZTnLWgBXnWtMefXqYNEhlACtP8lIXBxcbI2wFeBgRmaQA2DtEEjlymZ4RTaiQEuVC6B_wtTTVVFx3CL-TM62Xs-FOWTYafzKqGsHKzVWnB3fZqjDh4DNwb_A4mkfxoyb2UI9W4P7BJmbV5tOktYudVAFLt3XL7uNhBYLDSOjL_n8bixTg-sLUyO1H5-IxYQJciZdAro4yGO7r40FTYpZfTyvlr3RqFMDhBEPu_nSx9lPKrgpei0DmqYWw_cHJ3mPR1HDUE_i8Prlyzsyf9F-pYV4HMyupetXeXxMAIULnB52SE7uYfSUZHC25Ffo_tif8zMPo2ZKiMpc42BZ0cBptkPKUCvkR-Cq3pFnqXkKADWh1LLBP09hZtOjXqkFdsIFZq5KfRE6UVIyMJ91L-OSHOnggdjScFJzPlgEteUbydKSlPEBkicIs-uXXe8Tk8KZg2CfUU2WnziP0-VgymbTomKR5xz36kJqMjP2VYp6Xy-t3joQEqPE50ntT0AYUFJb6NZt5stHQz76-WecPnA6ZSA3aJec7NlLrxwM6BMGDyBZpbkgWtWFJdGWz6m9j4RKuyZBnyOXzYZU8FDNpDu6FVEymUkYxEhUnpIl7PZWuiZGADwj9-Bpzg0To8jMJ-jgx8w3RDzb5DoM6-jh0IJ5vecn8lSfRzHcOu48CLY5mTuHFr93YKiQCK06DS8VY3kI1b8irtdvmY-IvUCyCJlpzBYh8vFrS2PdJYsRVWuGJraxMiHYP_zH5r4Q4z27p6vt0Rbm0S4Fm1I9z4Hmaa4igF1mnktuTMizyxKWTsh3DjbsB_6W_lNU62RqJAt_IGr69MIphVY6ZOcbLx2I8p0z4yWpuYJey2BmRQPQeCZ-Tb9HtHESxTBFxupYul8619OJWA7RfLl-xv43X81OZnaxE9wLVFs5uEowKoEOzl_K5nxOw4igCiWYWufaFMMrZSu-0ni9RVSwytOPiEbGV0XOcVnB_lHDz1ymvDI4u1rfOVkehwB_wFBeRJRSEymzoD-bgRox-a1qLJNhOsdm9WUdZF3MHbmmA8NpyqU5qOTTqCvY-FXdJHL_OHmq1nDVHCJvcwLqUfPFMfFAi0MZQEhRndeQWYGdRGOhmvtwbRX-Hryz2HKXber1ixUvY3-XCxjzSyZdn845UzpKBdsgwAwY-LFzlgNQGEKHzsZUl8MSeqx3iO2QwDwC5eV4gNWBC5QBw0NAloBmdnQ84sSWydPViOYW1CinUQAnZsW2Z-0Xw0FP4fbS4HvulNBMIxE_mfJsHny4YZu_463zGR0SrV2hEFSiXuwSx27K6xSyPRB26__t8w37AeFYXtavqr9rB5GXnEJRQkW-i5lDnhtA93uRjfNzVhUpcTK-ROm3h7b4UrUTaK_IMLXxoYEjOFm8-cw1ZGHu3","DataProtected":true} |
@ -0,0 +1 @@ | |||||
{"Version":1,"Id":"1417AE0BAF26F393609EF30FA355D06C","Created":"2022-11-30T15:12:36.3392917Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8Etya4tS5j5DiLTWt2Gh6S_0SY7Shff7x3C-zhStPviP80hTMRjXNVXCumUQnwoKrJ48OLZhHuU7wgSxqXaAT3hk35KvO_KOhz5pV_sWLPOogHC-hpQTIzSEbXzyfEIkliqVgW3AfpLbuDTZY4Hj8hu-OM0waYEjCQkIv-PrftxX7JU4KCT1rxZBUrDJN_mKCQEGWy0LGLVsLV8dKyLMSvJsIz3HJh4sngzist-XVaPxat2eMPqVOJEWkbUf6VqwJOnssR-ZOS9b63YZPhihUcz6J0T5Y2aWIyZfZwSkuh8DiL9_wzKSRTmplaY1tzeq_-FhbkgwtLX9VDbeesx_HGL_Pxj5SG5aprfZ7DN2FBr70m7olb-bEl8jOm0w7ot0QGwxHft53Nf9piLEyQp4hsReq-PNwCirzFNkQ5aM6Kwvwitkop_Ab8gpzfwiEw-uVyPN_gpkrt099MQrdtljc9gXHKuXr4321pgcbwO6Ua-amRcF_9uYSCVBdIhwmpaQAgMmI-0uI-5Ph7H23ZV2zR1epClkiR2t5RWOtXNgLqoupXyBjDWrse3al-CsdnkofmtY0bqr2ZdNd_EKFxqn7Gf5-dNuPJEboDUdwvjUpWYIzMlq6_3q9KP012SbimkqMgz3-3jDBw_xe43DvZd2CnWFTDQSuD0m7DTf2GVL0DlasQNYQgO1T7xN7hgScsXmzhJNUx7iz2ze5BMqWV3vw6P6ny4sj_QA4KAi0zxl7sPgPo2p-65MWlbZy646ZYlm-jDD9dfORdo9nmURg3ITYNVUREtVg4IcEzPVDBMnV-NEf3T3vHD1V49yVhovA9oAHj4uTye-bbLUIhoLPCZ5GfR8AEHE4IhL0JyjuaWilp1qNEXAhziwZUELenYAf74wEzMvSSobEWL5Hq-ApMdRePBjjeGrIobS6KuE7b3HVsYwJt9pWvmVCYZ-nQCDAOIFcgH5kifkHu6NEi23Jk7a5hmgq1kVJXr5xcli9B-bHu1mO1kl0gXsqJcGpfetcw8bEtwO8F_6kdkmW3RXLa3rvrnkigPJJtn4sm_3ee1dqFYarJlXHPugwuyzfEPXNiT1HCcDhQYlrYxfKSzmlhjthGn14W2ZYtb3YVCXUFReA8V20rs4ec7VTdjuKCPgA8bk3xKevr6aETF9I65hbI0pIFQ9_cJHkl9a3BZL-X5qSH2s3_rvPupVuP6mu-Y1pUEGp4p5XjZc0CqsLm-W6MsUKmBgeCiokmUGfs5P_Riv9ZuvlHyYcwj2qmgf4MDkmhThfisook1nrPomO1lByHMRHx8rwbVQQXHPKMI32WZxfpRQiuAERonMvLBd2P2UOutqe6t7qy_gbeM0WNEcKb08UowSxGUFnEH6Vk1Dx9Vzye2tw7ej-EsT39hudhe_59nd9IYvx1o547N1cI_ASDVn3ibVQU_BWLrSK-HIqGaA52F_hthPGpMjVBO8xtLXHiyxqI-gh9iLq0vpvMeFQjyLSmFjKQifMEVaATAF9h2TJTc0yqkIVN9y77LvkUDoFvi96zGSYDyIf6rtiF_E1GnAwu-6Le16bj9npI-FcNmh-DRIvA6LepbSXs27rVMjQdK6dOPDavu5Y9IM5bg0pndKek0orZzkbaFwxTxmM08AhKlyN6PPiIqcZ7i456FQDhqMTUEeBEShsOJy4573e0GA-J1sHvzVQYiKDlzvgYD-zh79OxnJXlq22BNrWsQ6pqXfXgB5UIvhUTrmcrUtv3uw6GcAyl76ySgfs3Hb9Mpk0aeUdI-xyMuxxJvz1w1NffcuXPsxf3VosZO8vkofjVSO8lGtzarScy-abFGD9UA-S7xGvz0rqRSAEnpxe4AZ5eBfs_VmTy21RYJqxPbrYWcjrRdQjxERhND70wiuLkF1pivUQ-bohP3w0EfV0fJoSoH9HthQ9_aY2ASJnv82BX_lPRdEgYVh1RYWziVGB_6WWkch8BLrUzXezJZcfQHGuEQYzVB17RQIhCqFo7GexmHkFB40punvvS1gUZOQLiqSu8UvJ27F5KNLpkgDy8q0pDWewRGmZ5J09aMlJE6e08u_gPrA9G52SWXvdkX8CRvPn-xOLsBfb1_84QBhB1PuSKeptNLjgMufTdfUkGrL_b3fTo4AgLz7VuKrAMNYjwCTLr4e6towu-JJ08Fr3c8igbWApP6yNiVbE1W1mX66jaRfyw3jiVAP2ytEH_0kLOE3BDFkepIopwGtkQ2ultqFHxr_-oSadSpP3g4dLt1GfQ7fjzYkYAkstXxgYgGGe7Q-mwQM1dR8RnE0jFvQnTKi-Oc_pj0h91HgRK_ASQrqrgOf9JN6McbVlE3VbEKvFfOKP8ukFa6i8n7tk1H0B8bX5iy4m_b3i3MbQmQ16gkZ2ECW29-bA4vWz6-R3LZ3kf2hHAJzv8U6cG71KLVgWSBxIyRJE-cSiBPxtM1hGT1qcJvpAVWvowkcO9Anng3lrfgnGaYh","DataProtected":true} |
@ -0,0 +1 @@ | |||||
{"Version":1,"Id":"22178AEDB80CD0A41DF6874FE93F794D","Created":"2022-11-29T07:52:44.7571677Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8E4RSuU_s2BGnQ_I0J1Xz9YiYIkBDFNHHGo4A20gtVeWqoNIQNxtP1p0JacgjeZkGl9NlE2-7qD02Vs9NIZ_IfOHW71XkfBIuIp5JvnwMi-X42Qw1F6UTQKNlYNvk1Fh27a6tM-mQgxysayCdDLlJWoRJYKDPDkUO03rZVkWDD2r6QNcMNtYhjM7oxy2yiw479PIeIldPOk73cPqp_k4oMPTRWwS04dX8fpQNA_3d2PoV1sw8-Hbmspiv3PO-umUGU2OFcN243cP9aaGXDoR-RgEbCn225WuHAWTe3tf39zVDtbAWDnRTNEX_jCNBItSaFOdMerDKD28PXTnWXuoMKyZ1_8ETZk0CDMAMVktqOatYrx0JHisG8vJbHtzxIvQrZL1YqoAN_qraVLgTl99WIZv56rYSb3RbNSzqB7yNn1Y_YAobXVbqJCaqqVG6uEaQxQ4WRI78sit630anIlKP7vccOtJH_bspQlPisNVrpCWiGo0D0IzNsZXLNPkc9kUbJKGc3xgE5H-z9kbk3sOecVH_iQy4b6fJ0IrDGz9fIhu2UI0-_lUE6sQ0g-NAA9kW68vD6PVSytiag7tFVV0olRPhaiBkSRuNGKjqsPmlPtkpzVxuTxSj19YxoC200cFARZq68qjxZ7ugdHQGlqd6bKFjj6NWCfG3Sbub1HpTkPnEUIR6ic8EfUrbu18MlOh-7bnIJkIuz0Nr5zZ9tPgiJCdGPjNo47z4i-kPWiNhS9k1sJqg60dgCT3onC2Uft4F4dnQpIKb28Fli9qkTDOg9gObEr9W19tMJvEPAhWJSl6HSmIJ-TElrW7nNCdS-H7PIlGWIvP6f0CCzrlgtsfMh8TK0B-Vpv5ky9D67L03IA7VfrhHss5mEkoz2s5-yefn2Wo5uMTxy53O4GIS8j3lF0ufUIBjGqR5q6IyEQUyU79OA5A7A3gJx78Cb-OEK3GixhYwjXYOw1YcguMZo_rGsq-EnjPoVBuZfdwyEIZ847gVwDzFsh0FHZIi2vVxXZ3tNGjJI2TXxNx7nyXuBxERgCPI3C5OeK2Uy8e3ooZCqPICKS9xEnICZteEcSYkRFmvPX6O1pZMKWoiyzGe-bhuBiotTB3yGIVlkhMyqNni2hQHoR5b1CX4YaGBMT_fdtW4uhvYLhLnGR69aal2ffIhiFJMfB_ByI8sALzgsn5jW-zOAmtiEOaPs1O9FqPf1x3Wk2YuoyujgHjHi9SSUz1hAJRmsyCmcYsMdGjMd-wnk83dsdMMeNANTKJHlZP5-__XA8emO-sloHrCiHpALX-JB5RQ1BGVD-dPiTlM0MYeTLdt7y5HRKSAbsOCnnF1vzV8y8NkUgpKVLTPRsJaoNYqik4Wpoa2d2ez-gXrG9S-pAByXHyTHiVDVNyqUTDMI0s8Fm_6_QwJTkgGEV6dR6r89Rzq70FFjdKB5RygRLhRdF2Be8blyExHI3HQg8_S9fcZ3zCybjatlNuNCgNZTTelC1UzWSEql3HqeqiMWIi9Kr8GNl59OIGm2lm9KNoyvcFHlQVQ33lw4akd5Z7aYu_HcdpBhMcEr9CuN6yQPhbCDOVhsMkqzXGzpwC2ezFnALAkbj9P44vRU9p0FEKFDNn_pQimVTLQJoU5Taf1n70CxYcPgM9BU3JPL9VUb9rDS3bHA7XQoO6878k6PkLbJ6GcfIJitaRZL5TkAUJUywH-a5xADVo-1_k3ADJYhOTbwxsYU96Kuhw8FOIToBH8CtLnqJLoJ8iwVPjB9LSsHPhmR7Rsi-x7Oi5JeugLctOvBn1sq7x_hHQKPVHA5LvtQ6Y719bOgPKLybQSCebe0ordUIsPAZMq-OvLj1sPS8PvfvZBDkyUA04avHm5kgT6RJF7ARUI1vMNLijm-_dxxrzY2bj6FHT340iFDURFePLdSM-Ctu3X-gLXKqPzQ6MBBQS0pyC3GkuX7J_GhPOZ-yXvRJmvIEoDAVEvjUQc57Ud6nyS_C_ljLmIoHghJiTJxNOOjR5pucb9Vljxm_BGPZwMi6-bTbD454ESbZ1kyvOAG73PjnPoQSaEYyM3XLW3XgJ9R4dnYDk31zHpQebodqwcFsXzxV43yi7zPrJSMzaFnC3-uPQ7qH4xs9puX5RL4q4lSCUKAB82TMtCJ0xHwj4DYqsJQt9jUAz1IZbgbChq8d0EPe6y2yg38LZs50r6DD0f-VgF1fyup_1ORM1Ub6gRyWxIBZLyLFY6uLvzPhVQaBsDitK2ku-9ELv9frr0j-BPNNCBpNRQyJCbZwOcovJ3gn9w1TAJ4cXGxzJWRGVnJY0e_tJ5ZMwRrn4E6LwhKXK9N2lBU7-2RQxEU79sTRz12SV9v8Az51MlUHzjWZp8p2bttpkQKMeoIkIVu31t88Dn7MOZkwR5DQ8b1_tNV_3Nbqyc9OUUFTSrpfEJeU6VRrUilgvBq78lSov4q1tbKxp4OjhxdNv9NxfYCL11kTZVB_h","DataProtected":true} |
@ -0,0 +1 @@ | |||||
{"Version":1,"Id":"870FA7120249C21C30ADE458B07918C1","Created":"2022-11-30T09:59:58.1299062Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8BCNHSqqa79Fqw5EoSNNrOMZDqq5p81RZsuqUc9iuvF_3nm9RHjc2lAsu-T8AjFPxhbWkSNnQAU7JGWpNh_TU1XK9-Skieb68wO2lUFauCBnoNf7R7JEpuw7zdjkInQZFOF1hBzhL0qKszqhM8-2gz2Q2V5c8Ng0C_2CzHNqCsyhTHBD8NgXfzpxdym7V8HZSS4SkJ3QpKLH-VMXRFssCM7PXEimNWBN2yky4IW5_fRgYgRkkCaXCU43ayGxLyBe_J6DzZ1fefO7ZBne2b8HaVVQaAEQDTlsFUxNqDLcAO-k6q3FoL9ZW2gm82a8ra1mSJTbEeAacNK5oE22urnovVF1GPwTVnnXMi_9eloBqyTM5xtTQPYbVlOkZl5pgRjKOY_Mc62Ix9p9hoH-gCfDHw2Q9X34JT1ymzBdYqtgTmWoY548-ZkX6v3AEpkhmsMdKMyNSdz-SJfasTqKkp4JkqGR-gXW17HOtxHmaOPg0ZRuQ4Dx_YHuQr98UQPMUktYL_kF_76FevNyfy5eyN6eawHNGwhXzn0YdyCjaRV1LXfD-4LqqJBhExUSyCtG0Pq0qjZ0ySAuKi72-0532XNRJ1gh7CxlfRdeLDID4ffs1cgmfODFv1k1JElYihqqGwXMwx-SfgpM9eo4j6jqd0kdAgj3n4ZsJKQMCDOGv84Gfhnw7dxlIL0e-D0sKKc6f7KagNFJCbt0LZ492IWzIPIGp5lXQwhrDPECjdNvBv0jhC6yMzZQZlVQXX6MYue_qzXb45TK23j8HjlgYPCip9eqH1NBA1MWkHykA6ZkdZ29eCCMuO-m8zB_Z_7YdEdzGcViK25T2q8qo_0BMzAzdrEJ3rOYOly7wzsoESn6aKr2U1b4qh-fljOwQrYqoSzuiu5l4QY1qVG4ZR3VMjV2lo3LEbMayA6Sis38LQy_ulUKGdOi4KeyNSlbL44wN48gDM86kNPkTM2kVoJc6RRZ5zptnGyXgAsuknHaF5EX48Ihxt2AFU_pVgKGnrx4XkYkxMEwbInoL19WgaKZyLAXjwtUG3FSOPdWswUqbcDNoetfY21Y9yWZsZ0rUTUa3x9rLzRP-H9NGbK_Yf8j75lpdx2sFUvJyyxZ2KEoq74bS-Zg203Q3jhz_RVJi8M3R0oRQHh2I-wUimdAHQ9JQu-M-VAtVLG6DVrY4vyhdv2JxIPyLrRTJPVhMDEqKqUcbm0VJE8MJOb3C6hdFrrzcM8AAkxGhYHh2vlJu0MAa8y7U0lHm8mcrNqsjPahAFvVMwUPkxaXFSYwJDdnBfODquWHt6reE9GwQw-ZFvigPGmY3SWMZiEBK-2Zkz9NglOKEgnojT6xcAbdaeEDadvY-Qriej8rMBZ3F-2IHYNMM4J2MkbexgloQgS6UWx7Nx1ln-pI-uHCDUmwsb5bl05Jma4_DXMsIjzinc2u-CvpEUXe1KmDddh7NYSrUv5RjaZfxgzjYCW3gSynFRGNfy57PaX7R7VV9pn3IRMKsbdWKlA957M2tDqmgkzC2M8KZAJRFmw_tCVk-swjPjyFmXUgsdLs0oX8OSAh4JX8DWvK42wHerh4gbjX_N7mYWh38e4-jMvyZ5Zdqf-2phj21z9uZla1mCsncjDDPJy7sVCrlJasgPLgtCD5j0lxp3wdAjvx2I4q6CKNcP8-MapHABe2TIgMmEf2VOJv42yrO99q49OwkopbyhMf-0-drAOo7Tw7eKHH7S174LfEKewCMmj2BVaM_I-rTgU28Be8pfbl6jTmj-9WCvuBbBiCtvhpeOApuMbJkc43As4QV031W9DEQpB520DWUdAHjvNkUMo0QDBLm7Zx4xzeO3Ei35Xw8_w9OjZBCZw3kalIbZAL9MlsyfFXczbZXMZnzkccPTwTK2SgS5lq1Ic07YB_uqY52CwYlpjbRP3ygM2mVCLTB921NhXKh9-Md4oJ9ieaDUNyNPch2MLQluKmyIH0sumgBkyn1k17IM4qN3EXRSsUiqa-FBU-PFa1gVIf_dhdnLI_GOPGEYBmZYTrk8J8HK2-wIi5shGDOB-WjBaAWbymdMmPhifRKiDKdOmPVArGptSyrM_5UvHF189Jl0ABxEWcsW6DScgw72GtcXxhDQligdcjanZHNVGEnfwFN604v68G42IC7clOVAno7cgaapHXXYSadEqVzP_Q4d-d-obG0v44yMu3WSdfhmJ8GC-8jS3b16q5_vTfyim5VFmLdG0PX1L9--QYop1Rns-64a5Dz8T8MmyrtgRXBAMfXC84R7N8I7SgRbsSo2pgMzRafCOpYWqfUlNcPjqQXSPj8oEt0iz0DBq9ui1dK0JiiOGOO9uOdswQP2r_cVlF-QvTszcyy1hn6QHCdsfqojSi70jmyhpBa5Vgba7nyP_qbJCtYiZwk7RLWT-jZ47BwZzEK73UXoAYCc8NT5HsIW8KFuManq8Mau7BLNj6WhXJOKmbP0oD88LMj3TeEvZ3","DataProtected":true} |
@ -0,0 +1 @@ | |||||
{"Version":1,"Id":"B0B34AEB91E2638F61EB9A5543F3940F","Created":"2022-11-30T11:21:15.586844Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8Fzd_R2KZDdMrJulCrd4GFTnFtf9LeKAfSemG0-9e385u01WZOrLNU5PPCNSRvcQKMERaUvLDcAsYIsqdKPJ14QGDgSTlxggwrR50kCv3494z5uCvWOvWqYc3KKzhR-7e-mXViY4m2oYHpGaVCITKXYwCRYxO-P0mSdr6D7W-846cfdst47_rjIxxeMajL1suYSYe_sz_nz8ooKW8ZH_kl5M9cI0waB2XFviIP3f08pz_eyneKqy7tCc3ruSNiUzfUBAF7fBHDcyPrE2h4AKaXG1PT-I6JOFVj9lAgv_1ACSqTVEjJiRWfFIfft3sQ-chSv1yxy7b8GvhIsKKEn-OqXCnYr5pFW7VPagt7pf9IQdivU7I4m4zSyW0bjIWXdCL3sXQAChiDpvRCFqFfsiH4lWmI4X8Aq8Tz1_LkbicHW5-ZxeZpvgPlg6t77xqFIxFtixqdc1XxifVwJR5GwrFcbcI9VpnlBomkBSUFMslW9NxGwSztgnYatkQuiXTzdqFoOr5d4YGPEHe_Oc3h9yDyUxgBdJK30sSx1Dwki94CeQNBS7PisGbu5q60MbsyFQ2S6kBogReQdURLW326NkgOBJ27jHk3ccx24WfFuuDzquE7xetxvnX4AV0oKOudMClsOv4Wr1aMm98p4VimUQkAneFImQszK1-zSQfxeElamoYy-4h1P2V_A5siSmfKpDs1q2ja3C62lBr0YSx822O-QF4XI0id6GOZiCc_9v52B_teTlgMq6L54EjqDM-h6ERkPvoqNFL0fWz3ktOBPCQm56YrSyyaXNKG2TwL4AMLsaQ9us5SXnJ7UpiY_7ls4SVm_h3btoQcokOiO247mYo4-BRq4pCsFHNBsCReiKlJH8G8fwrujWcu_U7NdVWgmUgmcsIh9mfommuOdBdVQdP8CYZM8od1NBIIifIoj51WuZXtb9Wr4QVIddEjAQrYnexJAozF9TeBVfjhDkIIA2-b8yBGqNl5VrF4bo3b3LLm1rbq5vzfKN6pKzs2sDGple6kZn_t0Ym5fmf8iWXLzMnRyasEDuXX0-Vg9WxiWyXzHsUY-SZEfEXs_WfuzBZb9NP1JvrB_w33cUfgl9Sa6RD56O5F1YAR3WxiCiWGnlSnqpaiiyTb4NVr8N0qQl9xOz9KCSaN8H2WKgCML2DdjW5_O8Zea2EPpSf1NGB1G-Nm-9HWAn8xDAei5x8kzTIxka6Jb8GO6Rdffk7wwuYMoB4ZYuiuL1q-rqvaU5GffQWMjGz-yH-WCivVFCosvJgk9340K0bDl97CULs43LF3nQFcmuLQoW-fasNYk5ZAXYA8bjn9eYm-hWbsfA402c3iZKKZVcLAU-jPAr44Z1P9PbT-xwooGTKKnZuR55l24T3p4hDatpazlObRd27kPtSLgbXPkNKbru9N8--JUhoEeFGC8E01gCn9256wlpIO1hk9a-P87t7zJWdYkl9Uo8Poz_Hfm9Uaml55BWSghfXlB99hULUmpnPwyOVJnRhGOrnr6ys_2l6bCW-gZlWS7oevQ_BuJBcCXVuRYQPbVtH9ZZSKF-S-uczbtfzhJfH29tCNsHzAukVHFiIwHqzhLHNu1gX5COGzYdN57p7DXGkJXGJUipczm2ZAfI9g-Kgvy6MaXScuwaobPobL5AH5WG1CclEb8kg_08rnQsDfvNhmsAJE7dN1GjpA46fP2bJ09T4hfNdmW-IZbeQaDMGc_8RpoADdUY81swXUgrL56UxoCFxqifXtskbzxF5Z1ad4qRYNlqU_zMbxjSZTsSizNja4pmsx1O2NAhvN31bSoA8PWpLLz37Lq-craDpXW6-mIySm3NQWxTLx2hsk4i29-dD6AjUX2aDLEng4JLKLWKLcH_ulkujNP0oXdkXcGikLqFwUKG8_lEWV_ROlXUVlmwNf9WaohIX8caCK2CF_tyEkCxEjZIBzw5xazq7_zNbI66rXfqBHxDKqrdM3UJYBNFMk2LLyEGD4TetGu4RzBDt2r4rXatSXQh9a662LYlCXxlElP8xfH7BudBa9KgV0FQOdHQ4sm9chHiT9JPQX6bHwf_imYHmTyMV3cIN7f7bACLji-5hFf7if8ANbhY7zer9-rQWR9Wy-vYUyUaSWCWRXHQC2eXL0XklBNnv3vK1nFOcaeC5R0GGULfMnBluyClu2Z67OCsAzQqi-_aacxQVN91SNmAfUbVCrPgBTbQY_1u64fEMc8ZcL52xwT0qgCSvBPCNDEJ8YDVVd8L8eLX0hg7rV8SgCnAl5qG74BCdkmhCMLB-OiwGZxZ9s94yGdjX9jA4D70uOgn5t9OhpboX3OoRK40Xged6zCBEhUd9MilD9o-BtEPeAHloKqMrvrVDE4l9R739pjhgOfvXqGIytXD-bKlMZAY7dnJ4vu4GjVvhHfzxTV3zu_nAV8j3n41_knCe2RyCm9j74YZejnRC15ADqItTDmTre8_iS6hTOrVO9FP","DataProtected":true} |
@ -0,0 +1 @@ | |||||
{"Version":1,"Id":"CFE19FEED1112F0740C7CEAAE490834F","Created":"2022-11-29T07:38:58.7490696Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8PyEV1YRvftHv5uH-VZQ5M7m2ypM-2X7o3A9EvjXNQQsFD50DwqX3UR8e4cDEITQrBowxyPPRXo_bTgEnifJjx1kdzA3q0L-FPJqCOjh6AaRqUg_TlnhvbEgG1HYoBZTAwvhi6g3h86RFPU4juwhOWHcD1vzCgW1vfJ9VFXXq3F-FSfbpdykS5bLn5hUdg3zgNWh7zZXOIFxgNkelR7lGMfF6hquTQMbCRhtDg0-N8O8m7M0JaNsB6_H6v-Z2RlYzH0R0B0Ka2fH3uPnudPrd1RIP4BONzMorJpDFZP9TF0DY9sJhkcHEhKDZlRlwI_Z-e0z8WeW183Gg_HSV23wO4B5R8ldXRSioqWyUuQfoYplUanuNeccblBe7hxQw9fhsanHGgbiuBjuypXarZmlVlbFO2zPKl46Vj8YZTxopOFOi4ALcNlUzGgPG5lwPjDfXEyAp7_ipMIW84UVfwqnPZFE1FASpFx1BPif-huoAaXw2GF4HJ2QVqfxkr_Gn8isrzBOT3fqmeZ1n0HNOc8VxCSdScbBSKFMnxNSgPQSexHGSKnXpvXF0OL6R8oqHoi8TNfYFitGE_qED6wCjQ77qMXOUVXB18kW7nJYqph2u1BH1PykRWaWDlnh3H7fKWRhO0ejaTJWl7oSLllYzi99yL5JvudDCjI0QdfbMz7o3SrdrZbIKQcbTPPgl_cfNneP4QA6MSlJFbG3bc3q8IUPhISrZHO_s7SsoX0BiP-AHcA7M_WrkAQYLkTMsBuO3x6N-bWjtTouqzHY7-h5ZYHqvk7Mg-jlgJjFjgKRDOJFiDorbf-5nvxjl3eLV8rjX7ML9KqjD-ZUjMsDt79lpjCi6B2EFqbiGT35I67hC6a7l8gPjwLKB7Oc8h4YggwyS1eLCq3CgmfMqlIAeAqLL7nVwDPReW-pirHix92q2tSkKCuz5UV5jphmDAnRFQmICaIinoSsBtjb1RlonM-yklDO-jdR5r0ZEFb8ZCssSojhfqEi5mcK3xJsyzM_Inzg71zAHCUl5cuBhbWekBeHiqb2p5ar9guCK_h7zmga1ZZTqfWAGzxHrHkHb6XiK_wLlwBjJEoGhgHfi0UzUECddK2DVwEuTsXhNDJ8CgB2oWNhIHgnlNxL_JA6_bY8BnGtcOKnEuaKpR6rXEZit2Jcr-1-Q8YJ29tVNubphYm2ZMrDTGdAVE8ODftey6BIN_HmBELf6a7x-Cd5DPrW46iVqlOuBqmjxjZ4HHTS08HHGSD4_xj2vNaWl8rIbE4BbDq9V_qyP548KLNbGqSIed38DDcbsRMIuac8po95Z3q6NUf2JR0mCOEizdrszZOW9sGVlO-WReaTkpNN8MaKYoUy6k84IxwPPv-Tov0yMdTN-jp7H2HaTOGDvEb72swcW39huoAbXwjZdrpFQjTD9_cnNrVdclDGN_yix_i8-vQDQlQ9Ronpk2NvAdnrmEzQ6w7D7PqAe5c4YJfn_0z45ERm9zBwCMCbw8lP_Yi3nwTbqGtFqAoXTSZ8vR5_UxQLqxsJy6U6gK4Es68-hnD-YR5p3CzBXxSEgV7_qvW2gly5VCB-1yoD8dTVInogMhOzDZD71gmpJURgKXX9WN92yq-YB24E8koOXdbxTvSblIVTxwDqQ7lb81y68HmZVSRLk3zhxbkEkjI1kkB8uzlArbXljN7LnxyXHU81DDeufVK5_myoIMA5NpSwEKxAkmCoF7p642qBwo90k0q0AvJVmoy8xpCRiJyLDR8wYHFsed3T99jvvvV7GyA4SG5Tt-ST5kKFKvOOwT_WrOg-ao5cz6Br8_YL2xW5dB231R4ZxfJlmrJY9WcuTQMZrWnYOOSj_5u2I4sgcnp7nrRF1l2xnieiym5ef23a-rSMBZi_leN6BAma6N1M7tu1N8cEw7wd_6FV6hqrcKFPGNm4HE5M-pmoWvzjBTo25rh_4atG5uEmDgmhmY6qiZc1eso__Fz29uJ4792kkpdGyPAcV8igzPMdKts1KiO9ThrDRH25Knwxa1tOWwezkeId-OuZRwC4LhGuZseww5TxzQtyN8mMWbLDPCmWya56F-i0jag4i-r2Z26VEmhpSywbACetljEsaVDJF2ZdIrIFJqjYLa9zhcKbD9GZPHZt0HKilXDp8DomCQHHWsIapwCiG6WLGOhVc0-0VJ28dBqIowyCQScefePKIIk7Oa0T4UzPXcXiXYNQktHGs9VrF2ux4De-SbxDRdJy1IRSH8_NQ4SFtUelO8IG27B_8Ywbonn9yRsoojmuS-fXPuWSJMHDjPcSYwJqQyxjfPjGCLsnQWRZCNT5Ft6cfcSDRZWoCqKK2qLfqcGs-FR71HM0G6ipAuVI_2IyfalS-fXqTPCncyFsOqp4BRhhxLFu8UxTZ5-HQyKH3fX75mZCD21d9RObyWv8g2T0HtEBLEf2eR8I8zCUcOf98asCoT2CU4AtOhVwqrE2u1qNKupc5Nvw","DataProtected":true} |
@ -0,0 +1 @@ | |||||
{"AdditionalData":{},"Alg":"RS256","Crv":null,"D":"Pf7_o1LMYwcXEWHkZ54kP4ItOYu0lhOOn4vc88XYg9XmDR8DcPxyvRLWGIuV-jSioMP59HIN5yHeDrQbG3tY2lTUynRYHdwOFVQAzQ3LpGSF4eKfMOSDU05HkWv-w2E1XWogVFAafv2F4FHeYtqWAzjjcjlYy_n0GkCcjBvIneuVqWNZQ9-eBt28cApruVfTeoE01Liz-ww4T0CQ9ujJQf9zUxhYb6WWMQ1U-A0qGK9hmh00Ymf-rxS2HuZ9o8Bps9YEvy7rCBfy6nSMB8T2I2SNCHDdlR8Oi-j_l0VQyidnfnRY0_Lio8uSKDTiUL_vBKyCREAfCEHM6TCpdixR-Q","DP":"CR-Lyp15RyQV_TmxXq2EX8mntgdnT6ROePrkNlRCSXH-AAVJQ-24OlBH0qzehzMWI9Yk72bhqZdBIC1W8SGsTxf6T3xp-MwINJwHgYftX-D9RDQCYS5o6_gFW1775Of43Zv8tsjjwwkxUKHiwH8ImbEyqv2qVhdb_yMlgFwtXCc","DQ":"ulVv9t13HB8K8jKLomSoINMaFtNGN0LS6wY93JKtwGCjcvV5lwXRiHhq0AbDOrJggg-zZVedlbXpWRokMI-hLTHc9TguYEfiJ11DvgldTB5MUfMxHau0L8ofS5YAuTSEgg4EPIrWgvVEF1ljHwLQtDSBB3CvI6UHF7rXXy9cwIk","E":"AQAB","K":null,"KeyId":"F7383A0ED7CD960EF473FD2851803938","Kid":"F7383A0ED7CD960EF473FD2851803938","Kty":"RSA","N":"p3jTfCB0YsKpqJ6CNJi0tVmFBmoyI_D7QLLsbB-TCZ3-HIXDEr_k6zKb2GJ_QP7mncdSnYpJWSv7fWPfM0bL3A6NaMLF9MDjbfD5ti9irEW1dzBvIK0YjWmfks3eq6Mb2mM6PZtNEnoCqEzjgcRkDR1vtClEzUjs1E_i7TB-Y0J_aTYpLf-eN7yA1Obu8zMVRSSVBIwG5W5jljzA2nxk2u9qeDq8Sn0qgGwbX8cyYGQoVWBOPx7zap4cNcL6dHILjnlVrHqAUW9NVXtBWlVDP1Gvnm2zhCVJT_gW1twNhswyFULGVQH1ZWI0NukEqHG6LpN8Ti7Hx-K8MEEv_vQBYw","Oth":null,"P":"1XZw-ufz_e5fWroi8naLluW5Ow1f-Ems8oG1WYUJkc1Q4vZWklLRqlEsDH48gqk0dEgXLE9tz0dDTo03tAPJmR1InFpusyLp2XPPJ4XLhWFCMDDb0xr4oayV1XDOi3U9eKERtcASo1IDo8zDEuY8NHFdJlNKpfJ7P0JSB0hEwoc","Q":"yNg4tO9PVrwQ0CaouP0jniFouHAEfZMyJ0vVM1xPVim7jeKysOJi79lHO7OWNSABB3hY8dLcMSiKxS3BaU0Q2ns9Gw-NLR6eqT0BN9Nzh-a0sa0iuPUWCPgsWdrqjIf4FBX0Ra-6q7_LxuWCncehUAtoLFWMkcOrEj03Gwz1lUU","QI":"UWdYA2qY622GL97_tPvrgk1VWSqoO2a0-TlGu0gK51T_Z1hqLwzG9QpTujoxnRU7Js_QUe_9cIEO72N0qliEm6A-MClM-5LUA9XleyAosb1cblLjAPqT9gfGbRRAbB3Aj3YwsgASGUiSpGVfaUuXn-OnuLoidZQXw7w9xcPuxMo","Use":null,"X":null,"X5t":null,"X5tS256":null,"X5u":null,"Y":null,"KeySize":2048,"HasPrivateKey":true,"CryptoProviderFactory":{"CryptoProviderCache":{},"CustomCryptoProvider":null,"CacheSignatureProviders":true,"SignatureProviderObjectPoolCacheSize":16}} |
@ -0,0 +1 @@ | |||||
window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); |
@ -0,0 +1,6 @@ | |||||
window.addEventListener("load", function () { | |||||
var a = document.querySelector("a.PostLogoutRedirectUri"); | |||||
if (a) { | |||||
window.location = a.href; | |||||
} | |||||
}); |