Local Passwords validation
parent
39f98229eb
commit
2043dbfce6
@ -0,0 +1,12 @@
|
||||
|
||||
namespace Yavsc.Models.Relationship
|
||||
{
|
||||
public class StaticContact
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string EMail { get; set; }
|
||||
public PostalAddress? PostalAddress { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Yavsc.Attributes.Validation;
|
||||
|
||||
namespace Yavsc.ViewModels.Account
|
||||
{
|
||||
public class ForgotPasswordViewModel
|
||||
{
|
||||
[YaRequired]
|
||||
[YaStringLength(512)]
|
||||
[Required]
|
||||
[StringLength(512)]
|
||||
public string? LoginOrEmail { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,9 @@
|
||||
namespace Yavsc.ViewModels.Administration
|
||||
{
|
||||
public class RoleInfo
|
||||
{
|
||||
public RoleInfo ()
|
||||
{
|
||||
|
||||
}
|
||||
public RoleInfo ( string roleName, string roleId, string[] users)
|
||||
{
|
||||
Name = roleName; // role.Name;
|
||||
Id = roleId; // role.Id;
|
||||
Users = users ; // role.Users.Select(u => u.UserId).ToArray();
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string[] Users { get; set; }
|
||||
}
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int UserCount { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,265 @@
|
||||
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
using IdentityServer4.Events;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4.Validation;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using Yavsc.Models.Access;
|
||||
using Yavsc.Extensions;
|
||||
|
||||
namespace Yavsc.Controllers
|
||||
{
|
||||
/// <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 != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
RememberConsent = model.RememberConsent,
|
||||
ScopesValuesConsented = scopes.ToArray(),
|
||||
Description = model.Description
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
|
||||
}
|
||||
|
||||
if (grantedConsent != null)
|
||||
{
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.GrantConsentAsync(request, grantedConsent);
|
||||
|
||||
// indicate that's it ok to redirect back to authorization endpoint
|
||||
result.RedirectUri = model.ReturnUrl;
|
||||
result.Client = request.Client;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we need to redisplay the consent UI
|
||||
result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<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(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null));
|
||||
}
|
||||
vm.ApiScopes = apiScopes;
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = identity.Name,
|
||||
DisplayName = identity.DisplayName ?? identity.Name,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
|
||||
{
|
||||
var displayName = apiScope.DisplayName ?? apiScope.Name;
|
||||
if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter))
|
||||
{
|
||||
displayName += ":" + parsedScopeValue.ParsedParameter;
|
||||
}
|
||||
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = parsedScopeValue.RawValue,
|
||||
DisplayName = displayName,
|
||||
Description = apiScope.Description,
|
||||
Emphasize = apiScope.Emphasize,
|
||||
Required = apiScope.Required,
|
||||
Checked = check || apiScope.Required
|
||||
};
|
||||
}
|
||||
|
||||
private ScopeViewModel GetOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = ConsentOptions.OfflineAccessDisplayName,
|
||||
Description = ConsentOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,233 @@
|
||||
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4.Configuration;
|
||||
using IdentityServer4.Events;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Yavsc.Models.Access;
|
||||
|
||||
namespace Yavsc.Controllers
|
||||
{
|
||||
[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 != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
RememberConsent = model.RememberConsent,
|
||||
ScopesValuesConsented = scopes.ToArray(),
|
||||
Description = model.Description
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
|
||||
}
|
||||
|
||||
if (grantedConsent != null)
|
||||
{
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.HandleRequestAsync(model.UserCode, grantedConsent);
|
||||
|
||||
// indicate that's it ok to redirect back to authorization endpoint
|
||||
result.RedirectUri = model.ReturnUrl;
|
||||
result.Client = request.Client;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we need to redisplay the consent UI
|
||||
result.ViewModel = await BuildViewModelAsync(model.UserCode, model);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<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(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null));
|
||||
}
|
||||
vm.ApiScopes = apiScopes;
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = identity.Name,
|
||||
DisplayName = identity.DisplayName ?? identity.Name,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = parsedScopeValue.RawValue,
|
||||
// todo: use the parsed scope value in the display?
|
||||
DisplayName = apiScope.DisplayName ?? apiScope.Name,
|
||||
Description = apiScope.Description,
|
||||
Emphasize = apiScope.Emphasize,
|
||||
Required = apiScope.Required,
|
||||
Checked = check || apiScope.Required
|
||||
};
|
||||
}
|
||||
private ScopeViewModel GetOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = ConsentOptions.OfflineAccessDisplayName,
|
||||
Description = ConsentOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yavsc.Models;
|
||||
|
||||
namespace Yavsc.Controllers
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class DiagnosticsController : Controller
|
||||
{
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var localAddresses = new string[] { "127.0.0.1", "::1", HttpContext.Connection.LocalIpAddress.ToString() };
|
||||
if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString()))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var model = new DiagnosticsViewModel(await HttpContext.AuthenticateAsync());
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Yavsc.Models
|
||||
{
|
||||
public class DiagnosticsViewModel
|
||||
{
|
||||
public DiagnosticsViewModel(AuthenticateResult result)
|
||||
{
|
||||
AuthenticateResult = result;
|
||||
|
||||
if (result.Properties.Items.ContainsKey("client_list"))
|
||||
{
|
||||
var encoded = result.Properties.Items["client_list"];
|
||||
var bytes = Base64Url.Decode(encoded);
|
||||
var value = Encoding.UTF8.GetString(bytes);
|
||||
|
||||
Clients = JsonConvert.DeserializeObject<string[]>(value);
|
||||
}
|
||||
}
|
||||
|
||||
public AuthenticateResult AuthenticateResult { get; }
|
||||
public IEnumerable<string> Clients { get; } = new List<string>();
|
||||
}
|
||||
}
|
||||
@ -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 Yavsc.Models.Access
|
||||
{
|
||||
public class LogoutViewModel : LogoutInputModel
|
||||
{
|
||||
public bool ShowLogoutPrompt { get; set; } = true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
using IdentityServer4.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yavsc.Models.Access;
|
||||
|
||||
namespace Yavsc.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 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 Yavsc.Models.Access
|
||||
{
|
||||
public class AccountOptions
|
||||
{
|
||||
public static bool AllowLocalLogin = true;
|
||||
public static bool AllowRememberLogin = true;
|
||||
public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);
|
||||
|
||||
public static bool ShowLogoutPrompt = true;
|
||||
public static bool AutomaticRedirectAfterSignOut = false;
|
||||
|
||||
public static string InvalidCredentialsErrorMessage = "Invalid username or password";
|
||||
}
|
||||
}
|
||||
@ -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 Yavsc.Models.Access
|
||||
{
|
||||
public class DeviceAuthorizationInputModel : ConsentInputModel
|
||||
{
|
||||
public string UserCode { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
namespace Yavsc.Models.Access
|
||||
{
|
||||
|
||||
public class DeviceAuthorizationViewModel : ConsentViewModel
|
||||
{
|
||||
public string UserCode { get; set; }
|
||||
public bool ConfirmUserCode { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
namespace Yavsc.Models.Access
|
||||
{
|
||||
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,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 Yavsc.Models.Access
|
||||
{
|
||||
public class GrantsViewModel
|
||||
{
|
||||
public IEnumerable<GrantViewModel> Grants { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
namespace Yavsc.Models.Access
|
||||
{
|
||||
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,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 Yavsc.Models.Access
|
||||
{
|
||||
public class LogoutInputModel
|
||||
{
|
||||
public string LogoutId { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Yavsc.Models.Access
|
||||
{
|
||||
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,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 Yavsc.Models.Access
|
||||
{
|
||||
public class RedirectViewModel
|
||||
{
|
||||
public string RedirectUrl { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Account.AccessDeniedModel
|
||||
@{
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>Access Denied</h1>
|
||||
<p>You do not have permission to access that resource.</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,13 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Account;
|
||||
|
||||
public class AccessDeniedModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.ForgotPassword.Index
|
||||
|
||||
<h2>Forgot your password</h2>
|
||||
|
||||
<form asp-controller="Account" asp-action="ForgotPassword" method="post" class="form-horizontal" role="form">
|
||||
<h4>Enter your user name or e-mail.</h4>
|
||||
<hr />
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label">LoginOrEmail</label>
|
||||
<div class="col-md-10">
|
||||
<input asp-for="Input.LoginOrEmail" class="form-control" />
|
||||
<span asp-validation-for="Input.LoginOrEmail" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
|
||||
using System.Web;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Google.Apis.Calendar.v3.Data;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Yavsc.Helpers;
|
||||
using Yavsc.Interface;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.ViewModels.Account;
|
||||
|
||||
namespace Yavsc.Pages.ForgotPassword;
|
||||
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IAuthenticationSchemeProvider _schemeProvider;
|
||||
private readonly IIdentityProviderStore _identityProviderStore;
|
||||
private readonly IEventService _events;
|
||||
private readonly ApplicationDbContext _dbContext;
|
||||
private readonly ILogger<Index> _logger;
|
||||
private readonly SiteSettings _siteSettings;
|
||||
private readonly ITrueEmailSender _emailSender;
|
||||
private readonly IStringLocalizer<YavscLocalization> _localizer;
|
||||
|
||||
[BindProperty]
|
||||
public ForgotPasswordViewModel Input { get; set; }
|
||||
|
||||
public Index(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IAuthenticationSchemeProvider schemeProvider,
|
||||
IIdentityProviderStore identityProviderStore,
|
||||
IEventService events,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
ApplicationDbContext applicationDbContext,
|
||||
ILoggerFactory loggerFactory,
|
||||
ITrueEmailSender emailSender,
|
||||
IStringLocalizer<Yavsc.YavscLocalization> localizer,
|
||||
IOptions<SiteSettings> siteSettings
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_interaction = interaction;
|
||||
_schemeProvider = schemeProvider;
|
||||
_identityProviderStore = identityProviderStore;
|
||||
_events = events;
|
||||
_dbContext = applicationDbContext;
|
||||
_logger = loggerFactory.CreateLogger<Index>();
|
||||
_siteSettings = siteSettings.Value;
|
||||
_emailSender = emailSender;
|
||||
_localizer = localizer;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
ApplicationUser user;
|
||||
// Username should not contain any '@'
|
||||
if (Input.LoginOrEmail.Contains('@'))
|
||||
{
|
||||
user = await _userManager.FindByEmailAsync(Input.LoginOrEmail);
|
||||
}
|
||||
else
|
||||
{
|
||||
user = await _dbContext.Users.FirstOrDefaultAsync(u => u.UserName == Input.LoginOrEmail);
|
||||
}
|
||||
|
||||
// Don't reveal that the user does not exist or is not confirmed
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning($"ForgotPassword: Email or User name {Input.LoginOrEmail} not found");
|
||||
return Redirect("ForgotPasswordConfirmation");
|
||||
}
|
||||
// We cannot require the email to be confimed,
|
||||
// or a lot of non confirmed email never be able to finalyze
|
||||
// registration.
|
||||
if (!await _userManager.IsEmailConfirmedAsync(user))
|
||||
{
|
||||
_logger.LogWarning($"ForgotPassword: Email {Input.LoginOrEmail} not confirmed");
|
||||
// don't break this recovery process here ...
|
||||
// or else e-mail won't ever be validated, since user lost his password.
|
||||
// don't return View("ForgotPasswordConfirmation");
|
||||
}
|
||||
|
||||
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713
|
||||
// Send an email with this link
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var callbackUrl = _siteSettings.Audience + "/Account/ResetPassword/" +
|
||||
HttpUtility.UrlEncode(user.Id) + "/" + HttpUtility.UrlEncode(code);
|
||||
|
||||
var sent = await _emailSender.SendEmailAsync(user.UserName, user.Email, _localizer["Reset Password"],
|
||||
_localizer["Please reset your password by "] + " <a href=\"" +
|
||||
callbackUrl + "\" >following this link</a>");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
@ -1,136 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Login.Index
|
||||
|
||||
<div class="login-page">
|
||||
<div class="lead">
|
||||
<h1>Login</h1>
|
||||
<p>Choose how to login</p>
|
||||
</div>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
|
||||
@if (Model.View.EnableLocalLogin)
|
||||
{
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Local Account</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form asp-page="/Account/Login/Index">
|
||||
<input type="hidden" asp-for="Input.ReturnUrl" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Username"></label>
|
||||
<input class="form-control" placeholder="Username" asp-for="Input.Username" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Password"></label>
|
||||
<input type="password" class="form-control" placeholder="Password" asp-for="Input.Password" autocomplete="off">
|
||||
</div>
|
||||
|
||||
@if (Model.View.AllowRememberLogin)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" asp-for="Input.RememberLogin">
|
||||
<label class="form-check-label" asp-for="Input.RememberLogin">
|
||||
Remember My Login
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button class="btn btn-primary" name="Input.Button" value="login">Login</button>
|
||||
<button class="btn btn-secondary" name="Input.Button" value="cancel">Cancel</button>
|
||||
</form>
|
||||
|
||||
<a asp-page="ForgotPassword">Forgot Password</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.VisibleExternalProviders.Any())
|
||||
{
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>External Account</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-inline">
|
||||
@foreach (var provider in Model.View.VisibleExternalProviders)
|
||||
{
|
||||
<li class="list-inline-item">
|
||||
<a class="btn btn-secondary"
|
||||
asp-page="/ExternalLogin/Challenge"
|
||||
asp-route-scheme="@provider.AuthenticationScheme"
|
||||
asp-route-returnUrl="@Model.Input.ReturnUrl">
|
||||
@provider.DisplayName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!Model.View.EnableLocalLogin && !Model.View.VisibleExternalProviders.Any())
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>Invalid login request</strong>
|
||||
There are no login schemes configured for this request.
|
||||
</div>
|
||||
}
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark" aria-label="First navbar example">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Never expand</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarsExample01" aria-controls="navbarsExample01" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarsExample01">
|
||||
<ul class="navbar-nav me-auto mb-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdown01">
|
||||
<li><a class="dropdown-item" href="#">Action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Another action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<form>
|
||||
<input class="form-control" type="text" placeholder="Search" aria-label="Search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenu2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Dropdown
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenu2">
|
||||
<button class="dropdown-item" type="button">Action</button>
|
||||
<button class="dropdown-item" type="button">Another action</button>
|
||||
<button class="dropdown-item" type="button">Something else here</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -1,216 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Yavsc.Models;
|
||||
|
||||
namespace Yavsc.Pages.Login;
|
||||
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
private readonly IAuthenticationSchemeProvider _schemeProvider;
|
||||
private readonly IIdentityProviderStore _identityProviderStore;
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public Index(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IAuthenticationSchemeProvider schemeProvider,
|
||||
IIdentityProviderStore identityProviderStore,
|
||||
IEventService events,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_interaction = interaction;
|
||||
_schemeProvider = schemeProvider;
|
||||
_identityProviderStore = identityProviderStore;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet(string? returnUrl)
|
||||
{
|
||||
await BuildModelAsync(returnUrl);
|
||||
|
||||
if (View.IsExternalLoginOnly)
|
||||
{
|
||||
// we only have one option for logging in and it's an external provider
|
||||
return RedirectToPage("/ExternalLogin/Challenge", new { scheme = View.ExternalLoginScheme, returnUrl });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
// check if we are in the context of an authorization request
|
||||
var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);
|
||||
|
||||
// the user clicked the "cancel" button
|
||||
if (Input.Button != "login")
|
||||
{
|
||||
if (context != null)
|
||||
{
|
||||
// This "can't happen", because if the ReturnUrl was null, then the context would be null
|
||||
ArgumentNullException.ThrowIfNull(Input.ReturnUrl, nameof(Input.ReturnUrl));
|
||||
|
||||
// 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(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
return Redirect(Input.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(Input.Username!, Input.Password!, Input.RememberLogin, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var user = await _userManager.FindByNameAsync(Input.Username!);
|
||||
await _events.RaiseAsync(new UserLoginSuccessEvent(user!.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId));
|
||||
Telemetry.Metrics.UserLogin(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider);
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
// This "can't happen", because if the ReturnUrl was null, then the context would be null
|
||||
ArgumentNullException.ThrowIfNull(Input.ReturnUrl, nameof(Input.ReturnUrl));
|
||||
|
||||
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(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
return Redirect(Input.ReturnUrl ?? "~/");
|
||||
}
|
||||
|
||||
// request for a local page
|
||||
if (Url.IsLocalUrl(Input.ReturnUrl))
|
||||
{
|
||||
return Redirect(Input.ReturnUrl);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(Input.ReturnUrl))
|
||||
{
|
||||
return Redirect("~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new ArgumentException("invalid return URL");
|
||||
}
|
||||
}
|
||||
|
||||
const string error = "invalid credentials";
|
||||
await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, error, clientId:context?.Client.ClientId));
|
||||
Telemetry.Metrics.UserLoginFailure(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider, error);
|
||||
ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
|
||||
}
|
||||
|
||||
// something went wrong, show form with error
|
||||
await BuildModelAsync(Input.ReturnUrl);
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task BuildModelAsync(string? returnUrl)
|
||||
{
|
||||
Input = new InputModel
|
||||
{
|
||||
ReturnUrl = returnUrl
|
||||
};
|
||||
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
|
||||
{
|
||||
var local = context.IdP == Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider;
|
||||
|
||||
// this is meant to short circuit the UI and only trigger the one external IdP
|
||||
View = new ViewModel
|
||||
{
|
||||
EnableLocalLogin = local,
|
||||
};
|
||||
|
||||
Input.Username = context.LoginHint;
|
||||
|
||||
if (!local)
|
||||
{
|
||||
View.ExternalProviders = new[] { new ViewModel.ExternalProvider ( authenticationScheme: context.IdP ) };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var schemes = await _schemeProvider.GetAllSchemesAsync();
|
||||
|
||||
var providers = schemes
|
||||
.Where(x => x.DisplayName != null)
|
||||
.Select(x => new ViewModel.ExternalProvider
|
||||
(
|
||||
authenticationScheme: x.Name,
|
||||
displayName: x.DisplayName ?? x.Name
|
||||
)).ToList();
|
||||
|
||||
var dynamicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync())
|
||||
.Where(x => x.Enabled)
|
||||
.Select(x => new ViewModel.ExternalProvider
|
||||
(
|
||||
authenticationScheme: x.Scheme,
|
||||
displayName: x.DisplayName ?? x.Scheme
|
||||
));
|
||||
providers.AddRange(dynamicSchemes);
|
||||
|
||||
|
||||
var allowLocal = true;
|
||||
var client = context?.Client;
|
||||
if (client != null)
|
||||
{
|
||||
allowLocal = client.EnableLocalLogin;
|
||||
if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Count != 0)
|
||||
{
|
||||
providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
View = new ViewModel
|
||||
{
|
||||
AllowRememberLogin = LoginOptions.AllowRememberLogin,
|
||||
EnableLocalLogin = allowLocal && LoginOptions.AllowLocalLogin,
|
||||
ExternalProviders = providers.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Yavsc.Pages.Login;
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required]
|
||||
public string? Username { get; set; }
|
||||
[Required]
|
||||
public string? Password { get; set; }
|
||||
public bool RememberLogin { get; set; }
|
||||
public string? ReturnUrl { get; set; }
|
||||
public string? Button { get; set; }
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Login;
|
||||
|
||||
public static class LoginOptions
|
||||
{
|
||||
public static readonly bool AllowLocalLogin = true;
|
||||
public static readonly bool AllowRememberLogin = true;
|
||||
public static readonly TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);
|
||||
public static readonly string InvalidCredentialsErrorMessage = "Invalid username or password";
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Login;
|
||||
|
||||
public class ViewModel
|
||||
{
|
||||
public bool AllowRememberLogin { get; set; } = true;
|
||||
public bool EnableLocalLogin { get; set; } = true;
|
||||
|
||||
public IEnumerable<ViewModel.ExternalProvider> ExternalProviders { get; set; } = Enumerable.Empty<ExternalProvider>();
|
||||
public IEnumerable<ViewModel.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;
|
||||
|
||||
public class ExternalProvider
|
||||
{
|
||||
public ExternalProvider(string authenticationScheme, string? displayName = null)
|
||||
{
|
||||
AuthenticationScheme = authenticationScheme;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
public string AuthenticationScheme { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Services;
|
||||
using IdentityModel;
|
||||
using Yavsc.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Logout;
|
||||
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
|
||||
[BindProperty]
|
||||
public string? LogoutId { get; set; }
|
||||
|
||||
public Index(SignInManager<ApplicationUser> signInManager, IIdentityServerInteractionService interaction, IEventService events)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_interaction = interaction;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet(string? logoutId)
|
||||
{
|
||||
LogoutId = logoutId;
|
||||
|
||||
var showLogoutPrompt = LogoutOptions.ShowLogoutPrompt;
|
||||
|
||||
if (User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
// if the user is not authenticated, then just show logged out page
|
||||
showLogoutPrompt = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var context = await _interaction.GetLogoutContextAsync(LogoutId);
|
||||
if (context?.ShowSignoutPrompt == false)
|
||||
{
|
||||
// it's safe to automatically sign-out
|
||||
showLogoutPrompt = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (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 OnPost();
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
// if there's no current logout context, we need to create one
|
||||
// this captures necessary info from the current logged in user
|
||||
// this can still return null if there is no context needed
|
||||
LogoutId ??= await _interaction.CreateLogoutContextAsync();
|
||||
|
||||
// delete local authentication cookie
|
||||
await _signInManager.SignOutAsync();
|
||||
|
||||
// see if we need to trigger federated logout
|
||||
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
|
||||
|
||||
// raise the logout event
|
||||
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
|
||||
Telemetry.Metrics.UserLogout(idp);
|
||||
|
||||
// if it's a local login we can ignore this workflow
|
||||
if (idp != null && idp != Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider)
|
||||
{
|
||||
// we need to see if the provider supports external logout
|
||||
if (await HttpContext.GetSchemeSupportsSignOutAsync(idp))
|
||||
{
|
||||
// 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.
|
||||
var url = Url.Page("/Account/Logout/Loggedout", new { logoutId = LogoutId });
|
||||
|
||||
// this triggers a redirect to the external provider for sign-out
|
||||
return SignOut(new AuthenticationProperties { RedirectUri = url }, idp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToPage("/Account/Logout/LoggedOut", new { logoutId = LogoutId });
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Logout.LoggedOut
|
||||
|
||||
<div class="logged-out-page">
|
||||
<h1>
|
||||
Logout
|
||||
<small>You are now logged out</small>
|
||||
</h1>
|
||||
|
||||
@if (Model.View.PostLogoutRedirectUri != null)
|
||||
{
|
||||
<div>
|
||||
Click <a class="PostLogoutRedirectUri" href="@Model.View.PostLogoutRedirectUri">here</a> to return to the
|
||||
<span>@Model.View.ClientName</span> application.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.SignOutIframeUrl != null)
|
||||
{
|
||||
<iframe width="0" height="0" class="signout" src="@Model.View.SignOutIframeUrl"></iframe>
|
||||
}
|
||||
</div>
|
||||
|
||||
@section scripts
|
||||
{
|
||||
@if (Model.View.AutomaticRedirectAfterSignOut)
|
||||
{
|
||||
<script src="~/js/signout-redirect.js"></script>
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Logout;
|
||||
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class LoggedOut : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interactionService;
|
||||
|
||||
public LoggedOutViewModel View { get; set; } = default!;
|
||||
|
||||
public LoggedOut(IIdentityServerInteractionService interactionService)
|
||||
{
|
||||
_interactionService = interactionService;
|
||||
}
|
||||
|
||||
public async Task OnGet(string? logoutId)
|
||||
{
|
||||
// get context information (client name, post logout redirect URI and iframe for federated signout)
|
||||
var logout = await _interactionService.GetLogoutContextAsync(logoutId);
|
||||
|
||||
View = new LoggedOutViewModel
|
||||
{
|
||||
AutomaticRedirectAfterSignOut = LogoutOptions.AutomaticRedirectAfterSignOut,
|
||||
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
|
||||
ClientName = String.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
|
||||
SignOutIframeUrl = logout?.SignOutIFrameUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Logout;
|
||||
|
||||
public class LoggedOutViewModel
|
||||
{
|
||||
public string? PostLogoutRedirectUri { get; set; }
|
||||
public string? ClientName { get; set; }
|
||||
public string? SignOutIframeUrl { get; set; }
|
||||
public bool AutomaticRedirectAfterSignOut { get; set; }
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
namespace Yavsc.Pages.Logout;
|
||||
|
||||
public static class LogoutOptions
|
||||
{
|
||||
public static readonly bool ShowLogoutPrompt = true;
|
||||
public static readonly bool AutomaticRedirectAfterSignOut = false;
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
@model Activity
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Create";
|
||||
}
|
||||
|
||||
<h2>Create</h2>
|
||||
|
||||
<form asp-action="Create">
|
||||
<div class="form-horizontal">
|
||||
<h4>Activity</h4>
|
||||
<hr />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Code" class="col-md-2 control-label">Code</label>
|
||||
<div class="col-md-10">
|
||||
<input asp-for="Code" class="form-control" />
|
||||
<span asp-validation-for="Code" class="text-danger" ></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Name" class="col-md-2 control-label">
|
||||
Name"]</label>
|
||||
<div class="col-md-10">
|
||||
<input asp-for="Name" class="form-control" />
|
||||
<span asp-validation-for="Name" class="text-danger" ></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Parent" class="col-md-2 control-label">
|
||||
Parent"]</label>
|
||||
<div class="col-md-10">
|
||||
<select asp-for="ParentCode" asp-items=@ViewBag.ParentCode class="form-control" >
|
||||
</select>
|
||||
<span asp-validation-for="ParentCode" class="text-danger" ></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Description" class="col-md-2 control-label">
|
||||
Description"]</label>
|
||||
<div class="col-md-10">
|
||||
<input asp-for="Description" class="form-control" />
|
||||
<span asp-validation-for="Description" class="text-danger" ></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Photo" class="col-md-2 control-label">
|
||||
Photo"]
|
||||
</label>
|
||||
<div class="col-md-10">
|
||||
<input asp-for="Photo" class="form-control" />
|
||||
<span asp-validation-for="Photo" class="text-danger" ></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SettingsClassName" class="col-md-2 control-label">
|
||||
SettingsClass"]
|
||||
</label>
|
||||
<div class="col-md-10">
|
||||
<select asp-for="SettingsClassName" class="form-control" asp-items="@ViewBag.SettingsClassName">
|
||||
</select>
|
||||
<span asp-validation-for="SettingsClassName" class="text-danger" ></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<input type="submit" value="Create"]" class="btn btn-default" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<a asp-action="Index">Back to List</a>
|
||||
</div>
|
||||
|
||||
|
||||
@section Scripts {
|
||||
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
@model Activity
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Delete";
|
||||
}
|
||||
|
||||
<h2>Delete</h2>
|
||||
|
||||
<h3>AreYourSureYouWantToDeleteThis</h3>
|
||||
<div>
|
||||
<h4>Activity</h4>
|
||||
<hr />
|
||||
<dl class="dl-horizontal">
|
||||
<dt>
|
||||
@Html.DisplayNameFor(model => model.Description)
|
||||
</dt>
|
||||
<dd>
|
||||
@Html.DisplayFor(model => model.Description)
|
||||
</dd>
|
||||
<dt>
|
||||
@Html.DisplayNameFor(model => model.Name)
|
||||
</dt>
|
||||
<dd>
|
||||
@Html.DisplayFor(model => model.Name)
|
||||
</dd>
|
||||
<dt>
|
||||
@Html.DisplayNameFor(model => model.Photo)
|
||||
</dt>
|
||||
<dd>
|
||||
@Html.DisplayFor(model => model.Photo)
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<form asp-action="Delete">
|
||||
<div class="form-actions no-color">
|
||||
<input type="submit" value="Delete" class="btn btn-danger" />
|
||||
<a asp-action="Index" class="btn btn-link">Back to List</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -1,37 +0,0 @@
|
||||
@model Activity
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Details";
|
||||
}
|
||||
|
||||
<h2>Details</h2>
|
||||
|
||||
<div>
|
||||
<h4>Activity</h4>
|
||||
<hr />
|
||||
<dl class="dl-horizontal">
|
||||
<dt>
|
||||
@Html.DisplayNameFor(model => model.Description)
|
||||
</dt>
|
||||
<dd>
|
||||
@Html.DisplayFor(model => model.Description)
|
||||
</dd>
|
||||
<dt>
|
||||
@Html.DisplayNameFor(model => model.Name)
|
||||
</dt>
|
||||
<dd>
|
||||
@Html.DisplayFor(model => model.Name)
|
||||
</dd>
|
||||
<dt>
|
||||
@Html.DisplayNameFor(model => model.Photo)
|
||||
</dt>
|
||||
<dd>
|
||||
@Html.DisplayFor(model => model.Photo)
|
||||
<img src="@Model.Photo" style="max-width:100%"/>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@Model.Code">Edit</a> |
|
||||
<a asp-action="Index">Back to List</a>
|
||||
</p>
|
||||
@ -1,86 +0,0 @@
|
||||
@model Activity
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit";
|
||||
}
|
||||
|
||||
<h2>Edit</h2>
|
||||
|
||||
<form asp-action="Edit">
|
||||
<div class="form-horizontal">
|
||||
<h4>Activity @Model.Code</h4>
|
||||
<hr />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<input type="hidden" asp-for="Code" />
|
||||
<div class="form-group">
|
||||
<label asp-for="Name" class="col-md-2 control-label"></label>
|
||||
<div class="col-md-10">
|
||||
<input asp-for="Name" class="form-control" />
|
||||
<span asp-validation-for="Name" class="text-danger" ></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Parent" class="col-md-2 control-label"></label>
|
||||
<div class="col-md-10">
|
||||
<select asp-for="ParentCode" asp-items=@ViewBag.ParentCode class="form-control" >
|
||||
</select>
|
||||
<span asp-validation-for="ParentCode" class="text-danger" >
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Description" class="col-md-2 control-label"></label>
|
||||
<div class="col-md-10">
|
||||
<input asp-for="Description" class="form-control" />
|
||||
<span asp-validation-for="Description" class="text-danger" >
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Photo" class="col-md-2 control-label"></label>
|
||||
<div class="col-md-10">
|
||||
<input asp-for="Photo" class="form-control" />
|
||||
<span asp-validation-for="Photo" class="text-danger">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SettingsClassName" class="col-md-2 control-label"></label>
|
||||
<div class="col-md-10">
|
||||
<select asp-for="SettingsClassName" class="form-control" asp-items="@ViewBag.SettingsClassName">
|
||||
</select>
|
||||
<span asp-validation-for="SettingsClassName" class="text-danger" ></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Hidden" class="col-md-2 control-label"></label>
|
||||
<div class="col-md-10">
|
||||
<input asp-for="Hidden" class="form-control" />
|
||||
<span asp-validation-for="Hidden" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rate" class="col-md-2 control-label"></label>
|
||||
<div class="col-md-10">
|
||||
<input asp-for="Rate" class="form-control" />
|
||||
<span asp-validation-for="Rate" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<input type="submit" value="Save" class="btn btn-default" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<a asp-action="Index">Back to List</a>
|
||||
</div>
|
||||
|
||||
|
||||
@section Scripts {
|
||||
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
@model IEnumerable<Activity>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Index";
|
||||
}
|
||||
@section scripts {
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$("tr[data-hidden=True]").css('background-color','grey')
|
||||
})
|
||||
</script>
|
||||
}
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
|
||||
<p>
|
||||
<a asp-action="Create">Create New</a>
|
||||
</p>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Name)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Code)
|
||||
</th>
|
||||
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Description)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Photo)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Parent)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.SettingsClassName)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Children)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Rate)
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
@foreach (var item in Model) {
|
||||
<tr data-hidden="@item.Hidden">
|
||||
<td>
|
||||
<a name="@item.Code" class="btn btn-link"></a> @Html.DisplayFor(modelItem => item.Name)
|
||||
</td>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.Code)
|
||||
</td>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.Description)
|
||||
</td>
|
||||
<td>@if (item.Photo!=null) {
|
||||
<img src="@item.Photo" style="max-height: 4em;" />
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (item.Parent!=null) {
|
||||
<text>
|
||||
<a href="#@item.ParentCode">@Html.DisplayFor(modelItem => item.Parent)</a>
|
||||
</text>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (item.SettingsClassName!=null) {
|
||||
<text>
|
||||
@item.SettingsClassName
|
||||
</text>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.Children)
|
||||
</td>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.Rate)
|
||||
</td>
|
||||
<td>
|
||||
<a asp-action="Edit" asp-route-id="@item.Code" class="btn btn-default">Edit</a>
|
||||
<a asp-action="Details" asp-route-id="@item.Code" class="btn btn-success">Details</a>
|
||||
<a asp-action="Delete" asp-route-id="@item.Code" class="btn btn-danger">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
@ -1,48 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Ciba.AllModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="ciba-page">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Pending Backchannel Login Requests</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (Model.Logins.Any())
|
||||
{
|
||||
<table class="table table-bordered table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Client Id</th>
|
||||
<th>Binding Message</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var login in Model.Logins)
|
||||
{
|
||||
<tr>
|
||||
<td>@login.InternalId</td>
|
||||
<td>@login.Client.ClientId</td>
|
||||
<td>@login.BindingMessage</td>
|
||||
<td>
|
||||
<a asp-page="Consent" asp-route-id="@login.InternalId" class="btn btn-primary">Process</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>No Pending Login Requests</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,28 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Ciba;
|
||||
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class AllModel : PageModel
|
||||
{
|
||||
public IEnumerable<BackchannelUserLoginRequest> Logins { get; set; } = default!;
|
||||
|
||||
private readonly IBackchannelAuthenticationInteractionService _backchannelAuthenticationInteraction;
|
||||
|
||||
public AllModel(IBackchannelAuthenticationInteractionService backchannelAuthenticationInteractionService)
|
||||
{
|
||||
_backchannelAuthenticationInteraction = backchannelAuthenticationInteractionService;
|
||||
}
|
||||
|
||||
public async Task OnGet()
|
||||
{
|
||||
Logins = await _backchannelAuthenticationInteraction.GetPendingLoginRequestsForCurrentUserAsync();
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Ciba.Consent
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="ciba-consent">
|
||||
<div class="lead">
|
||||
@if (Model.View.ClientLogoUrl != null)
|
||||
{
|
||||
<div class="client-logo"><img src="@Model.View.ClientLogoUrl"></div>
|
||||
}
|
||||
<h1>
|
||||
@Model.View.ClientName
|
||||
<small class="text-muted">is requesting your permission</small>
|
||||
</h1>
|
||||
|
||||
<h3>Verify that this identifier matches what the client is displaying: <em class="text-primary">@Model.View.BindingMessage</em></h3>
|
||||
|
||||
<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-page="/Ciba/Consent">
|
||||
<input type="hidden" asp-for="Input.Id" />
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
@if (Model.View.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.View.IdentityScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="@scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.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.View.ApiScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
Description
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input class="form-control" placeholder="Description or name of device" asp-for="Input.Description" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<button name="Input.button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
|
||||
<button name="Input.button" value="no" class="btn btn-secondary">No, Do Not Allow</button>
|
||||
</div>
|
||||
<div class="col-sm-4 col-lg-auto">
|
||||
@if (Model.View.ClientUrl != null)
|
||||
{
|
||||
<a class="btn btn-outline-info" href="@Model.View.ClientUrl">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
<strong>@Model.View.ClientName</strong>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -1,228 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Ciba;
|
||||
|
||||
[Authorize]
|
||||
[SecurityHeaders]
|
||||
public class Consent : PageModel
|
||||
{
|
||||
private readonly IBackchannelAuthenticationInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
private readonly ILogger<Consent> _logger;
|
||||
|
||||
public Consent(
|
||||
IBackchannelAuthenticationInteractionService interaction,
|
||||
IEventService events,
|
||||
ILogger<Consent> logger)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_events = events;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGet(string? id)
|
||||
{
|
||||
if (!await SetViewModelAsync(id))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
Id = id
|
||||
};
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
// validate return url is still valid
|
||||
var request = await _interaction.GetLoginRequestByInternalIdAsync(Input.Id ?? throw new ArgumentNullException(nameof(Input.Id)));
|
||||
if (request == null || request.Subject.GetSubjectId() != User.GetSubjectId())
|
||||
{
|
||||
_logger.InvalidId(Input.Id);
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
|
||||
CompleteBackchannelLoginRequest? result = null;
|
||||
|
||||
// user clicked 'no' - send back the standard 'access_denied' response
|
||||
if (Input.Button == "no")
|
||||
{
|
||||
result = new CompleteBackchannelLoginRequest(Input.Id);
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
|
||||
}
|
||||
// user clicked 'yes' - validate the data
|
||||
else if (Input.Button == "yes")
|
||||
{
|
||||
// if the user consented to some scope, build the response model
|
||||
if (Input.ScopesConsented.Any())
|
||||
{
|
||||
var scopes = Input.ScopesConsented;
|
||||
if (ConsentOptions.EnableOfflineAccess == false)
|
||||
{
|
||||
scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
result = new CompleteBackchannelLoginRequest(Input.Id)
|
||||
{
|
||||
ScopesValuesConsented = scopes.ToArray(),
|
||||
Description = Input.Description
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, result.ScopesValuesConsented, false));
|
||||
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, result.ScopesValuesConsented, false);
|
||||
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(result.ScopesValuesConsented);
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage);
|
||||
}
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.CompleteLoginRequestAsync(result);
|
||||
|
||||
return RedirectToPage("/Ciba/All");
|
||||
}
|
||||
|
||||
// we need to redisplay the consent UI
|
||||
if (!await SetViewModelAsync(Input.Id))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task<bool> SetViewModelAsync(string? id)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
|
||||
var request = await _interaction.GetLoginRequestByInternalIdAsync(id);
|
||||
if (request != null && request.Subject.GetSubjectId() == User.GetSubjectId())
|
||||
{
|
||||
View = CreateConsentViewModel(request);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.NoMatchingBackchannelLoginRequest(id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ViewModel CreateConsentViewModel(BackchannelUserLoginRequest request)
|
||||
{
|
||||
var vm = new ViewModel
|
||||
{
|
||||
ClientName = request.Client.ClientName ?? request.Client.ClientId,
|
||||
ClientUrl = request.Client.ClientUri,
|
||||
ClientLogoUrl = request.Client.LogoUri,
|
||||
BindingMessage = request.BindingMessage
|
||||
};
|
||||
|
||||
vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources
|
||||
.Select(x => CreateScopeViewModel(x, Input == null || Input.ScopesConsented.Contains(x.Name)))
|
||||
.ToArray();
|
||||
|
||||
var resourceIndicators = request.RequestedResourceIndicators ?? Enumerable.Empty<string>();
|
||||
var apiResources = request.ValidatedResources.Resources.ApiResources.Where(x => resourceIndicators.Contains(x.Name));
|
||||
|
||||
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, Input == null || Input.ScopesConsented.Contains(parsedScope.RawValue));
|
||||
scopeVm.Resources = apiResources.Where(x => x.Scopes.Contains(parsedScope.ParsedName))
|
||||
.Select(x => new ResourceViewModel
|
||||
{
|
||||
Name = x.Name,
|
||||
DisplayName = x.DisplayName ?? x.Name,
|
||||
}).ToArray();
|
||||
apiScopes.Add(scopeVm);
|
||||
}
|
||||
}
|
||||
if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
|
||||
{
|
||||
apiScopes.Add(GetOfflineAccessScope(Input == null || Input.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess)));
|
||||
}
|
||||
vm.ApiScopes = apiScopes;
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = identity.Name,
|
||||
Value = identity.Name,
|
||||
DisplayName = identity.DisplayName ?? identity.Name,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static 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
|
||||
{
|
||||
Name = parsedScopeValue.ParsedName,
|
||||
Value = parsedScopeValue.RawValue,
|
||||
DisplayName = displayName,
|
||||
Description = apiScope.Description,
|
||||
Emphasize = apiScope.Emphasize,
|
||||
Required = apiScope.Required,
|
||||
Checked = check || apiScope.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel GetOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = ConsentOptions.OfflineAccessDisplayName,
|
||||
Description = ConsentOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Ciba;
|
||||
|
||||
public static class ConsentOptions
|
||||
{
|
||||
public static readonly bool EnableOfflineAccess = true;
|
||||
public static readonly string OfflineAccessDisplayName = "Offline Access";
|
||||
public static readonly 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";
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Ciba.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="ciba-page">
|
||||
<div class="lead">
|
||||
@if (Model.LoginRequest.Client.LogoUri != null)
|
||||
{
|
||||
<div class="client-logo"><img src="@Model.LoginRequest.Client.LogoUri"></div>
|
||||
}
|
||||
<h1>
|
||||
@Model.LoginRequest.Client.ClientName
|
||||
<small class="text-muted">is requesting your permission</small>
|
||||
</h1>
|
||||
|
||||
<h3>
|
||||
Verify that this identifier matches what the client is displaying:
|
||||
<em class="text-primary">@Model.LoginRequest.BindingMessage</em>
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Do you wish to continue?
|
||||
</p>
|
||||
<div>
|
||||
<a class="btn btn-primary" asp-page="/Ciba/Consent" asp-route-id="@Model.LoginRequest.InternalId">Yes, Continue</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -1,42 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Ciba;
|
||||
|
||||
[AllowAnonymous]
|
||||
[SecurityHeaders]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
public BackchannelUserLoginRequest LoginRequest { get; set; } = default!;
|
||||
|
||||
private readonly IBackchannelAuthenticationInteractionService _backchannelAuthenticationInteraction;
|
||||
private readonly ILogger<IndexModel> _logger;
|
||||
|
||||
public IndexModel(IBackchannelAuthenticationInteractionService backchannelAuthenticationInteractionService, ILogger<IndexModel> logger)
|
||||
{
|
||||
_backchannelAuthenticationInteraction = backchannelAuthenticationInteractionService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet(string id)
|
||||
{
|
||||
var result = await _backchannelAuthenticationInteraction.GetLoginRequestByInternalIdAsync(id);
|
||||
if (result == null)
|
||||
{
|
||||
_logger.InvalidBackchannelLoginId(id);
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
else
|
||||
{
|
||||
LoginRequest = result;
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Ciba;
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
public string? Button { get; set; }
|
||||
public IEnumerable<string> ScopesConsented { get; set; } = new List<string>();
|
||||
public string? Id { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Ciba;
|
||||
|
||||
public class ViewModel
|
||||
{
|
||||
public string? ClientName { get; set; }
|
||||
public string? ClientUrl { get; set; }
|
||||
public string? ClientLogoUrl { get; set; }
|
||||
|
||||
public string? BindingMessage { get; set; }
|
||||
|
||||
public IEnumerable<ScopeViewModel> IdentityScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
|
||||
public IEnumerable<ScopeViewModel> ApiScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
|
||||
}
|
||||
|
||||
public class ScopeViewModel
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
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; }
|
||||
public IEnumerable<ResourceViewModel> Resources { get; set; } = Enumerable.Empty<ResourceViewModel>();
|
||||
}
|
||||
|
||||
public class ResourceViewModel
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
@using Yavsc.Pages.Ciba
|
||||
@model ScopeViewModel
|
||||
|
||||
<li class="list-group-item">
|
||||
<label>
|
||||
<input class="consent-scopecheck"
|
||||
type="checkbox"
|
||||
name="Input.ScopesConsented"
|
||||
id="scopes_@Model.Value"
|
||||
value="@Model.Value"
|
||||
checked="@Model.Checked"
|
||||
disabled="@Model.Required" />
|
||||
@if (Model.Required)
|
||||
{
|
||||
<input type="hidden"
|
||||
name="Input.ScopesConsented"
|
||||
value="@Model.Value" />
|
||||
}
|
||||
<strong>@Model.DisplayName</strong>
|
||||
@if (Model.Emphasize)
|
||||
{
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
}
|
||||
</label>
|
||||
@if (Model.Required)
|
||||
{
|
||||
<span><em>(required)</em></span>
|
||||
}
|
||||
@if (Model.Description != null)
|
||||
{
|
||||
<div class="consent-description">
|
||||
<label for="scopes_@Model.Value">@Model.Description</label>
|
||||
</div>
|
||||
}
|
||||
@if (Model.Resources?.Any() == true)
|
||||
{
|
||||
<div class="consent-description">
|
||||
<label>Will be available to these resource servers:</label>
|
||||
<ul>
|
||||
@foreach (var resource in Model.Resources)
|
||||
{
|
||||
<li>@resource.DisplayName</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
@ -1,14 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Consent;
|
||||
|
||||
public static class ConsentOptions
|
||||
{
|
||||
public static readonly bool EnableOfflineAccess = true;
|
||||
public static readonly string OfflineAccessDisplayName = "Offline Access";
|
||||
public static readonly 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";
|
||||
}
|
||||
@ -1,236 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Consent;
|
||||
|
||||
[Authorize]
|
||||
[SecurityHeaders]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
private readonly ILogger<Index> _logger;
|
||||
|
||||
public Index(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IEventService events,
|
||||
ILogger<Index> logger)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_events = events;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGet(string? returnUrl)
|
||||
{
|
||||
if (!await SetViewModelAsync(returnUrl))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
ReturnUrl = returnUrl,
|
||||
};
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
// validate return url is still valid
|
||||
var request = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);
|
||||
if (request == null) return RedirectToPage("/Home/Error/Index");
|
||||
|
||||
ConsentResponse? grantedConsent = null;
|
||||
|
||||
// user clicked 'no' - send back the standard 'access_denied' response
|
||||
if (Input.Button == "no")
|
||||
{
|
||||
grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied };
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
|
||||
}
|
||||
// user clicked 'yes' - validate the data
|
||||
else if (Input.Button == "yes")
|
||||
{
|
||||
// if the user consented to some scope, build the response model
|
||||
if (Input.ScopesConsented.Any())
|
||||
{
|
||||
var scopes = Input.ScopesConsented;
|
||||
if (ConsentOptions.EnableOfflineAccess == false)
|
||||
{
|
||||
scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
RememberConsent = Input.RememberConsent,
|
||||
ScopesValuesConsented = scopes.ToArray(),
|
||||
Description = Input.Description
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
|
||||
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
|
||||
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage);
|
||||
}
|
||||
|
||||
if (grantedConsent != null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(Input.ReturnUrl, nameof(Input.ReturnUrl));
|
||||
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.GrantConsentAsync(request, grantedConsent);
|
||||
|
||||
// redirect back to authorization endpoint
|
||||
if (request.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(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
return Redirect(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
// we need to redisplay the consent UI
|
||||
if (!await SetViewModelAsync(Input.ReturnUrl))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task<bool> SetViewModelAsync(string? returnUrl)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(returnUrl);
|
||||
|
||||
var request = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (request != null)
|
||||
{
|
||||
View = CreateConsentViewModel(request);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.NoConsentMatchingRequest(returnUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ViewModel CreateConsentViewModel(AuthorizationRequest request)
|
||||
{
|
||||
var vm = new ViewModel
|
||||
{
|
||||
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, Input == null || Input.ScopesConsented.Contains(x.Name)))
|
||||
.ToArray();
|
||||
|
||||
var resourceIndicators = request.Parameters.GetValues(OidcConstants.AuthorizeRequest.Resource) ?? Enumerable.Empty<string>();
|
||||
var apiResources = request.ValidatedResources.Resources.ApiResources.Where(x => resourceIndicators.Contains(x.Name));
|
||||
|
||||
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, Input == null || Input.ScopesConsented.Contains(parsedScope.RawValue));
|
||||
scopeVm.Resources = apiResources.Where(x => x.Scopes.Contains(parsedScope.ParsedName))
|
||||
.Select(x => new ResourceViewModel
|
||||
{
|
||||
Name = x.Name,
|
||||
DisplayName = x.DisplayName ?? x.Name,
|
||||
}).ToArray();
|
||||
apiScopes.Add(scopeVm);
|
||||
}
|
||||
}
|
||||
if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
|
||||
{
|
||||
apiScopes.Add(CreateOfflineAccessScope(Input == null || Input.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess)));
|
||||
}
|
||||
vm.ApiScopes = apiScopes;
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = identity.Name,
|
||||
Value = identity.Name,
|
||||
DisplayName = identity.DisplayName ?? identity.Name,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static 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
|
||||
{
|
||||
Name = parsedScopeValue.ParsedName,
|
||||
Value = parsedScopeValue.RawValue,
|
||||
DisplayName = displayName,
|
||||
Description = apiScope.Description,
|
||||
Emphasize = apiScope.Emphasize,
|
||||
Required = apiScope.Required,
|
||||
Checked = check || apiScope.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = ConsentOptions.OfflineAccessDisplayName,
|
||||
Description = ConsentOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Consent;
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
public string? Button { get; set; }
|
||||
public IEnumerable<string> ScopesConsented { get; set; } = new List<string>();
|
||||
public bool RememberConsent { get; set; } = true;
|
||||
public string? ReturnUrl { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Consent;
|
||||
|
||||
public class ViewModel
|
||||
{
|
||||
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; } = Enumerable.Empty<ScopeViewModel>();
|
||||
public IEnumerable<ScopeViewModel> ApiScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
|
||||
}
|
||||
|
||||
public class ScopeViewModel
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
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; }
|
||||
public IEnumerable<ResourceViewModel> Resources { get; set; } = Enumerable.Empty<ResourceViewModel>();
|
||||
}
|
||||
|
||||
public class ResourceViewModel
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
@using Yavsc.Pages.Consent
|
||||
@model ScopeViewModel
|
||||
|
||||
<li class="list-group-item">
|
||||
<label>
|
||||
<input class="consent-scopecheck"
|
||||
type="checkbox"
|
||||
name="Input.ScopesConsented"
|
||||
id="scopes_@Model.Value"
|
||||
value="@Model.Value"
|
||||
checked="@Model.Checked"
|
||||
disabled="@Model.Required" />
|
||||
@if (Model.Required)
|
||||
{
|
||||
<input type="hidden"
|
||||
name="Input.ScopesConsented"
|
||||
value="@Model.Value" />
|
||||
}
|
||||
<strong>@Model.DisplayName</strong>
|
||||
@if (Model.Emphasize)
|
||||
{
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
}
|
||||
</label>
|
||||
@if (Model.Required)
|
||||
{
|
||||
<span><em>(required)</em></span>
|
||||
}
|
||||
@if (Model.Description != null)
|
||||
{
|
||||
<div class="consent-description">
|
||||
<label for="scopes_@Model.Value">@Model.Description</label>
|
||||
</div>
|
||||
}
|
||||
@if (Model.Resources?.Any() == true)
|
||||
{
|
||||
<div class="consent-description">
|
||||
<label>Will be available to these resource servers:</label>
|
||||
<ul>
|
||||
@foreach (var resource in Model.Resources)
|
||||
{
|
||||
<li>@resource.DisplayName</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
@ -1,15 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Device;
|
||||
|
||||
public static class DeviceOptions
|
||||
{
|
||||
public static readonly bool EnableOfflineAccess = true;
|
||||
public static readonly string OfflineAccessDisplayName = "Offline Access";
|
||||
public static readonly string OfflineAccessDescription = "Access to your applications and resources, even when you are offline";
|
||||
|
||||
public static readonly string InvalidUserCode = "Invalid user code";
|
||||
public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission";
|
||||
public static readonly string InvalidSelectionErrorMessage = "Invalid selection";
|
||||
}
|
||||
@ -1,141 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Device.Index
|
||||
@{
|
||||
}
|
||||
|
||||
@if (Model.Input.UserCode == null)
|
||||
{
|
||||
@*We need to collect the user code*@
|
||||
<div class="page-device-code">
|
||||
<div class="lead">
|
||||
<h1>User Code</h1>
|
||||
<p>Please enter the code displayed on your device.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<partial name="_ValidationSummary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<form asp-page="/Device/Index" method="get">
|
||||
<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>
|
||||
}
|
||||
else
|
||||
{
|
||||
@*collect consent for the user code provided*@
|
||||
<div class="page-device-confirmation">
|
||||
<div class="lead">
|
||||
@if (Model.View.ClientLogoUrl != null)
|
||||
{
|
||||
<div class="client-logo"><img src="@Model.View.ClientLogoUrl"></div>
|
||||
}
|
||||
<h1>
|
||||
@Model.View.ClientName
|
||||
<small class="text-muted">is requesting your permission</small>
|
||||
</h1>
|
||||
<p>Please confirm that the authorization request matches the code: <strong>@Model.Input.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-page="/Device/Index">
|
||||
<input asp-for="Input.UserCode" type="hidden" />
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
@if (Model.View.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.View.IdentityScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="@scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.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.View.ApiScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
Description
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input class="form-control" placeholder="Description or name of device" asp-for="Input.Description" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.View.AllowRememberConsent)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" asp-for="Input.RememberConsent">
|
||||
<label class="form-check-label" asp-for="Input.RememberConsent">
|
||||
<strong>Remember My Decision</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<button name="Input.button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
|
||||
<button name="Input.button" value="no" class="btn btn-secondary">No, Do Not Allow</button>
|
||||
</div>
|
||||
<div class="col-sm-4 col-lg-auto">
|
||||
@if (Model.View.ClientUrl != null)
|
||||
{
|
||||
<a class="btn btn-outline-info" href="@Model.View.ClientUrl">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
<strong>@Model.View.ClientName</strong>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
@ -1,220 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Configuration;
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Yavsc.Pages.Consent;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Yavsc.Pages.Device;
|
||||
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IDeviceFlowInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
private readonly IOptions<IdentityServerOptions> _options;
|
||||
private readonly ILogger<Index> _logger;
|
||||
|
||||
public Index(
|
||||
IDeviceFlowInteractionService interaction,
|
||||
IEventService eventService,
|
||||
IOptions<IdentityServerOptions> options,
|
||||
ILogger<Index> logger)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_events = eventService;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGet(string? userCode)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(userCode))
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
if (!await SetViewModelAsync(userCode))
|
||||
{
|
||||
ModelState.AddModelError("", DeviceOptions.InvalidUserCode);
|
||||
return Page();
|
||||
}
|
||||
|
||||
Input = new InputModel {
|
||||
UserCode = userCode,
|
||||
};
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
var request = await _interaction.GetAuthorizationContextAsync(Input.UserCode ?? throw new ArgumentNullException(nameof(Input.UserCode)));
|
||||
if (request == null) return RedirectToPage("/Home/Error/Index");
|
||||
|
||||
ConsentResponse? grantedConsent = null;
|
||||
|
||||
// user clicked 'no' - send back the standard 'access_denied' response
|
||||
if (Input.Button == "no")
|
||||
{
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
Error = AuthorizationError.AccessDenied
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
|
||||
}
|
||||
// user clicked 'yes' - validate the data
|
||||
else if (Input.Button == "yes")
|
||||
{
|
||||
// if the user consented to some scope, build the response model
|
||||
if (Input.ScopesConsented.Any())
|
||||
{
|
||||
var scopes = Input.ScopesConsented;
|
||||
if (ConsentOptions.EnableOfflineAccess == false)
|
||||
{
|
||||
scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
RememberConsent = Input.RememberConsent,
|
||||
ScopesValuesConsented = scopes.ToArray(),
|
||||
Description = Input.Description
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
|
||||
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
|
||||
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage);
|
||||
}
|
||||
|
||||
if (grantedConsent != null)
|
||||
{
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.HandleRequestAsync(Input.UserCode, grantedConsent);
|
||||
|
||||
// indicate that's it ok to redirect back to authorization endpoint
|
||||
return RedirectToPage("/Device/Success");
|
||||
}
|
||||
|
||||
// we need to redisplay the consent UI
|
||||
if (!await SetViewModelAsync(Input.UserCode))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
|
||||
private async Task<bool> SetViewModelAsync(string userCode)
|
||||
{
|
||||
var request = await _interaction.GetAuthorizationContextAsync(userCode);
|
||||
if (request != null)
|
||||
{
|
||||
View = CreateConsentViewModel(request);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
View = new ViewModel();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ViewModel CreateConsentViewModel(DeviceFlowAuthorizationRequest request)
|
||||
{
|
||||
var vm = new ViewModel
|
||||
{
|
||||
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, Input == null || Input.ScopesConsented.Contains(x.Name))).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, Input == null || Input.ScopesConsented.Contains(parsedScope.RawValue));
|
||||
apiScopes.Add(scopeVm);
|
||||
}
|
||||
}
|
||||
if (DeviceOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
|
||||
{
|
||||
apiScopes.Add(GetOfflineAccessScope(Input == null || Input.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess)));
|
||||
}
|
||||
vm.ApiScopes = apiScopes;
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private static 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
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = parsedScopeValue.RawValue,
|
||||
// todo: use the parsed scope value in the display?
|
||||
DisplayName = apiScope.DisplayName ?? apiScope.Name,
|
||||
Description = apiScope.Description,
|
||||
Emphasize = apiScope.Emphasize,
|
||||
Required = apiScope.Required,
|
||||
Checked = check || apiScope.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel GetOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = DeviceOptions.OfflineAccessDisplayName,
|
||||
Description = DeviceOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Device;
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
public string? Button { get; set; }
|
||||
public IEnumerable<string> ScopesConsented { get; set; } = new List<string>();
|
||||
public bool RememberConsent { get; set; } = true;
|
||||
public string? ReturnUrl { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? UserCode { get; set; }
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Device;
|
||||
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class SuccessModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Device;
|
||||
|
||||
public class ViewModel
|
||||
{
|
||||
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; } = Enumerable.Empty<ScopeViewModel>();
|
||||
public IEnumerable<ScopeViewModel> ApiScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Diagnostics.Index
|
||||
|
||||
<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">
|
||||
@if(Model.View.AuthenticateResult.Principal != null)
|
||||
{
|
||||
<dl>
|
||||
@foreach (var claim in Model.View.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>
|
||||
@if (Model.View.AuthenticateResult.Properties != null)
|
||||
{
|
||||
@foreach (var prop in Model.View.AuthenticateResult.Properties.Items)
|
||||
{
|
||||
<dt>@prop.Key</dt>
|
||||
<dd>@prop.Value</dd>
|
||||
}
|
||||
}
|
||||
@if (Model.View.Clients.Any())
|
||||
{
|
||||
<dt>Clients</dt>
|
||||
<dd>
|
||||
@{
|
||||
var clients = Model.View.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>
|
||||
@ -1,34 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Yavsc.Pages.Diagnostics;
|
||||
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class Index : PageModel
|
||||
{
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
var localAddresses = new List<string?> { "127.0.0.1", "::1" };
|
||||
if(HttpContext.Connection.LocalIpAddress != null)
|
||||
{
|
||||
localAddresses.Add(HttpContext.Connection.LocalIpAddress.ToString());
|
||||
}
|
||||
|
||||
if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress?.ToString()))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
View = new ViewModel(await HttpContext.AuthenticateAsync());
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Yavsc.Pages.Diagnostics;
|
||||
|
||||
public class ViewModel
|
||||
{
|
||||
public ViewModel(AuthenticateResult result)
|
||||
{
|
||||
AuthenticateResult = result;
|
||||
|
||||
if (result?.Properties?.Items.TryGetValue("client_list", out var encoded) == true)
|
||||
{
|
||||
if (encoded != null)
|
||||
{
|
||||
var bytes = Base64Url.Decode(encoded);
|
||||
var value = Encoding.UTF8.GetString(bytes);
|
||||
Clients = JsonSerializer.Deserialize<string[]>(value) ?? Enumerable.Empty<string>();
|
||||
return;
|
||||
}
|
||||
}
|
||||
Clients = Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
public AuthenticateResult AuthenticateResult { get; }
|
||||
public IEnumerable<string> Clients { get; }
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines if the authentication scheme support signout.
|
||||
/// </summary>
|
||||
internal static async Task<bool> GetSchemeSupportsSignOutAsync(this HttpContext context, string scheme)
|
||||
{
|
||||
var provider = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
|
||||
var handler = await provider.GetHandlerAsync(context, scheme);
|
||||
return (handler is IAuthenticationSignOutHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the redirect URI is for a native client.
|
||||
/// </summary>
|
||||
internal static bool IsNativeClient(this AuthorizationRequest context)
|
||||
{
|
||||
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
|
||||
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a loading page that is used to redirect back to the redirectUri.
|
||||
/// </summary>
|
||||
internal static IActionResult LoadingPage(this PageModel page, string? redirectUri)
|
||||
{
|
||||
page.HttpContext.Response.StatusCode = 200;
|
||||
page.HttpContext.Response.Headers["Location"] = "";
|
||||
|
||||
return page.RedirectToPage("/Redirect/Index", new { RedirectUri = redirectUri });
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.ExternalLogin.Callback
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,203 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Security.Claims;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Services;
|
||||
using IdentityModel;
|
||||
using Yavsc.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.ExternalLogin;
|
||||
|
||||
[AllowAnonymous]
|
||||
[SecurityHeaders]
|
||||
public class Callback : PageModel
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly ILogger<Callback> _logger;
|
||||
private readonly IEventService _events;
|
||||
|
||||
public Callback(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IEventService events,
|
||||
ILogger<Callback> logger,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_interaction = interaction;
|
||||
_logger = logger;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
// read external identity from the temporary cookie
|
||||
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
|
||||
if (result.Succeeded != true)
|
||||
{
|
||||
throw new InvalidOperationException($"External authentication error: { result.Failure }");
|
||||
}
|
||||
|
||||
var externalUser = result.Principal ??
|
||||
throw new InvalidOperationException("External authentication produced a null Principal");
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
var externalClaims = externalUser.Claims.Select(c => $"{c.Type}: {c.Value}");
|
||||
_logger.ExternalClaims(externalClaims);
|
||||
}
|
||||
|
||||
// lookup our user and external provider info
|
||||
// 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 InvalidOperationException("Unknown userid");
|
||||
|
||||
var provider = result.Properties.Items["scheme"] ?? throw new InvalidOperationException("Null scheme in authentiation properties");
|
||||
var providerUserId = userIdClaim.Value;
|
||||
|
||||
// find external user
|
||||
var user = await _userManager.FindByLoginAsync(provider, providerUserId);
|
||||
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, externalUser.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();
|
||||
CaptureExternalLoginContext(result, additionalLocalClaims, localSignInProps);
|
||||
|
||||
// issue authentication cookie for user
|
||||
await _signInManager.SignInWithClaimsAsync(user, localSignInProps, additionalLocalClaims);
|
||||
|
||||
// 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, user.UserName, true, context?.Client.ClientId));
|
||||
Telemetry.Metrics.UserLogin(context?.Client.ClientId, provider!);
|
||||
|
||||
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(returnUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1851:Possible multiple enumerations of 'IEnumerable' collection", Justification = "<Pending>")]
|
||||
private async Task<ApplicationUser> AutoProvisionUserAsync(string provider, string providerUserId, IEnumerable<Claim> claims)
|
||||
{
|
||||
var sub = Guid.NewGuid().ToString();
|
||||
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
Id = sub,
|
||||
UserName = sub, // don't need a username, since the user will be using an external provider to login
|
||||
};
|
||||
|
||||
// email
|
||||
var email = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Email)?.Value ??
|
||||
claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value;
|
||||
if (email != null)
|
||||
{
|
||||
user.Email = email;
|
||||
}
|
||||
|
||||
// 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 identityResult = await _userManager.CreateAsync(user);
|
||||
if (!identityResult.Succeeded) throw new InvalidOperationException(identityResult.Errors.First().Description);
|
||||
|
||||
if (filtered.Count != 0)
|
||||
{
|
||||
identityResult = await _userManager.AddClaimsAsync(user, filtered);
|
||||
if (!identityResult.Succeeded) throw new InvalidOperationException(identityResult.Errors.First().Description);
|
||||
}
|
||||
|
||||
identityResult = await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerUserId, provider));
|
||||
if (!identityResult.Succeeded) throw new InvalidOperationException(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 static void CaptureExternalLoginContext(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(externalResult.Principal, nameof(externalResult.Principal));
|
||||
|
||||
// capture the idp used to login, so the session knows where the user came from
|
||||
localClaims.Add(new Claim(JwtClaimTypes.IdentityProvider, externalResult.Properties?.Items["scheme"] ?? "unknown identity provider"));
|
||||
|
||||
// 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 } });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.ExternalLogin.Challenge
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,48 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.ExternalLogin;
|
||||
|
||||
[AllowAnonymous]
|
||||
[SecurityHeaders]
|
||||
public class Challenge : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interactionService;
|
||||
|
||||
public Challenge(IIdentityServerInteractionService interactionService)
|
||||
{
|
||||
_interactionService = interactionService;
|
||||
}
|
||||
|
||||
public IActionResult OnGet(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 && _interactionService.IsValidReturnUrl(returnUrl) == false)
|
||||
{
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new ArgumentException("invalid return URL");
|
||||
}
|
||||
|
||||
// start challenge and roundtrip the return URL and scheme
|
||||
var props = new AuthenticationProperties
|
||||
{
|
||||
RedirectUri = Url.Page("/externallogin/callback"),
|
||||
|
||||
Items =
|
||||
{
|
||||
{ "returnUrl", returnUrl },
|
||||
{ "scheme", scheme },
|
||||
}
|
||||
};
|
||||
|
||||
return Challenge(props, scheme);
|
||||
}
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Grants;
|
||||
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IClientStore _clients;
|
||||
private readonly IResourceStore _resources;
|
||||
private readonly IEventService _events;
|
||||
|
||||
public Index(IIdentityServerInteractionService interaction,
|
||||
IClientStore clients,
|
||||
IResourceStore resources,
|
||||
IEventService events)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_clients = clients;
|
||||
_resources = resources;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
public async Task OnGet()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
View = new ViewModel
|
||||
{
|
||||
Grants = list
|
||||
};
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public string? ClientId { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
await _interaction.RevokeUserConsentAsync(ClientId);
|
||||
await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), ClientId));
|
||||
Telemetry.Metrics.GrantsRevoked(ClientId);
|
||||
|
||||
return RedirectToPage("/Grants/Index");
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages.Grants;
|
||||
|
||||
public class ViewModel
|
||||
{
|
||||
public IEnumerable<GrantViewModel> Grants { get; set; } = Enumerable.Empty<GrantViewModel>();
|
||||
}
|
||||
|
||||
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; } = Enumerable.Empty<string>();
|
||||
public IEnumerable<string> ApiGrantNames { get; set; } = Enumerable.Empty<string>();
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Error.Index
|
||||
|
||||
<div class="error-page">
|
||||
<div class="lead">
|
||||
<h1>Error</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="alert alert-danger">
|
||||
Sorry, there was an error
|
||||
|
||||
@if (Model.View.Error != null)
|
||||
{
|
||||
<strong>
|
||||
<em>
|
||||
: @Model.View.Error.Error
|
||||
</em>
|
||||
</strong>
|
||||
|
||||
if (Model.View.Error.ErrorDescription != null)
|
||||
{
|
||||
<div>@Model.View.Error.ErrorDescription</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Model?.View?.Error?.RequestId != null)
|
||||
{
|
||||
<div class="request-id">Request Id: @Model.View.Error.RequestId</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,40 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Error;
|
||||
|
||||
[AllowAnonymous]
|
||||
[SecurityHeaders]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
public ViewModel View { get; set; } = new();
|
||||
|
||||
public Index(IIdentityServerInteractionService interaction, IWebHostEnvironment environment)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public async Task OnGet(string? errorId)
|
||||
{
|
||||
// retrieve error details from identityserver
|
||||
var message = await _interaction.GetErrorContextAsync(errorId);
|
||||
if (message != null)
|
||||
{
|
||||
View.Error = message;
|
||||
|
||||
if (!_environment.IsDevelopment())
|
||||
{
|
||||
// only show in development
|
||||
message.ErrorDescription = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
// global/shared
|
||||
[assembly: SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Consistent with the IdentityServer APIs")]
|
||||
[assembly: SuppressMessage("Design", "CA1056:URI-like properties should not be strings", Justification = "Consistent with the IdentityServer APIs")]
|
||||
[assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "No need for ConfigureAwait in ASP.NET Core application code, as there is no SynchronizationContext.")]
|
||||
|
||||
// page specific
|
||||
[assembly: SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "TestUsers are not designed to be extended", Scope = "member", Target = "~P:Yavsc.TestUsers.Users")]
|
||||
[assembly: SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "ExternalProvider is nested by design", Scope = "type", Target = "~T:Yavsc.Pages.Login.ViewModel.ExternalProvider")]
|
||||
[assembly: SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "This namespace is just for organization, and won't be referenced elsewhere", Scope = "namespace", Target = "~N:Yavsc.Pages.Error")]
|
||||
[assembly: SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Namespaces of pages are not likely to be used elsewhere, so there is little chance of confusion", Scope = "type", Target = "~T:Yavsc.Pages.Ciba.Consent")]
|
||||
[assembly: SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Namespaces of pages are not likely to be used elsewhere, so there is little chance of confusion", Scope = "type", Target = "~T:Yavsc.Pages.Extensions")]
|
||||
[assembly: SuppressMessage("Performance", "CA1805:Do not initialize unnecessarily", Justification = "This is for clarity and consistency with the surrounding code", Scope = "member", Target = "~F:Yavsc.Pages.Logout.LogoutOptions.AutomaticRedirectAfterSignOut")]
|
||||
@ -1,46 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Home.Index
|
||||
|
||||
<div class="welcome-page">
|
||||
<h1>
|
||||
<img src="~/logo.svg" class="logo">
|
||||
Welcome to Duende IdentityServer
|
||||
<small class="text-muted">(version @Model.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>
|
||||
Click <a href="~/serversidesessions">here</a> to view the server side sessions.
|
||||
</li>
|
||||
<li>
|
||||
Click <a href="~/ciba/all">here</a> to view your pending CIBA login requests.
|
||||
</li>
|
||||
<li>
|
||||
Here are links to the
|
||||
<a href="https://github.com/duendesoftware/IdentityServer">source code repository</a>,
|
||||
and <a href="https://github.com/duendesoftware/samples">ready to use samples</a>.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if(Model.License != null)
|
||||
{
|
||||
<h2>License</h2>
|
||||
<dl>
|
||||
<dt>Serial Number</dt>
|
||||
<dd>@Model.License.SerialNumber</dd>
|
||||
<dt>Expiration</dt>
|
||||
<dd>@Model.License.Expiration!.Value.ToLongDateString()</dd>
|
||||
</dl>
|
||||
}
|
||||
</div>
|
||||
@ -1,27 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Yavsc.Pages.Home;
|
||||
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
public Index(IdentityServerLicense? license = null)
|
||||
{
|
||||
License = license;
|
||||
}
|
||||
|
||||
public string Version
|
||||
{
|
||||
get => typeof(Duende.IdentityServer.Hosting.IdentityServerMiddleware).Assembly
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion.Split('+').First()
|
||||
?? "unavailable";
|
||||
}
|
||||
public IdentityServerLicense? License { get; }
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Yavsc.Pages;
|
||||
|
||||
internal static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, string?, Exception?> _invalidId = LoggerMessage.Define<string?>(
|
||||
LogLevel.Error,
|
||||
EventIds.InvalidId,
|
||||
"Invalid id {Id}");
|
||||
|
||||
public static void InvalidId(this ILogger logger, string? id)
|
||||
{
|
||||
_invalidId(logger, id, null);
|
||||
}
|
||||
|
||||
private static readonly Action<ILogger, string?, Exception?> _invalidBackchannelLoginId = LoggerMessage.Define<string?>(
|
||||
LogLevel.Warning,
|
||||
EventIds.InvalidBackchannelLoginId,
|
||||
"Invalid backchannel login id {Id}");
|
||||
|
||||
public static void InvalidBackchannelLoginId(this ILogger logger, string? id)
|
||||
{
|
||||
_invalidBackchannelLoginId(logger, id, null);
|
||||
}
|
||||
|
||||
private static Action<ILogger, IEnumerable<string>, Exception?> _externalClaims = LoggerMessage.Define<IEnumerable<string>>(
|
||||
LogLevel.Debug,
|
||||
EventIds.ExternalClaims,
|
||||
"External claims: {Claims}");
|
||||
|
||||
public static void ExternalClaims(this ILogger logger, IEnumerable<string> claims)
|
||||
{
|
||||
_externalClaims(logger, claims, null);
|
||||
}
|
||||
|
||||
private static Action<ILogger, string, Exception?> _noMatchingBackchannelLoginRequest = LoggerMessage.Define<string>(
|
||||
LogLevel.Error,
|
||||
EventIds.NoMatchingBackchannelLoginRequest,
|
||||
"No backchannel login request matching id: {Id}");
|
||||
|
||||
public static void NoMatchingBackchannelLoginRequest(this ILogger logger, string id)
|
||||
{
|
||||
_noMatchingBackchannelLoginRequest(logger, id, null);
|
||||
}
|
||||
|
||||
private static Action<ILogger, string, Exception?> _noConsentMatchingRequest = LoggerMessage.Define<string>(
|
||||
LogLevel.Error,
|
||||
EventIds.NoConsentMatchingRequest,
|
||||
"No consent request matching request: {ReturnUrl}");
|
||||
|
||||
public static void NoConsentMatchingRequest(this ILogger logger, string returnUrl)
|
||||
{
|
||||
_noConsentMatchingRequest(logger, returnUrl, null);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
internal static class EventIds
|
||||
{
|
||||
private const int UIEventsStart = 10000;
|
||||
|
||||
//////////////////////////////
|
||||
// Consent
|
||||
//////////////////////////////
|
||||
private const int ConsentEventsStart = UIEventsStart + 1000;
|
||||
public const int InvalidId = ConsentEventsStart + 0;
|
||||
public const int NoConsentMatchingRequest = ConsentEventsStart + 1;
|
||||
|
||||
//////////////////////////////
|
||||
// External Login
|
||||
//////////////////////////////
|
||||
private const int ExternalLoginEventsStart = UIEventsStart + 2000;
|
||||
public const int ExternalClaims = ExternalLoginEventsStart + 0;
|
||||
|
||||
//////////////////////////////
|
||||
// CIBA
|
||||
//////////////////////////////
|
||||
private const int CibaEventsStart = UIEventsStart + 3000;
|
||||
public const int InvalidBackchannelLoginId = CibaEventsStart + 0;
|
||||
public const int NoMatchingBackchannelLoginRequest = CibaEventsStart + 1;
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
@page
|
||||
@model Yavsc.Pages.Redirect.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<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.RedirectUri" data-url="@Model.RedirectUri">
|
||||
<script src="~/js/signin-redirect.js"></script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue