Add simple authentication feature

This commit is contained in:
khanhna
2021-02-26 20:49:34 +07:00
parent 195c58bbbc
commit 965148bdd6
20 changed files with 443 additions and 91 deletions

View File

@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SilkierQuartz.Example
namespace SilkierQuartz.Example
{
public class AppSettings
{

View File

@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace SilkierQuartz.Example.Pages

View File

@@ -22,7 +22,7 @@
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" href= "/SilkierQuartz">SilkierQuartz</a>
<a class="nav-link text-dark" href= "/SilkierQuartz/Authenticate/Login">SilkierQuartz</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>

View File

@@ -1,15 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SilkierQuartz.Example.Jobs;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Quartz;
using SilkierQuartz.Example.Jobs;
using System.Collections.Generic;
namespace SilkierQuartz.Example
{
@@ -50,6 +46,8 @@ namespace SilkierQuartz.Example
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.AddSilkierQuartzAuthentication();
app.UseAuthorization();
app.UseSilkierQuartz(
new SilkierQuartzOptions()
@@ -61,9 +59,13 @@ namespace SilkierQuartz.Example
CronExpressionOptions = new CronExpressionDescriptor.Options()
{
DayOfWeekStartIndexZero = false //Quartz uses 1-7 as the range
}
},
AccountName = "admin",
AccountPassword = "password",
IsAuthenticationPersist = false
}
);
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();

View File

@@ -1,14 +1,11 @@

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Quartz;
using Quartz.Impl;
using SilkierQuartz;
using SilkierQuartz.Middlewares;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
@@ -90,6 +87,14 @@ namespace SilkierQuartz
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(nameof(SilkierQuartz), $"{options.VirtualPathRoot}/{{controller=Scheduler}}/{{action=Index}}");
SilkierQuartzAuthenticateConfig.VirtualPathRoot = options.VirtualPathRoot;
SilkierQuartzAuthenticateConfig.VirtualPathRootUrlEncode = options.VirtualPathRoot.Replace("/", "%2F");
SilkierQuartzAuthenticateConfig.UserName = options.AccountName;
SilkierQuartzAuthenticateConfig.UserPassword = options.AccountPassword;
SilkierQuartzAuthenticateConfig.IsPersist = options.IsAuthenticationPersist;
endpoints.MapControllerRoute($"{nameof(SilkierQuartz)}Authenticate",
$"{options.VirtualPathRoot}{{controller=Authenticate}}/{{action=Login}}");
});
var types = GetSilkierQuartzJobs();
@@ -165,6 +170,31 @@ namespace SilkierQuartz
services.AddControllers()
.AddApplicationPart(Assembly.GetExecutingAssembly())
.AddNewtonsoftJson();
services.AddAuthentication(SilkierQuartzAuthenticateConfig.AuthScheme).AddCookie(
SilkierQuartzAuthenticateConfig.AuthScheme,
cfg =>
{
cfg.Cookie.Name = SilkierQuartzAuthenticateConfig.AuthScheme;
cfg.LoginPath = $"{SilkierQuartzAuthenticateConfig.VirtualPathRoot}/Authenticate/Login";
cfg.AccessDeniedPath = $"{SilkierQuartzAuthenticateConfig.VirtualPathRoot}/Authenticate/Login";
if (SilkierQuartzAuthenticateConfig.IsPersist)
{
cfg.ExpireTimeSpan = TimeSpan.FromDays(7);
cfg.SlidingExpiration = true;
}
});
services.AddAuthorization(opts =>
{
opts.AddPolicy(SilkierQuartzAuthenticateConfig.AuthScheme, authBuilder =>
{
authBuilder.RequireAuthenticatedUser();
authBuilder.RequireClaim(SilkierQuartzAuthenticateConfig.SilkierQuartzSpecificClaim,
SilkierQuartzAuthenticateConfig.SilkierQuartzSpecificClaimValue);
});
});
services.UseQuartzHostedService(stdSchedulerFactoryOptions);
var types = GetSilkierQuartzJobs(jobsasmlist?.Invoke());
@@ -200,6 +230,21 @@ namespace SilkierQuartz
}
return _silkierQuartzJobs;
}
/// <summary>
/// Adds the <see cref="SilkierQuartzAuthenticationMiddleware"/> to the specified <see cref="IApplicationBuilder"/>, which enables simple authentication for silkier Quartz.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IApplicationBuilder AddSilkierQuartzAuthentication(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
return app.UseMiddleware<SilkierQuartzAuthenticationMiddleware>();
}
}
}

