Compare commits

..

No commits in common. 'main' and 'dotnet-7.0' have entirely different histories.

1100 changed files with 25235 additions and 208753 deletions

7
.gitignore vendored

@ -7,7 +7,7 @@
.paket/
.vscode/
.vs/
.sass-cache/
.dnx/
bin
obj
@ -21,8 +21,6 @@ RSA-Params.json
appsettings.*.json
omnisharp.json
DataProtection/
/packages/
/src/Yavsc/Avatars-*/
/src/Yavsc/Blog-*/
@ -31,7 +29,7 @@ DataProtection/
/src/Yavsc/Temp-*/
/src/Yavsc/*-Avatars/
/src/Yavsc/bower_components/
/src/Yavsc/Data-Dev/
/src/Yavsc/AppData*/
/src/test/testingrepo/
connectionsettings.Development.json
appsettings.Development.json
@ -42,4 +40,3 @@ builds/
/test/yavscTests/test-results.html
/binaries/Debug/yavscd
yavsc-pre

@ -15,10 +15,11 @@ C'est une application mettant en oeuvre une prise de contact entre un demandeur
dotnet build
```
et, pour execution en environement de développement
```
~/workspace/yavsc/Yavsc @ ASPNETCORE_ENV=Development dotnet run
[monoperso] ~/workspace/yavsc/Yavsc @ ASPNETCORE_ENV=Development dotnet run
```
## Tests

@ -6,6 +6,7 @@ Ceci est une grosse liste de fonctionnalités, existantes, ou à implémenter, o
## Jalon 1
☐ Redirection vers la Page d'accueil à la validation du second facteur (evitera l'erreur Anti-forgery token)
☐ Des spécifications détaillées du coeur de l'application
✔ Acces (publique) aux Blogs. @done (August 13th 2016, 0:51)
✔ Accès aux profiles des intervenants. @done (August 13th 2016, 0:57)

@ -0,0 +1,131 @@
#!/bin/bash
### BEGIN INIT INFO
# Provides: kestrel
# Required-Start: $local_fs $network $named $time $syslog $postgresql
# Required-Stop: $local_fs $network $named $time $syslog $postgresql
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: Script to run asp.net 5 application in background
### END INIT INFO
# Author: Ivan Derevianko aka druss <drussilla7@gmail.com>
# Modified by: Paul Schneider <redienhcs.luap@gmail.com>
. /lib/init/vars.sh
. /lib/lsb/init-functions
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
CONFIGS="/etc/kestrel/*.webenv"
TMP_SAVE_runlevel_VAR=$runlevel
unset runlevel
running() {
if [ -f $PIDFILE ]
then
PID=$(cat $PIDFILE)
if kill -0 $PID 2>/dev/null
then
return 0
fi
fi
return 1
}
setdnxenv() {
env=$1
. $env
# reset all except the name to default values
export ASPNET_ENV=$NAME
export WWW_USER=www-data
YAVSCBIN=/usr/local/bin/yavscd
PROJECT=approot/Web
CONFIGURATION=Release
ROOT=/srv/www/yavsc
DESC="$NAME"
PIDFILE=/var/run/kestrel-${NAME}.pid
LOGDIR=/var/log
# reset to specified values
. $env
}
status() {
for env in $CONFIGS
do
setdnxenv "$env"
if running;
then
echo "Service running $DESC ($NAME; pid: $PID)"
else
echo "Service stopped $DESC ($NAME)"
fi
done
}
start() {
# wait a little for postgresql to be available
sleep 1
for env in $CONFIGS
do
setdnxenv "$env"
if running; then
echo "Service already running $DESC" "$NAME"
log_end_msg 0
else
log_daemon_msg "Starting service $NAME for user $WWW_USER"
if ! start-stop-daemon -SbmCv -u $WWW_USER -p $PIDFILE -d $ROOT -g www-data -x $YAVSCBIN -- --project $PROJECT --configuration $CONFIGURATION $NAME > "${LOGDIR}/kestrel-${NAME}.log"
then
log_daemon_msg "Could not start $NAME : $?, see ${LOGDIR}/kestrel-${NAME}.log"
log_end_msg 2
else
log_daemon_msg "Service $DESC started ($NAME), logs: ${LOGDIR}/kestrel-${NAME}.log"
log_end_msg 0
fi
fi
done
}
stop() {
for env in $CONFIGS
do
setdnxenv "$env"
if running
then
log_daemon_msg "Stopping service $NAME"
start-stop-daemon -K -p "$PIDFILE"
log_daemon_msg "$DESC stopped"
log_end_msg 0
else
log_daemon_msg "$DESC Service not running"
log_end_msg 1
fi
done
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
sleep 1
start
;;
status)
status
;;
*)
echo "Usage: $0 {start|stop|restart}"
esac
export runlevel=$TMP_SAVE_runlevel_VAR

@ -1,11 +1,11 @@
#!/bin/bash
### BEGIN INIT INFO
# Provides: yavsc
# Provides: kestrel-pre
# Required-Start: $local_fs $network $named $time $syslog $postgresql
# Required-Stop: $local_fs $network $named $time $syslog $postgresql
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: Script to run yavsc
# Description: Script to run asp.net 5 application in background
### END INIT INFO
# Author: Ivan Derevianko aka druss <drussilla7@gmail.com>
@ -14,9 +14,8 @@
. /lib/init/vars.sh
. /lib/lsb/init-functions
NAME=yavsc
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
CONFIGS="/etc/kestrel/*.prewebenv"
TMP_SAVE_runlevel_VAR=$runlevel
unset runlevel
@ -34,34 +33,47 @@ running() {
}
export WWW_USER=www-data
export ROOT=/srv/www/${NAME}
export DESC="$NAME"
export PIDFILE=/var/run/kestrel-${NAME}.pid
export LOGDIR=/var/log
export DOTNET_CLI_HOME=$ROOT
export ASPDOTNETCORE_ENVIRONMENT=Production
export ASPDOTNETCORE_LOGLEVEL=Information
setdnxenv() {
env=$1
. $env
# reset all except the name to default values
export ASPNET_ENV=$NAME
export WWW_USER=www-data
YAVSCBIN=/usr/local/bin/yavscd-pre
PROJECT=approot/Web
CONFIGURATION=Release
ROOT=/srv/www/yavscpre
DESC="$NAME"
PIDFILE=/var/run/kestrel-${NAME}.pid
LOGDIR=/var/log
# reset to specified values
. $env
}
status() {
for env in $CONFIGS
do
setdnxenv "$env"
if running;
then
echo "Service running $DESC ($NAME; pid: $PID)"
else
echo "Service stopped $DESC ($NAME)"
fi
echo WWW_USER: $WWW_USER ROOT:$ROOT DESC: $DESC NAME: $NAME PIDFILE: $PIDFILE LOGDIR=$LOGDIR
done
}
start() {
for env in $CONFIGS
do
setdnxenv "$env"
if running; then
echo "Service already running $DESC" "$NAME"
log_end_msg 0
else
cd $ROOT
sleep 4
log_daemon_msg "Starting service $NAME for user $WWW_USER"
if ! start-stop-daemon -SbmCv -u $WWW_USER -p $PIDFILE -d $ROOT -g www-data -x yavsc > "${LOGDIR}/kestrel-${NAME}.log"
if ! start-stop-daemon -SbmCv -u $WWW_USER -p $PIDFILE -d $ROOT -g www-data -x $YAVSCBIN -- --project $PROJECT --configuration $CONFIGURATION $NAME > "${LOGDIR}/kestrel-${NAME}.log"
then
log_daemon_msg "Could not start $NAME : $?, see ${LOGDIR}/kestrel-${NAME}.log"
log_end_msg 2
@ -70,10 +82,14 @@ start() {
log_end_msg 0
fi
fi
done
}
stop() {
for env in $CONFIGS
do
setdnxenv "$env"
if running
then
log_daemon_msg "Stopping service $NAME"
@ -84,6 +100,7 @@ stop() {
log_daemon_msg "$DESC Service not running"
log_end_msg 1
fi
done
}
@ -96,6 +113,7 @@ case "$1" in
;;
restart)
stop
sleep 1
start
;;
status)

@ -5,7 +5,7 @@
"tests"
],
"sdk": {
"version": "8.0.200",
"version": "7.0.202",
"runtime": "dotnet",
"architecture": "x64"
},

@ -2,10 +2,9 @@ using System;
namespace Yavsc.Abstract.Workflow
{
public interface IDecidableQuery: IBaseTrackedEntity, IQuery
public interface IDecidableQuery: IQuery
{
bool Decided { get; set; }
bool Accepted { get; set; }
bool Rejected { get; set; }
DateTime RejectedAt { get; set; }
}
}

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

@ -13,7 +13,7 @@ namespace Yavsc.Models.Billing
using Yavsc.Abstract.Workflow;
using Yavsc.Services;
public abstract class NominativeServiceCommand : IDecidableQuery, IIdentified<long>
public abstract class NominativeServiceCommand : IBaseTrackedEntity, IDecidableQuery, IIdentified<long>
{
public string GetInvoiceId() { return GetType().Name + "/" + Id; }
@ -78,8 +78,9 @@ namespace Yavsc.Models.Billing
[ForeignKey("ActivityCode"),JsonIgnore,Display(Name="Domaine d'activité")]
public virtual Activity Context  { get; set ; }
public bool Decided { get; set; }
public bool Rejected { get; set; }
public DateTime RejectedAt { get; set; }
public abstract System.Collections.Generic.List<IBillItem> GetBillItems();
@ -102,6 +103,6 @@ namespace Yavsc.Models.Billing
[Display(Name = "Acquittement de la facture")]
public virtual PayPalPayment Regularisation { get; set; }
public bool Accepted { get; set; }
}
}

@ -1,12 +0,0 @@
{
"profiles": {
"SelfHost": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001"
}
}
}

@ -8,6 +8,6 @@ namespace Yavsc.ViewModels.Account
{
[YaRequired]
[YaStringLength(512)]
public string? LoginOrEmail { get; set; }
public string LoginOrEmail { get; set; }
}
}

@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.2" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.4" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.5" />
<PackageReference Include="Google.Apis.Calendar.v3" Version="1.60.0.2993" />
<PackageReference Include="PayPalMerchantSDK" Version="2.16.250" />
</ItemGroup>

@ -130,7 +130,7 @@ namespace Yavsc.ApiControllers
u => u.Id == uid
);
try {
if (Config.UserFilesOptions.FileProvider.GetFileInfo(Path.Combine(user.UserName, query.id)).Exists)
if (Startup.UserFilesOptions.FileProvider.GetFileInfo(Path.Combine(user.UserName, query.id)).Exists)
{
var result = user.MoveUserFile(query.id, query.to);
if (!result.Done) return new BadRequestObjectResult(result);

@ -49,7 +49,7 @@ namespace Yavsc.ApiControllers
string url = string.Format(
"{0}/{1}/{2}",
Config.UserFilesOptions.RequestPath.ToUriComponent(),
Startup.UserFilesOptions.RequestPath.ToUriComponent(),
userName,
filename
);

@ -32,7 +32,7 @@ namespace Yavsc.ApiControllers
public BillingController(
IAuthorizationService authorizationService,
ILoggerFactory loggerFactory,
IStringLocalizer<Yavsc.YavscLocalization> SR,
IStringLocalizer<Yavsc.YavscLocalisation> SR,
ApplicationDbContext context,
IOptions<GoogleAuthSettings> googleSettings,
IYavscMessageSender GCMSender,

@ -34,23 +34,10 @@ namespace Yavsc.ApiControllers
if (queryId == 0) return BadRequest("queryId");
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
if (billing == null) return BadRequest();
billing.Decided = true;
billing.Accepted = false;
billing.Rejected = true;
billing.RejectedAt = DateTime.Now;
dbContext.SaveChanges();
return Ok();
}
[HttpPost("query/reject")]
public IActionResult AcceptQuery(string billingCode, long queryId)
{
if (billingCode == null) return BadRequest("billingCode");
if (queryId == 0) return BadRequest("queryId");
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
if (billing == null) return BadRequest();
billing.Accepted = true;
billing.Decided = true;
dbContext.SaveChanges();
return Ok();
}
}
}

@ -166,7 +166,7 @@ namespace Yavsc.ApiControllers
if (payment==null) {
_logger.LogError("Error doing SetExpressCheckout, aborting.");
_logger.LogError(JsonConvert.SerializeObject(Config.PayPalSettings));
_logger.LogError(JsonConvert.SerializeObject(Startup.PayPalSettings));
return new StatusCodeResult(500);
}
switch (payment.Ack)

@ -1,82 +0,0 @@
using Duende.IdentityServer.Models;
using Yavsc.Settings;
namespace Yavsc;
public static class Config
{
public static string Authority { get; set; }
public static IConfigurationRoot? GoogleWebClientConfiguration { get; set; }
public static GoogleServiceAccount? GServiceAccount { get; set; }
public static SiteSettings SiteSetup { get; set; }
public static FileServerOptions UserFilesOptions { get; set; }
public static FileServerOptions GitOptions { get; set; }
public static string AvatarsDirName { set; get; }
public static string GitDirName { set; get; }
public static GoogleAuthSettings GoogleSettings { get; set; }
public static SmtpSettings SmtpSetup { get; set; }
public static string Temp { get; set; }
public static FileServerOptions AvatarsOptions { get; set; }
public static string UserBillsDirName { set; get; }
public static string UserFilesDirName { set; get; }
/// <summary>
/// Lists Available user profile classes,
/// populated at startup, using reflexion.
/// </summary>
public static List<Type> ProfileTypes = new List<Type>();
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("scope1"),
new ApiScope("scope2"),
};
public static IEnumerable<Client> Clients =>
new Client[]
{
// m2m client credentials flow client
new Client
{
ClientId = "m2m.client",
ClientName = "Client Credentials Client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },
AllowedScopes = { "scope1" }
},
// interactive client using code flow + pkce
new Client
{
ClientId = "interactive",
ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:5003/signin-oidc" },
FrontChannelLogoutUri = "https://localhost:5003/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:5003/signout-callback-oidc" },
AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "scope2" }
},
};
public static PayPalSettings PayPalSettings { get; set; }
}

@ -1,17 +1,20 @@
using System.Web;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Yavsc.Models;
using Yavsc.Services;
using Yavsc.ViewModels.Account;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Yavsc.Models;
using Yavsc.ViewModels.Account;
using Yavsc.Helpers;
using Yavsc.Abstract.Manage;
using Microsoft.AspNetCore.Identity.UI.Services;
using Yavsc.Interface;
namespace Yavsc.Controllers
@ -35,13 +38,14 @@ namespace Yavsc.Controllers
readonly ApplicationDbContext _dbContext;
public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ITrueEmailSender emailSender,
IOptions<SiteSettings> siteSettings,
ILoggerFactory loggerFactory, IOptions<TwilioSettings> twilioSettings,
IStringLocalizer<Yavsc.YavscLocalization> localizer,
IStringLocalizer<Yavsc.YavscLocalisation> localizer,
ApplicationDbContext dbContext)
{
_userManager = userManager;
@ -201,7 +205,7 @@ namespace Yavsc.Controllers
return BadRequest();
}
// Note: this still is not the redirect uri given to the third party provider, at building the challenge.
var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { model.ReturnUrl }, protocol:"https", host: Config.Authority);
var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { model.ReturnUrl }, protocol:"https", host: Startup.Authority);
var properties = _signInManager.ConfigureExternalAuthenticationProperties(model.Provider, redirectUrl);
// var properties = new AuthenticationProperties{RedirectUri=ReturnUrl};
return new ChallengeResult(model.Provider, properties);
@ -234,14 +238,14 @@ namespace Yavsc.Controllers
if (result.Succeeded)
{
_logger.LogInformation(3, "User created a new account with password.");
await _emailSender.SendEmailAsync(Config.SiteSetup.Owner.Name, Config.SiteSetup.Owner.EMail,
await _emailSender.SendEmailAsync(Startup.SiteSetup.Owner.Name, Startup.SiteSetup.Owner.EMail,
$"[{_siteSettings.Title}] Inscription avec mot de passe: {user.UserName} ", $"{user.Id}/{user.UserName}/{user.Email}");
// TODO user.DiskQuota = Startup.SiteSetup.UserFiles.Quota;
// 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.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code }, protocol: "https", host: Config.Authority);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code }, protocol: "https", host: Startup.Authority);
await _emailSender.SendEmailAsync(model.UserName, model.Email, _localizer["ConfirmYourAccountTitle"],
string.Format(_localizer["ConfirmYourAccountBody"], _siteSettings.Title, callbackUrl, _siteSettings.Slogan, _siteSettings.Audience));
// No, wait for more than a login pass submission:
@ -288,7 +292,7 @@ namespace Yavsc.Controllers
{
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Action("ConfirmEmail", "Account",
new { userId = user.Id, code }, protocol: "https", host: Config.Authority);
new { userId = user.Id, code }, protocol: "https", host: Startup.Authority);
var res = await _emailSender.SendEmailAsync(user.UserName, user.Email,
this._localizer["ConfirmYourAccountTitle"],
string.Format(this._localizer["ConfirmYourAccountBody"],
@ -301,7 +305,7 @@ namespace Yavsc.Controllers
{
var code = await _userManager.GenerateTwoFactorTokenAsync(user, provider);
var callbackUrl = Url.Action("VerifyCode", "Account",
new { userId = user.Id, code, provider }, protocol: "https", host: Config.Authority);
new { userId = user.Id, code, provider }, protocol: "https", host: Startup.Authority);
var res = await _emailSender.SendEmailAsync(user.UserName, user.Email,
this._localizer["AccountEmailFactorTitle"],
string.Format(this._localizer["AccountEmailFactorBody"],
@ -412,8 +416,8 @@ namespace Yavsc.Controllers
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
info.ProviderDisplayName = info.Principal.Claims.First(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")?.Value;
throw new NotImplementedException();
// info.ProviderDisplayName = info.Claims.First(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")?.Value;
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
@ -421,7 +425,7 @@ namespace Yavsc.Controllers
await _signInManager.SignInAsync(user, isPersistent: false);
await _emailSender.SendEmailAsync(Config.SiteSetup.Owner.Name, Config.SiteSetup.Owner.EMail,
await _emailSender.SendEmailAsync(Startup.SiteSetup.Owner.Name, Startup.SiteSetup.Owner.EMail,
$"[{_siteSettings.Title}] Inscription via {info.LoginProvider}: {user.UserName} ", $"{user.Id}/{user.UserName}/{user.Email}");
_logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider);

@ -49,7 +49,7 @@ namespace Yavsc.Controllers
IOptions<GoogleAuthSettings> googleSettings,
IOptions<PayPalSettings> paypalSettings,
IOptions<CompanyInfoSettings> cinfoSettings,
IStringLocalizer<Yavsc.YavscLocalization> SR,
IStringLocalizer<Yavsc.YavscLocalisation> SR,
ICalendarManager calendarManager,
ILoggerFactory loggerFactory)
{

@ -102,7 +102,7 @@ namespace Yavsc.Controllers
var youAreAdmin = await _userManager.IsInRoleAsync(
await _userManager.FindByIdAsync(User.GetUserId()),
Constants.AdminGroupName);
throw new NotImplementedException();
var roles = _roleManager.Roles.Select(x => new RoleInfo {
Id = x.Id,
Name = x.Name
@ -110,6 +110,7 @@ namespace Yavsc.Controllers
var assembly = GetType().Assembly;
ViewBag.ThisAssembly = assembly.FullName;
ViewBag.RunTimeVersion = assembly.ImageRuntimeVersion;
ViewBag.HostContextFullName = Startup.HostingFullName;
return View(new AdminViewModel
{
Roles = roles.ToArray(),
@ -119,6 +120,7 @@ namespace Yavsc.Controllers
});
}
[Authorize("AdministratorOnly")]
public IActionResult Enroll(string roleName)
{

@ -48,7 +48,7 @@ namespace Yavsc.Controllers
return View();
}
[Route("~/Title/{id?}")]
[Route("/Title/{id?}")]
[AllowAnonymous]
public IActionResult Title(string id)
{
@ -61,7 +61,7 @@ namespace Yavsc.Controllers
).ToList());
}
[Route("~/Blog/{userName}/{pageLen?}/{pageNum?}")]
[Route("/Blog/{userName}/{pageLen?}/{pageNum?}")]
[AllowAnonymous]
public async Task<IActionResult> UserPosts(string userName, int pageLen=10, int pageNum=0)
{

@ -14,11 +14,11 @@ namespace Yavsc.Controllers
public class ActivityController : Controller
{
private readonly ApplicationDbContext _context;
readonly IStringLocalizer<Yavsc.YavscLocalization> SR;
readonly IStringLocalizer<Yavsc.YavscLocalisation> SR;
readonly ILogger logger;
public ActivityController(ApplicationDbContext context,
IStringLocalizer<Yavsc.YavscLocalization> SR,
IStringLocalizer<Yavsc.YavscLocalisation> SR,
ILoggerFactory loggerFactory)
{
_context = context;
@ -35,7 +35,7 @@ namespace Yavsc.Controllers
private void SetSettingClasseInfo(string currentCode = null)
{
var items = Config.ProfileTypes.Select(
var items = Startup.ProfileTypes.Select(
pt => new SelectListItem
{
Text = SR[pt.FullName],

@ -35,7 +35,7 @@ namespace Yavsc.Controllers
IYavscMessageSender messageSender,
UserManager<ApplicationUser> userManager,
ICalendarManager calendarManager,
IStringLocalizer<Yavsc.YavscLocalization> localizer,
IStringLocalizer<Yavsc.YavscLocalisation> localizer,
ITrueEmailSender emailSender,
IOptions<SmtpSettings> smtpSettings,
IOptions<SiteSettings> siteSettings,

@ -49,7 +49,7 @@ namespace Yavsc.Controllers
private void SetViewBag(CommandForm commandForm = null)
{
ViewBag.ActivityCode = new SelectList(_context.Activities, "Code", "Name", commandForm?.ActivityCode);
ViewBag.ActionName = _context.CommandForm.Select(c => new SelectListItem { Value = c.Id.ToString(), Text = c.Title, Selected = commandForm.Id == c.Id });
ViewBag.ActionName = Startup.Forms.Select(c => new SelectListItem { Value = c, Text = c, Selected = (commandForm?.ActionName == c) });
}
// POST: CommandForms/Create
[HttpPost]

@ -60,7 +60,7 @@ namespace Yavsc.Controllers
}
bool hasConfigurableSettings = (userActivity.Does.SettingsClassName != null);
var settings = await billing.GetPerformerSettingsAsync(activityCode,id);
ViewBag.ProfileType = Config.ProfileTypes.Single(t=>t.FullName==userActivity.Does.SettingsClassName);
ViewBag.ProfileType = Startup.ProfileTypes.Single(t=>t.FullName==userActivity.Does.SettingsClassName);
var gift = new UserActivityViewModel {
Declaration = userActivity,

@ -24,7 +24,7 @@ namespace Yavsc.Controllers
UserManager<ApplicationUser> userManager,
IBillingService billing,
ILoggerFactory loggerFactory,
IStringLocalizer<Yavsc.YavscLocalization> SR)
IStringLocalizer<Yavsc.YavscLocalisation> SR)
{
_context = context;
_userManager = userManager;
@ -92,7 +92,7 @@ namespace Yavsc.Controllers
[Authorize, Route("Estimate-{id}.pdf")]
public IActionResult EstimatePdf(long id)
{
ViewBag.TempDir = Config.SiteSetup.TempDir;
ViewBag.TempDir = Startup.SiteSetup.TempDir;
ViewBag.BillsDir = AbstractFileSystemHelpers.UserBillsDirName;
var estimate = _context.Estimates.Include(x => x.Query)
.Include(x => x.Query.Client)

@ -34,7 +34,7 @@ namespace Yavsc.Controllers
IOptions<GoogleAuthSettings> googleSettings,
IYavscMessageSender GCMSender,
UserManager<ApplicationUser> userManager,
IStringLocalizer<Yavsc.YavscLocalization> localizer,
IStringLocalizer<Yavsc.YavscLocalisation> localizer,
ITrueEmailSender emailSender,
IOptions<SmtpSettings> smtpSettings,
IOptions<SiteSettings> siteSettings,

@ -33,7 +33,7 @@ namespace Yavsc.Controllers
return NotFound();
}
*/
var info = Config.GitOptions.FileProvider.GetFileInfo(path);
var info = Startup.GitOptions.FileProvider.GetFileInfo(path);
if (!info.Exists)
return NotFound();
var stream = info.CreateReadStream();

@ -16,11 +16,11 @@ namespace Yavsc.Controllers
public class ProjectController : Controller
{
private readonly ApplicationDbContext _context;
readonly IStringLocalizer<Yavsc.YavscLocalization> _localizer;
readonly IStringLocalizer<Yavsc.YavscLocalisation> _localizer;
readonly IStringLocalizer<BugController> _bugLocalizer;
public ProjectController(ApplicationDbContext context,
IStringLocalizer<Yavsc.YavscLocalization> localizer,
IStringLocalizer<Yavsc.YavscLocalisation> localizer,
IStringLocalizer<BugController> bugLocalizer
)
{

@ -60,14 +60,6 @@ namespace Yavsc.Helpers
}
}
public static string GetValidHRef(this Link link)
{
if (link.Href.StartsWith("link:\\"))
return link.Href.Substring(7);
if (link.Href.StartsWith("link:"))
return link.Href.Substring(5);
return link.Href;
}
static void ToHtml(this IInlineElement elt, IHtmlContentBuilder sb)
{
@ -75,26 +67,11 @@ namespace Yavsc.Helpers
{
case "AsciiDocNet.Link":
Link link = (Link)elt;
Uri uri;
if (Uri.TryCreate(link.Href,
UriKind.RelativeOrAbsolute
, out uri))
{
if (string.IsNullOrEmpty(link.Text))
{
link.Text = $"{uri.Host}({uri.LocalPath})";
}
}
sb.AppendFormat("<a href=\"{0}\">{1}</a> ", link.GetValidHRef(), link.Text);
sb.AppendFormat("<a href=\"{0}\">{1}</a> ", link.Href, link.Text);
break;
case "AsciiDocNet.TextLiteral":
var tl = elt as TextLiteral;
if (tl.Attributes.Anchor!=null)
{
sb.AppendFormat("<a name=\"{0}\">{1}</a> ", tl.Attributes.Anchor.Id, tl.Attributes.Anchor.XRefLabel);
}
sb.Append(tl.Text);
sb.Append(elt.ToString());
break;
case "AsciiDocNet.Emphasis":
@ -113,10 +90,6 @@ namespace Yavsc.Helpers
}
sb.AppendHtml("</b>");
break;
case "AsciiDocNet.InternalAnchor":
InternalAnchor a = (InternalAnchor) elt;
sb.AppendFormat("<a name=\"{0}\">{1}</a> ", a.Id, a.XRefLabel);
break;
default:
string unsupportedType = elt.GetType().FullName;

@ -8,16 +8,14 @@ namespace Yavsc.Helpers
{
public override async Task ProcessAsync (TagHelperContext context, TagHelperOutput output)
{
await base.ProcessAsync(context, output);
var content = await output.GetChildContentAsync();
string text = content.GetContent();
if (string.IsNullOrWhiteSpace(text)) return;
Document document = Document.Parse(text);
var html = document.ToHtml(2);
var html = document.ToHtml(4);
using var stringWriter = new StringWriter();
html.WriteTo(stringWriter, HtmlEncoder.Default);
var processedHere = stringWriter.ToString();
output.Content.AppendHtml(processedHere);
output.Content.AppendHtml(stringWriter.ToString());
}
}
}

@ -50,7 +50,7 @@ namespace Yavsc.Helpers
}
public static string GetSender(this ApplicationUser user)
{
return user.UserName+" ["+user.Id+"@"+Config.Authority+"]";
return user.UserName+" ["+user.Id+"@"+Startup.Authority+"]";
}
public static HairCutQueryEvent CreateEvent(this HairMultiCutQuery query,
IStringLocalizer SR, BrusherProfile bpr)

@ -21,7 +21,7 @@ namespace Yavsc.Helpers
FileName = AbstractFileSystemHelpers.SignFileNameFormat("pro", billingCode, estimateId)
};
var destFileName = Path.Combine(Config.SiteSetup.Bills, item.FileName);
var destFileName = Path.Combine(Startup.SiteSetup.Bills, item.FileName);
var fi = new FileInfo(destFileName);
if (fi.Exists) item.Overriden = true;
@ -44,7 +44,7 @@ namespace Yavsc.Helpers
/// <param name="source"></param>
private static void CreateAvatars(this ApplicationUser user, Bitmap source)
{
var dir = Config.SiteSetup.Avatars;
var dir = Startup.SiteSetup.Avatars;
var name = user.UserName + ".png";
var smallname = user.UserName + ".s.png";
var xsmallname = user.UserName + ".xs.png";
@ -235,12 +235,12 @@ namespace Yavsc.Helpers
public static HtmlString FileLink(this RemoteFileInfo info, string username, string subpath)
{
return new HtmlString(
$"{Config.UserFilesOptions.RequestPath}/{username}/{subpath}/{info.Name}" );
$"{Startup.UserFilesOptions.RequestPath}/{username}/{subpath}/{info.Name}" );
}
public static RemoteFileInfo FileInfo(this ApplicationUser user, string path)
{
IFileInfo info = Config.UserFilesOptions.FileProvider.GetFileInfo($"{user.UserName}/{path}");
IFileInfo info = Startup.UserFilesOptions.FileProvider.GetFileInfo($"{user.UserName}/{path}");
if (!info.Exists) return null;
return new RemoteFileInfo{ Name = info.Name, Size = info.Length, LastModified = info.LastModified.UtcDateTime };
@ -253,7 +253,7 @@ namespace Yavsc.Helpers
FileName = user.UserName + ".png"
};
var destFileName = Path.Combine(Config.SiteSetup.Avatars, item.FileName);
var destFileName = Path.Combine(Startup.SiteSetup.Avatars, item.FileName);
var fi = new FileInfo(destFileName);
if (fi.Exists) item.Overriden = true;
@ -289,7 +289,7 @@ namespace Yavsc.Helpers
}
}
item.DestDir = Config.AvatarsOptions.RequestPath.ToUriComponent();
item.DestDir = Startup.AvatarsOptions.RequestPath.ToUriComponent();
user.Avatar = $"{item.DestDir}/{item.FileName}";
return item;
}
@ -298,7 +298,7 @@ namespace Yavsc.Helpers
{
if (flow.DifferedFileName==null) return null;
// no server-side backup for this stream
return $"{Config.UserFilesOptions.RequestPath}/{flow.Owner.UserName}/live/"+GetFileName(flow);
return $"{Startup.UserFilesOptions.RequestPath}/{flow.Owner.UserName}/live/"+GetFileName(flow);
}
public static string GetFileName (this LiveFlow flow)

@ -110,8 +110,8 @@ namespace Yavsc.Helpers
try {
using (var m = new SimpleJsonPostMethod(ep)) {
return await m.Invoke<TokenResponse>(
new { refresh_token= oldResponse.RefreshToken, client_id=Config.GoogleWebClientConfiguration["web:client_id"],
client_secret=Config.GoogleWebClientConfiguration["web:client_secret"],
new { refresh_token= oldResponse.RefreshToken, client_id=Startup.GoogleWebClientConfiguration["web:client_id"],
client_secret=Startup.GoogleWebClientConfiguration["web:client_secret"],
grant_type="refresh_token" }
);
}

@ -11,11 +11,9 @@ namespace Yavsc.Helpers {
public static List<SelectListItem> ActivityItems(
this ApplicationDbContext _dbContext, List<UserActivity> activity)
{
var activities = activity.ToArray();
List<SelectListItem> items = _dbContext.Activities.Select(
x=> new SelectListItem() {
Value = x.Code, Text = x.Name, Selected = activities.Any(a=>a.DoesCode == x.Code)
Value = x.Code, Text = x.Name, Selected = activity.Any(a=>a.DoesCode == x.Code)
} ).ToList();
return items;

@ -22,23 +22,22 @@ namespace Yavsc.Helpers
if (payPalProperties==null) {
payPalProperties = new Dictionary<string,string>();
var paypalSettings = Config.PayPalSettings;
// Don't do:
// payPalProperties.Add("mode", Startup.PayPalSettings.Mode);
// Instead, set the endpoint parameter.
if (paypalSettings.Mode == "production") {
if (Startup.PayPalSettings.Mode == "production") {
// use nvp end point: https://api-3t.paypal.com/nvp
payPalProperties.Add("endpoint", "https://api-3t.paypal.com/nvp");
} else {
payPalProperties.Add("endpoint", "https://api-3t.sandbox.paypal.com/nvp");
}
payPalProperties.Add("clientId", paypalSettings.ClientId);
payPalProperties.Add("clientSecret", paypalSettings.ClientSecret);
payPalProperties.Add("clientId", Startup.PayPalSettings.ClientId);
payPalProperties.Add("clientSecret", Startup.PayPalSettings.ClientSecret);
int numClient = 0;
if (paypalSettings.Accounts!=null)
foreach (var account in paypalSettings.Accounts) {
if (Startup.PayPalSettings.Accounts!=null)
foreach (var account in Startup.PayPalSettings.Accounts) {
numClient++;
payPalProperties.Add ($"account{numClient}.apiUsername",account.ApiUsername);
payPalProperties.Add ($"account{numClient}.apiPassword",account.ApiPassword);
@ -107,8 +106,8 @@ namespace Yavsc.Helpers
var d = new SetExpressCheckoutRequestDetailsType();
logger.LogInformation($"Creating express checkout for {Config.PayPalSettings.MerchantAccountUserName} : "+JsonConvert.SerializeObject(coreq));
var response = PayPalService.SetExpressCheckout( coreq, Config.PayPalSettings.MerchantAccountUserName );
logger.LogInformation($"Creating express checkout for {Startup.PayPalSettings.MerchantAccountUserName} : "+JsonConvert.SerializeObject(coreq));
var response = PayPalService.SetExpressCheckout( coreq, Startup.PayPalSettings.MerchantAccountUserName );
return response;
}
@ -126,7 +125,7 @@ namespace Yavsc.Helpers
Token = token
}
};
return PayPalService.GetExpressCheckoutDetails(req,Config.PayPalSettings.Accounts[0].ApiUsername);
return PayPalService.GetExpressCheckoutDetails(req,Startup.PayPalSettings.Accounts[0].ApiUsername);
}
public static async Task<PaymentInfo> ConfirmPayment(
this ApplicationDbContext context,

@ -122,7 +122,7 @@ namespace Yavsc.Helpers
{
string errorMsg = null;
var billdir = Model.DestDir;
var tempdir = Config.SiteSetup.TempDir;
var tempdir = Startup.SiteSetup.TempDir;
string name = Model.BaseFileName;
string fullname = new FileInfo(
System.IO.Path.Combine(tempdir, name)).FullName;

@ -12,7 +12,7 @@ namespace Yavsc.Helpers
public static string GetUserId(this ClaimsPrincipal user)
{
return user.FindFirstValue("sub");
return user.FindFirstValue(ClaimTypes.NameIdentifier);
}
public static string GetUserName(this ClaimsPrincipal user)

@ -1,358 +0,0 @@
using System.Globalization;
using Duende.IdentityServer;
using Google.Apis.Util.Store;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
using Yavsc.Abstract.Workflow;
using Yavsc.Billing;
using Yavsc.Helpers;
using Yavsc.Interface;
using Yavsc.Models;
using Yavsc.Models.Billing;
using Yavsc.Models.Haircut;
using Yavsc.Models.Workflow;
using Yavsc.Services;
using Yavsc.Settings;
namespace Yavsc;
internal static class HostingExtensions
{
public static IApplicationBuilder ConfigureFileServerApp(this IApplicationBuilder app,
bool enableDirectoryBrowsing = false)
{
var userFilesDirInfo = new DirectoryInfo(Config.SiteSetup.Blog);
AbstractFileSystemHelpers.UserFilesDirName = userFilesDirInfo.FullName;
if (!userFilesDirInfo.Exists) userFilesDirInfo.Create();
Config.UserFilesOptions = new FileServerOptions()
{
FileProvider = new PhysicalFileProvider(AbstractFileSystemHelpers.UserFilesDirName),
RequestPath = PathString.FromUriComponent(Constants.UserFilesPath),
EnableDirectoryBrowsing = enableDirectoryBrowsing,
};
Config.UserFilesOptions.EnableDefaultFiles = true;
Config.UserFilesOptions.StaticFileOptions.ServeUnknownFileTypes = true;
var avatarsDirInfo = new DirectoryInfo(Config.SiteSetup.Avatars);
if (!avatarsDirInfo.Exists) avatarsDirInfo.Create();
Config.AvatarsDirName = avatarsDirInfo.FullName;
Config.AvatarsOptions = new FileServerOptions()
{
FileProvider = new PhysicalFileProvider(Config.AvatarsDirName),
RequestPath = PathString.FromUriComponent(Constants.AvatarsPath),
EnableDirectoryBrowsing = enableDirectoryBrowsing
};
var gitdirinfo = new DirectoryInfo(Config.SiteSetup.GitRepository);
Config.GitDirName = gitdirinfo.FullName;
if (!gitdirinfo.Exists) gitdirinfo.Create();
Config.GitOptions = new FileServerOptions()
{
FileProvider = new PhysicalFileProvider(Config.GitDirName),
RequestPath = PathString.FromUriComponent(Constants.GitPath),
EnableDirectoryBrowsing = enableDirectoryBrowsing,
};
Config.GitOptions.DefaultFilesOptions.DefaultFileNames.Add("index.md");
Config.GitOptions.StaticFileOptions.ServeUnknownFileTypes = true;
app.UseFileServer(Config.UserFilesOptions);
app.UseFileServer(Config.AvatarsOptions);
app.UseFileServer(Config.GitOptions);
app.UseStaticFiles();
return app;
}
public static void ConfigureWorkflow()
{
foreach (var a in System.AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var c in a.GetTypes())
{
if (c.IsClass && !c.IsAbstract &&
c.GetInterface("ISpecializationSettings") != null)
{
Config.ProfileTypes.Add(c);
}
}
}
foreach (var propertyInfo in typeof(ApplicationDbContext).GetProperties())
{
foreach (var attr in propertyInfo.CustomAttributes)
{
// something like a DbSet?
if (typeof(Yavsc.Attributes.ActivitySettingsAttribute).IsAssignableFrom(attr.AttributeType))
{
BillingService.UserSettings.Add(propertyInfo);
}
}
}
RegisterBilling<HairCutQuery>(BillingCodes.Brush, new Func<ApplicationDbContext,long,IDecidableQuery>
( ( db, id) =>
{
var query = db.HairCutQueries.Include(q=>q.Prestation).Include(q=>q.Regularisation).Single(q=>q.Id == id) ;
query.SelectedProfile = db.BrusherProfile.Single(b=>b.UserId == query.PerformerId);
return query;
})) ;
RegisterBilling<HairMultiCutQuery>(BillingCodes.MBrush,new Func<ApplicationDbContext,long,IDecidableQuery>
( (db, id) => db.HairMultiCutQueries.Include(q=>q.Regularisation).Single(q=>q.Id == id)));
RegisterBilling<RdvQuery>(BillingCodes.Rdv, new Func<ApplicationDbContext,long,IDecidableQuery>
( (db, id) => db.RdvQueries.Include(q=>q.Regularisation).Single(q=>q.Id == id)));
}
public static void RegisterBilling<T>(string code, Func<ApplicationDbContext,long,IDecidableQuery> getter) where T : IBillable
{
BillingService.Billing.Add(code,getter) ;
BillingService.GlobalBillingMap.Add(typeof(T).Name,code);
}
public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
{
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
IConfigurationRoot configuration = configurationBuilder.Build();
string? googleClientFile = configuration["Authentication:Google:GoogleWebClientJson"];
string? googleServiceAccountJsonFile = configuration["Authentication:Google:GoogleServiceAccountJson"];
if (googleClientFile != null)
{
Config.GoogleWebClientConfiguration = new ConfigurationBuilder().AddJsonFile(googleClientFile).Build();
}
if (googleServiceAccountJsonFile != null)
{
FileInfo safile = new FileInfo(googleServiceAccountJsonFile);
Config.GServiceAccount = JsonConvert.DeserializeObject<GoogleServiceAccount>(safile.OpenText().ReadToEnd());
}
var services = builder.Services;
services.AddRazorPages();
services.AddSignalR(o =>
{
o.EnableDetailedErrors = true;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services
.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://docs.duendesoftware.com/identityserver/v6/fundamentals/resources/
options.EmitStaticAudienceClaim = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddAspNetIdentity<ApplicationUser>().AddServerSideSessions();
services.AddAuthentication()
.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to https://localhost:5001/signin-google
options.ClientId = "325408689282-6bekh7p3guj4k0f3301a6frf025cnrk1.apps.googleusercontent.com";
options.ClientSecret = "XV1DLrq8cQE2JI4gZP3h6d8y";
});
services.Configure<RequestLocalizationOptions>(options =>
{
CultureInfo[] supportedCultures = new[]
{
new CultureInfo("en"),
new CultureInfo("fr"),
new CultureInfo("pt")
};
CultureInfo[] supportedUICultures = new[]
{
new CultureInfo("fr"),
new CultureInfo("en"),
new CultureInfo("pt")
};
// You must explicitly state which cultures your application supports.
// These are the cultures the app supports for formatting numbers, dates, etc.
options.SupportedCultures = supportedCultures;
// These are the cultures the app supports for UI strings, i.e. we have localized resources for.
options.SupportedUICultures = supportedUICultures;
options.RequestCultureProviders = new List<IRequestCultureProvider>
{
new QueryStringRequestCultureProvider { Options = options },
new CookieRequestCultureProvider { Options = options, CookieName="ASPNET_CULTURE" },
new AcceptLanguageHeaderRequestCultureProvider { Options = options }
};
});
services.AddOptions();
_ = services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder =>
{
_ = builder.WithOrigins("*");
});
});
// Add the system clock service
_ = services.AddSingleton<ISystemClock, SystemClock>();
_ = services.AddSingleton<IConnexionManager, HubConnectionManager>();
_ = services.AddSingleton<ILiveProcessor, LiveProcessor>();
_ = services.AddTransient<IFileSystemAuthManager, FileSystemAuthManager>();
services.AddMvc(config =>
{
/* var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy)); */
config.Filters.Add(new ProducesAttribute("application/json"));
// config.ModelBinders.Insert(0,new MyDateTimeModelBinder());
// config.ModelBinders.Insert(0,new MyDecimalModelBinder());
config.EnableEndpointRouting = true;
}).AddFormatterMappings(
config => config.SetMediaTypeMappingForFormat("text/pdf",
new MediaTypeHeaderValue("text/pdf"))
).AddFormatterMappings(
config => config.SetMediaTypeMappingForFormat("text/x-tex",
new MediaTypeHeaderValue("text/x-tex"))
)
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix,
options =>
{
options.ResourcesPath = "Resources";
}).AddDataAnnotationsLocalization();
_ = services.AddTransient<ITrueEmailSender, MailSender>();
_ = services.AddTransient<Microsoft.AspNetCore.Identity.UI.Services.IEmailSender, MailSender>();
_ = services.AddTransient<IYavscMessageSender, YavscMessageSender>();
_ = services.AddTransient<IBillingService, BillingService>();
_ = services.AddTransient<IDataStore, FileDataStore>((sp) => new FileDataStore("googledatastore", false));
_ = services.AddTransient<ICalendarManager, CalendarManager>();
// TODO for SMS: services.AddTransient<ISmsSender, AuthMessageSender>();
_ = services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
var dataDir = new DirectoryInfo(configuration["Site:DataDir"]);
// Add session related services.
services.AddDataProtection().PersistKeysToFileSystem(dataDir);
services.AddAuthorization(options =>
{
options.AddPolicy("AdministratorOnly", policy =>
{
_ = policy.RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Constants.AdminGroupName);
});
options.AddPolicy("FrontOffice", policy => policy.RequireRole(Constants.FrontOfficeGroupName));
options.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("Bearer")
.RequireAuthenticatedUser().Build());
// options.AddPolicy("EmployeeId", policy => policy.RequireClaim("EmployeeId", "123", "456"));
// options.AddPolicy("BuildingEntry", policy => policy.Requirements.Add(new OfficeEntryRequirement()));
options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
});
_ = services.AddControllersWithViews()
.AddNewtonsoftJson();
LoadGoogleConfig(configuration);
return builder.Build();
}
public static WebApplication ConfigurePipeline(this WebApplication app)
{
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
//app.UseSignalR();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages()
.RequireAuthorization();
app.MapHub<ChatHub>("/chatHub");
app.MapAreaControllerRoute("api", "api", "~/api/{controller}/{action}/{id?}");
ConfigureWorkflow();
var services = app.Services;
ILoggerFactory loggerFactory = services.GetRequiredService<ILoggerFactory>();
var siteSettings = services.GetRequiredService<IOptions<SiteSettings>>();
var smtpSettings = services.GetRequiredService<IOptions<SmtpSettings>>();
var payPalSettings = services.GetRequiredService<IOptions<PayPalSettings>>();
var googleAuthSettings = services.GetRequiredService<IOptions<GoogleAuthSettings>>();
var authorizationService = services.GetRequiredService<IAuthorizationService>();
var localization = services.GetRequiredService<IStringLocalizer<YavscLocalization>>();
Startup.Configure(app, siteSettings, smtpSettings, authorizationService,
payPalSettings, googleAuthSettings, localization, loggerFactory,
app.Environment.EnvironmentName );
app.ConfigureFileServerApp();
return app;
}
static void LoadGoogleConfig(IConfigurationRoot configuration)
{
string? googleClientFile = configuration["Authentication:Google:GoogleWebClientJson"];
string? googleServiceAccountJsonFile = configuration["Authentication:Google:GoogleServiceAccountJson"];
if (googleClientFile != null)
{
Config.GoogleWebClientConfiguration = new ConfigurationBuilder().AddJsonFile(googleClientFile).Build();
}
if (googleServiceAccountJsonFile != null)
{
FileInfo safile = new FileInfo(googleServiceAccountJsonFile);
Config.GServiceAccount = JsonConvert.DeserializeObject<GoogleServiceAccount>(safile.OpenText().ReadToEnd());
}
}
}

@ -44,15 +44,17 @@ namespace Yavsc
public HubInputValidator InputValidator { get; }
public ChatHub(ApplicationDbContext dbContext,
ILoggerFactory loggerFactory,
IStringLocalizerFactory stringLocalizerFactory,
IConnexionManager connexionManager)
public ChatHub()
{
_dbContext = dbContext;
_localizer = stringLocalizerFactory.Create(typeof(ChatHub));
var scope = Startup.Services.GetRequiredService<IServiceScopeFactory>().CreateScope();
_cxManager = connexionManager;
_dbContext = scope.ServiceProvider.GetService<ApplicationDbContext>();
var loggerFactory = scope.ServiceProvider.GetService<ILoggerFactory>();
var stringLocFactory = scope.ServiceProvider.GetService<IStringLocalizerFactory>();
_localizer = stringLocFactory.Create(typeof(ChatHub));
_cxManager = scope.ServiceProvider.GetService<IConnexionManager>();
_cxManager.SetErrorHandler ((context, error) =>
{
NotifyUser(NotificationTypes.Error, context, error);
@ -68,7 +70,7 @@ namespace Yavsc
public override async Task OnConnectedAsync()
{
bool isAuth = Context.User?.Identity?.IsAuthenticated ?? false;
bool isAuth = (Context.User != null);
bool isCop = false;
string userName = setUserName();
if (isAuth)
@ -80,7 +82,7 @@ namespace Yavsc
await Groups.AddToGroupAsync(Context.ConnectionId, group);
_logger.LogInformation(_localizer.GetString(ChatHubConstants.LabAuthChatUser));
var userId = _dbContext.Users.First(u => u.UserName == Context.User.Identity.Name).Id;
var userId = _dbContext.Users.First(u => u.UserName == userName).Id;
await Clients.Group(ChatHubConstants.HubGroupFollowingPrefix + userId).SendAsync("notifyUser", NotificationTypes.Connected, userName, null);
isCop = Context.User.IsInRole(Constants.AdminGroupName) ;
@ -111,6 +113,8 @@ namespace Yavsc
return Context.User.Identity.Name;
}
anonymousSequence++;
var aname = $"{ChatHubConstants.AnonymousUserNamePrefix}{queryUname}{anonymousSequence}";
SetUserName(Context.ConnectionId, aname);
return aname;
@ -178,7 +182,6 @@ namespace Yavsc
}
_logger.LogInformation($"returning chan info");
await Clients.Caller.SendAsync("joint", chanInfo);
return chanInfo;
}
@ -312,9 +315,7 @@ namespace Yavsc
NotifyUserInRoom(NotificationTypes.Error, roomName, notSentMsg);
return;
}
var group = Clients.Group(groupname);
var msg = new { Name = userName, Room = roomName, Message = message};
await group.SendAsync("ReceiveMessage", msg);
await Clients.Group(groupname).SendAsync("addMessage", userName, roomName, message);
}
async Task NotifyUser(string type, string targetId, string message)
@ -386,5 +387,6 @@ namespace Yavsc
await cli.SendAsync("addStreamInfo", sender, streamId, message);
}
}
}

@ -1,30 +1,80 @@
DESTDIR=/srv/www/yavsc
DESTDIR=/srv/www/yavscpre
PRODDESTDIR=/srv/www/yavsc
HOSTING=localhost
ASPNET_LOG_LEVEL=debug
SOURCE_DIR=../..
USER=www-data
HOSTING_PRE=yavsv.pschneider.fr
CONFIGURATION=Debug
SERVICE_PRE=Yavsc
MAKEFILE_DIR=$(SOURCE_DIR)/scripts/make
BASERESX= Resources/Yavsc.Models.IT.Fixing.Resources.resx \
Resources/Yavsc.ViewComponents.CommentViewComponent.resx \
Resources/Yavsc.ViewModels.FrontOffice.PerformerProfileViewModel.resx \
Resources/Yavsc.ViewModels.EnrolerViewModel.resx \
Resources/Yavsc.YavscLocalisation.resx
BASERESXGEN=$(BASERESX:.resx=.Designer.cs)
MCS_OPTIONS=-debug
MONO_OPTIONS=--debug
all:
dotnet build
include $(MAKEFILE_DIR)/dnx.mk
include $(MAKEFILE_DIR)/versioning.mk
MINJS=wwwroot/js/bootstrap.min.js wwwroot/js/input-lib.min.js wwwroot/js/jquery.signalR-2.2.1.min.js wwwroot/js/quill.min.js wwwroot/js/chat.min.js wwwroot/js/jquery-2.2.4.min.js wwwroot/js/jquery-ui.min.js wwwroot/js/showdown.min.js wwwroot/js/comment.min.js wwwroot/js/jquery-3.2.1.min.js wwwroot/js/md-helpers.min.js wwwroot/js/site.min.js wwwroot/js/dropzone.min.js wwwroot/js/jquery-migrate-3.0.0.min.js wwwroot/js/parallax.min.js wwwroot/js/str.min.js wwwroot/js/google-geoloc.min.js wwwroot/js/jquery.min.js wwwroot/js/paypalbutton.min.js wwwroot/js/to-markdown.min.js
MINCSS=wwwroot/css/coiffure.min.css wwwroot/css/dev.min.css wwwroot/css/freespeech.min.css wwwroot/css/yavsc.min.css wwwroot/css/default.min.css wwwroot/css/font-awesome.min.css wwwroot/css/lua.min.css wwwroot/css/yavscpre.min.css wwwroot/css/clear/site.min.css wwwroot/css/main/bootstrap.min.css wwwroot/css/main/jquery-ui.min.css wwwroot/css/main/site.min.css wwwroot/css/dark/site.min.css wwwroot/css/main/dropzone.min.css wwwroot/css/main/quill.snow.min.css
web: project.lock.json
MCS_OPTIONS=$(MCS_OPTIONS) MONO_OPTIONS=$(MONO_OPTIONS) ASPNET_LOG_LEVEL=$(ASPNET_LOG_LEVEL) ASPNET_ENV=$(ASPNET_ENV) dnx web --configuration=$(CONFIGURATION) |tee web.log
start_debug:
ASPNET_ENV=Development mono --debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:4669 /home/paul/.dnx/runtimes/dnx-mono.1.0.0-rc1-update2/bin/Microsoft.Dnx.Host.Mono.dll web
nweb:
MONO_PATH=$(MONO_PATH):~/.dnx/runtimes/dnx-mono.1.0.0-rc1-update2/bin ~/.dnx/runtimes/dnx-mono.1.0.0-rc1-update2/bin/ndnx web
showConfig:
@echo HOSTING_PRE: $(USER)@$(HOSTING_PRE)
@echo HOSTING: $(USER)@$(HOSTING)
@echo ENV: $(ASPNET_ENV)
@echo CONFIGURATION: $(CONFIGURATION)
@echo DESTDIR: $(DESTDIR)
@echo SERVICE_PRE: $(SERVICE_PRE)
@echo PRODDESTDIR: $(PRODDESTDIR)
status:
ifeq ($(git_status),0)
@echo Nothing to be done.
else
@echo There are pending changes:
@git status
endif
publish:
ASPNETCORE_ENV=$(CONFIGURATION) dotnet publish
ASPNET_ENV=$(ASPNET_ENV) dnu publish
deploy-pkg: publish
pushInPre: publish
sudo service $(SERVICE_PRE) stop
sudo cp -a bin/Debug/net7.0/publish/* $(DESTDIR)
sudo chown -R $(USER) $(DESTDIR)
deploy: pushInPre pushInProd
pushInPre: cleanoutput bin/output/wwwroot/version ../../yavscd-pre
sudo service kestrel-pre stop
sudo rm -rf $(DESTDIR)/approot
sudo cp -a bin/output/* $(DESTDIR)
sudo cp ../../yavscd /usr/local/bin/yavscd-pre
sudo sync
sudo service $(SERVICE_PRE) start
sudo service kestrel-pre start
pushInProd: cleanoutput bin/output/wwwroot/version ../../yavscd
ifeq ($(git_status),0)
sudo service kestrel stop
sudo rm -rf $(PRODDESTDIR)/approot
sudo cp -a bin/output/* $(PRODDESTDIR)
sudo cp ../../yavscd /usr/local/bin/yavscd
sudo sync
sudo service kestrel start
else
$(error EPRODANDGITSTATUS! Refus de pousser en production: des changements doivent être validés auprès du contrôle de versions.)
endif
cleanPublish: pushInPre pushInProd
rm -rf bin/output/
%.min.js: %.js
jsmin < $^ > $@
@ -34,3 +84,26 @@ pushInPre: publish
%.Designer.cs: %.resx
strongresbuildercli -l -p -t -r "Yavsc.Resources." $^
prepare_code: $(BASERESXGEN)
web_rebuild:
make -C ../Yavsc.Abstract
make -C ../Yavsc.Server
make bin/Debug/dnx451/Yavsc.dll
minJs: $(MINJS)
minCss: $(MINCSS)
minify: minCss minJs
prepare_mvc_templates: $(SOURCE_DIR)/scripts/configure/tools/mvc-code-generators/new-templates/ViewGenerator/Create.cshtml \
$(SOURCE_DIR)/scripts/configure/tools/mvc-code-generators/new-templates/ViewGenerator/Delete.cshtml \
$(SOURCE_DIR)/scripts/configure/tools/mvc-code-generators/new-templates/ViewGenerator/Edit.cshtml \
$(SOURCE_DIR)/scripts/configure/tools/mvc-code-generators/new-templates/ViewGenerator/List.cshtml \
$(SOURCE_DIR)/scripts/configure/tools/mvc-code-generators/new-templates/ViewGenerator/Details.cshtml
cp $^ $(SOURCE_DIR)/packages/Microsoft.Extensions.CodeGenerators.Mvc/1.0.0-rc1-final/Templates/ViewGenerator/
$(BINTARGETPATH): $(BASERESXGEN) project.lock.json

@ -42,14 +42,6 @@ namespace Yavsc.Models
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
{
}
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
@ -97,7 +89,15 @@ namespace Yavsc.Models
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!string.IsNullOrWhiteSpace(Startup.ConnectionString))
{
optionsBuilder.UseNpgsql(Startup.ConnectionString);
return;
}
var appSetup = (string) AppDomain.CurrentDomain.GetData(Constants.YavscConnectionStringEnvName);
if (!string.IsNullOrWhiteSpace(appSetup))
{
optionsBuilder.UseNpgsql(appSetup);

@ -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,17 +0,0 @@
@page
@model Yavsc.Pages.Logout.Index
<div class="logout-page">
<div class="lead">
<h1>Logout</h1>
<p>Would you like to logout of IdentityServer?</p>
</div>
<form asp-page="/Account/Logout/Index">
<input type="hidden" name="logoutId" value="@Model.LogoutId" />
<div class="form-group">
<button class="btn btn-primary">Yes</button>
</div>
</form>
</div>

@ -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,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,107 +0,0 @@
@page
@model Yavsc.Pages.Consent.Index
@{
}
<div class="page-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>
<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="/Consent/Index">
<input type="hidden" asp-for="Input.ReturnUrl" />
<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,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,12 +0,0 @@
@page
@model Yavsc.Pages.Device.SuccessModel
@{
}
<div class="page-device-success">
<div class="lead">
<h1>Success</h1>
<p>You have successfully authorized the device</p>
</div>
</div>

@ -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,35 +0,0 @@
@using Yavsc.Pages.Device
@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>
}
</li>

@ -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,90 +0,0 @@
@page
@model Yavsc.Pages.Grants.Index
@{
}
<div class="grants-page">
<div class="lead">
<h1>Client Application Permissions</h1>
<p>Below is the list of applications you have given permission to and the resources they have access to.</p>
</div>
@if (!Model.View.Grants.Any())
{
<div class="row">
<div class="col-sm-8">
<div class="alert alert-info">
You have not given access to any applications
</div>
</div>
</div>
}
else
{
foreach (var grant in Model.View.Grants)
{
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-sm-8 card-title">
@if (grant.ClientLogoUrl != null)
{
<img src="@grant.ClientLogoUrl">
}
<strong>@grant.ClientName</strong>
</div>
<div class="col-sm-2">
<form asp-page="/Grants/Index">
<input type="hidden" name="clientId" value="@grant.ClientId">
<button class="btn btn-danger">Revoke Access</button>
</form>
</div>
</div>
</div>
<ul class="list-group list-group-flush">
@if (grant.Description != null)
{
<li class="list-group-item">
<label>Description:</label> @grant.Description
</li>
}
<li class="list-group-item">
<label>Created:</label> @grant.Created.ToString("yyyy-MM-dd")
</li>
@if (grant.Expires.HasValue)
{
<li class="list-group-item">
<label>Expires:</label> @grant.Expires.Value.ToString("yyyy-MM-dd")
</li>
}
@if (grant.IdentityGrantNames.Any())
{
<li class="list-group-item">
<label>Identity Grants</label>
<ul>
@foreach (var name in grant.IdentityGrantNames)
{
<li>@name</li>
}
</ul>
</li>
}
@if (grant.ApiGrantNames.Any())
{
<li class="list-group-item">
<label>API Grants</label>
<ul>
@foreach (var name in grant.ApiGrantNames)
{
<li>@name</li>
}
</ul>
</li>
}
</ul>
</div>
}
}
</div>

@ -1,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,20 +0,0 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Models;
namespace Yavsc.Pages.Error;
public class ViewModel
{
public ViewModel()
{
}
public ViewModel(string error)
{
Error = new ErrorMessage { Error = error };
}
public ErrorMessage? Error { get; set; }
}

@ -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;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…