View File

@@ -0,0 +1,110 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SilkierQuartz.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Claims;
using System.Threading.Tasks;
namespace SilkierQuartz.Controllers
{
[AllowAnonymous]
public class AuthenticateController : PageControllerBase
{
[HttpGet]
public async Task<IActionResult> Login([FromServices] IAuthenticationSchemeProvider schemes)
{
var silkierScheme = await schemes.GetSchemeAsync(SilkierQuartzAuthenticateConfig.AuthScheme);
if (string.IsNullOrEmpty(SilkierQuartzAuthenticateConfig.UserName) ||
string.IsNullOrEmpty(SilkierQuartzAuthenticateConfig.UserPassword))
{
foreach (var userClaim in HttpContext.User.Claims)
{
Debug.WriteLine($"{userClaim.Type} - {userClaim.Value}");
}
if (HttpContext.User == null || !HttpContext.User.Identity.IsAuthenticated ||
!HttpContext.User.HasClaim(SilkierQuartzAuthenticateConfig.SilkierQuartzSpecificClaim,
SilkierQuartzAuthenticateConfig.SilkierQuartzSpecificClaimValue))
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, string.IsNullOrEmpty(SilkierQuartzAuthenticateConfig.UserName) ? "SilkierQuartzAdmin" : SilkierQuartzAuthenticateConfig.UserName ),
new Claim(ClaimTypes.Name, string.IsNullOrEmpty(SilkierQuartzAuthenticateConfig.UserPassword) ? "SilkierQuartzPassword" : SilkierQuartzAuthenticateConfig.UserPassword),
new Claim(SilkierQuartzAuthenticateConfig.SilkierQuartzSpecificClaim, SilkierQuartzAuthenticateConfig.SilkierQuartzSpecificClaimValue)
};
var authProperties = new AuthenticationProperties()
{
IsPersistent = SilkierQuartzAuthenticateConfig.IsPersist
};
var userIdentity = new ClaimsIdentity(claims, SilkierQuartzAuthenticateConfig.AuthScheme);
await HttpContext.SignInAsync(SilkierQuartzAuthenticateConfig.AuthScheme, new ClaimsPrincipal(userIdentity),
authProperties);
return RedirectToAction(nameof(SchedulerController.Index), nameof(Scheduler));
}
else
{
return RedirectToAction(nameof(SchedulerController.Index), nameof(Scheduler));
}
}
else
{
if (HttpContext.User == null || !HttpContext.User.Identity.IsAuthenticated ||
!HttpContext.User.HasClaim(SilkierQuartzAuthenticateConfig.SilkierQuartzSpecificClaim, "Authorized"))
{
ViewBag.IsLoginError = false;
return View(new AuthenticateViewModel());
}
else
{
return RedirectToAction(nameof(SchedulerController.Index), nameof(Scheduler));
}
}
}
[HttpPost]
public async Task<IActionResult> Login(AuthenticateViewModel request)
{
if (string.Compare(request.UserName, SilkierQuartzAuthenticateConfig.UserName,
StringComparison.InvariantCulture) != 0 ||
string.Compare(request.Password, SilkierQuartzAuthenticateConfig.UserPassword,
StringComparison.InvariantCulture) != 0)
{
request.IsLoginError = true;
return View(request);
}
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, string.IsNullOrEmpty(SilkierQuartzAuthenticateConfig.UserName) ? "SilkierQuartzAdmin" : SilkierQuartzAuthenticateConfig.UserName ),
new Claim(ClaimTypes.Name, string.IsNullOrEmpty(SilkierQuartzAuthenticateConfig.UserPassword) ? "SilkierQuartzPassword" : SilkierQuartzAuthenticateConfig.UserPassword),
new Claim(SilkierQuartzAuthenticateConfig.SilkierQuartzSpecificClaim, "Authorized")
};
var authProperties = new AuthenticationProperties()
{
IsPersistent = request.IsPersist
};
var userIdentity = new ClaimsIdentity(claims, SilkierQuartzAuthenticateConfig.AuthScheme);
await HttpContext.SignInAsync(SilkierQuartzAuthenticateConfig.AuthScheme, new ClaimsPrincipal(userIdentity),
authProperties);
return RedirectToAction(nameof(SchedulerController.Index), nameof(Scheduler));
}
[HttpGet]
[Authorize(SilkierQuartzAuthenticateConfig.AuthScheme)]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(SilkierQuartzAuthenticateConfig.AuthScheme);
return RedirectToAction(nameof(Login));
}
}
}

View File

@@ -1,14 +1,16 @@
using Quartz;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Quartz;
using SilkierQuartz.Helpers;
using SilkierQuartz.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace SilkierQuartz.Controllers
{
[Authorize(SilkierQuartzAuthenticateConfig.AuthScheme)]
public class CalendarsController : PageControllerBase
{
[HttpGet]

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SilkierQuartz.Helpers;
using System;
using System.Collections.Generic;
@@ -6,6 +7,7 @@ using System.Threading.Tasks;
namespace SilkierQuartz.Controllers
{
[Authorize(SilkierQuartzAuthenticateConfig.AuthScheme)]
public class ExecutionsController : PageControllerBase
{
[HttpGet]

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Quartz.Plugins.RecentHistory;
using System;
using System.Collections.Generic;
@@ -7,6 +8,7 @@ using System.Threading.Tasks;
namespace SilkierQuartz.Controllers
{
[Authorize(SilkierQuartzAuthenticateConfig.AuthScheme)]
public class HistoryController : PageControllerBase
{
[HttpGet]

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SilkierQuartz.Helpers;
@@ -9,6 +10,7 @@ using System.Threading.Tasks;
namespace SilkierQuartz.Controllers
{
[Authorize(SilkierQuartzAuthenticateConfig.AuthScheme)]
public class JobDataMapController : PageControllerBase
{
[HttpPost, JsonErrorResponse]

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Quartz;
using Quartz.Impl.Matchers;
using Quartz.Plugins.RecentHistory;
@@ -11,6 +12,7 @@ using System.Threading.Tasks;
namespace SilkierQuartz.Controllers
{
[Authorize(SilkierQuartzAuthenticateConfig.AuthScheme)]
public class JobsController : PageControllerBase
{
[HttpGet]

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
@@ -14,6 +15,7 @@ using System.Text.Json;
namespace SilkierQuartz.Controllers
{
[Authorize(SilkierQuartzAuthenticateConfig.AuthScheme)]
public abstract partial class PageControllerBase : ControllerBase
{
private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings()

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Quartz;
using Quartz.Impl.Matchers;
using Quartz.Plugins.RecentHistory;
@@ -12,6 +13,7 @@ using System.Threading.Tasks;
namespace SilkierQuartz.Controllers
{
[Authorize(SilkierQuartzAuthenticateConfig.AuthScheme)]
public class SchedulerController : PageControllerBase
{
[HttpGet]

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Quartz;
using Quartz.Impl.Matchers;
using Quartz.Plugins.RecentHistory;
@@ -11,6 +12,7 @@ using System.Threading.Tasks;
namespace SilkierQuartz.Controllers
{
[Authorize(SilkierQuartzAuthenticateConfig.AuthScheme)]
public class TriggersController : PageControllerBase
{
[HttpGet]

View File

@@ -0,0 +1,108 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
namespace SilkierQuartz.Middlewares
{
/// <summary>
/// Middleware that performs authentication.
/// </summary>
public class SilkierQuartzAuthenticationMiddleware
{
private readonly RequestDelegate _next;
/// <summary>
/// Initializes a new instance of <see cref="Microsoft.AspNetCore.Authentication.AuthenticationMiddleware"/>.
/// </summary>
/// <param name="next">The next item in the middleware pipeline.</param>
/// <param name="schemes">The <see cref="IAuthenticationSchemeProvider"/>.</param>
public SilkierQuartzAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
}
/// <summary>
/// Gets or sets the <see cref="IAuthenticationSchemeProvider"/>.
/// </summary>
public IAuthenticationSchemeProvider Schemes { get; set; }
/// <summary>
/// Invokes the middleware performing authentication.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
public async Task Invoke(HttpContext context)
{
var relativePath = GetRelativeUrlPath(context);
if (relativePath.StartsWith(SilkierQuartzAuthenticateConfig.VirtualPathRoot) ||
relativePath.StartsWith("?ReturnUrl") &&
relativePath.Contains(SilkierQuartzAuthenticateConfig.VirtualPathRootUrlEncode))
{
await DetailProcess(context, SilkierQuartzAuthenticateConfig.AuthScheme);
}
await _next(context);
}
public async Task DetailProcess(HttpContext httpContext, string authSchemeName = null)
{
httpContext.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
OriginalPath = httpContext.Request.Path,
OriginalPathBase = httpContext.Request.PathBase
});
// Give any IAuthenticationRequestHandler schemes a chance to handle the request
var handlers = httpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
if (await handlers.GetHandlerAsync(httpContext, scheme.Name) is IAuthenticationRequestHandler handler &&
await handler.HandleRequestAsync())
{
return;
}
}
var authScheme = string.IsNullOrEmpty(authSchemeName)
? await Schemes.GetDefaultAuthenticateSchemeAsync()
: await Schemes.GetSchemeAsync(authSchemeName);
if (authScheme != null)
{
var result = await httpContext.AuthenticateAsync(authScheme.Name);
if (result.Principal == null || !result.Principal.HasClaim(SilkierQuartzAuthenticateConfig.SilkierQuartzSpecificClaim,
SilkierQuartzAuthenticateConfig.SilkierQuartzSpecificClaimValue))
{
return;
}
if (result?.Principal != null)
{
httpContext.User = result.Principal;
}
}
}
public string GetRelativeUrlPath(HttpContext httpContext)
{
/*
In some cases, like when running integration tests with WebApplicationFactory<T>
the RawTarget returns an empty string instead of null, in that case we can't use
?? as fallback.
*/
if (httpContext == null)
{
return string.Empty;
}
var requestPath = httpContext.Features.Get<IHttpRequestFeature>()?.RawTarget;
if (string.IsNullOrEmpty(requestPath))
{
requestPath = httpContext.Request.Path.ToString();
}
return requestPath;
}
}
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace SilkierQuartz.Models
{
public class AuthenticateViewModel
{
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
public bool IsPersist { get; set; }
public bool IsLoginError { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
namespace SilkierQuartz
{
internal class SilkierQuartzAuthenticateConfig
{
internal static string VirtualPathRoot = string.Empty;
internal static string VirtualPathRootUrlEncode = string.Empty;
internal static string UserName;
internal static string UserPassword;
internal static bool IsPersist;
internal const string AuthScheme = "SilkierQuartzAuth";
internal const string SilkierQuartzSpecificClaim = "SilkierQuartzManage";
internal const string SilkierQuartzSpecificClaimValue = "Authorized";
}
}

View File

@@ -23,6 +23,12 @@ namespace SilkierQuartz
public IScheduler Scheduler { get; set; }
public string AccountName { get; set; }
public string AccountPassword { get; set; }
public bool IsAuthenticationPersist { get; set; }
/// <summary>
/// Supported value types in job data map.
/// </summary>

View File

@@ -0,0 +1,44 @@
{{!<Layout}}
{{ViewBag Title='Login'}}
<div class="ui inverted page dimmer" id="dimmer"><div class="ui loader"></div></div>
<form class="ui form" method="post" enctype="multipart/form-data">
<div class="ui clearing basic segment" style="padding: 0px" id="header">
<h1 class="ui left floated header">
Login
</h1>
</div>
{{#with Model}}
<div class="ui segment" style="width: 700px; height: 250px;">
<div>
<div class="field accept-error">
<label>Name</label>
<input type="text" placeholder="User Name" value="{{UserName}}" name="userName" />
</div>
<div class="field accept-error">
<label>Password</label>
<input type="password" placeholder="User Name" value="{{Password}}" name="password" />
</div>
<div class="field accept-error">
<div class="ui checkbox">
<input type="checkbox" value="False" {{IsPersist}} />
<label>Remember Me</label>
</div>
</div>
</div>
<div style="float: left; margin-top: 15px;">
<div class="ui buttons">
<button type="submit" class="ui primary button">Login</button>
</div>
</div>
</div>
{{#if IsLoginError}}
<div class="ui negative message">
<p>User Name or Password is incorrect!</p>
</div>
{{/if}}
{{/with}}
</form>
<script src="Content/Scripts/post-validation.js"></script>

View File

@@ -41,12 +41,13 @@
</a>
</div>
{{MenuItemActionLink text='Overview' controller='Scheduler'}}
{{MenuItemActionLink title='Overview' controller='Scheduler'}}
{{MenuItemActionLink 'Jobs'}}
{{MenuItemActionLink 'Triggers'}}
{{MenuItemActionLink 'Executions'}}
{{MenuItemActionLink 'History'}}
{{MenuItemActionLink 'Calendars'}}
{{MenuItemActionLink title='Logout' controller='Authenticate/Logout'}}
<!--
<div class="right menu">