233 changed files with 15221 additions and 3525 deletions
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
๏ปฟusing System; |
||||
using System.Collections.Generic; |
||||
using System.Text; |
||||
|
||||
namespace Teknik.Configuration |
||||
{ |
||||
public class IdentityServerConfig |
||||
{ |
||||
public string Authority { get; set; } |
||||
|
||||
public string ClientId { get; set; } |
||||
public string ClientSecret { get; set; } |
||||
public List<string> RedirectUris { get; set; } |
||||
public List<string> PostLogoutRedirectUris { get; set; } |
||||
|
||||
public string APIName { get; set; } |
||||
public string APISecret { get; set; } |
||||
|
||||
public IdentityServerConfig() |
||||
{ |
||||
Authority = "https://localhost:5002"; |
||||
ClientId = "mvc.client"; |
||||
ClientSecret = "mysecret"; |
||||
RedirectUris = new List<string>(); |
||||
PostLogoutRedirectUris = new List<string>(); |
||||
APIName = "api"; |
||||
APISecret = "secret"; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
๏ปฟusing Microsoft.AspNetCore.Identity.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Teknik.IdentityServer.Models; |
||||
|
||||
namespace Teknik.IdentityServer |
||||
{ |
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> |
||||
{ |
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } |
||||
} |
||||
} |
@ -0,0 +1,143 @@
@@ -0,0 +1,143 @@
|
||||
๏ปฟusing System; |
||||
using System.Collections.Generic; |
||||
using System.IdentityModel.Tokens.Jwt; |
||||
using System.Security.Claims; |
||||
using IdentityModel; |
||||
using IdentityServer4; |
||||
using IdentityServer4.Models; |
||||
using IdentityServer4.Test; |
||||
using Teknik.Configuration; |
||||
|
||||
namespace Teknik.IdentityServer.Configuration |
||||
{ |
||||
internal class Clients |
||||
{ |
||||
public static IEnumerable<Client> Get(Config config) |
||||
{ |
||||
return new List<Client> { |
||||
new Client |
||||
{ |
||||
ClientId = config.UserConfig.IdentityServerConfig.ClientId, |
||||
ClientName = "Teknik Web Services", |
||||
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials, |
||||
|
||||
ClientSecrets = |
||||
{ |
||||
new Secret(config.UserConfig.IdentityServerConfig.ClientSecret.Sha256()) |
||||
}, |
||||
|
||||
RequireConsent = false, |
||||
|
||||
AllowedScopes = |
||||
{ |
||||
IdentityServerConstants.StandardScopes.OpenId, |
||||
"role", |
||||
"account-info", |
||||
"security-info", |
||||
"teknik-api.read", |
||||
"teknik-api.write", |
||||
"auth-api" |
||||
}, |
||||
AllowOfflineAccess = true |
||||
} |
||||
}; |
||||
} |
||||
} |
||||
|
||||
internal class Resources |
||||
{ |
||||
public static IEnumerable<IdentityResource> GetIdentityResources() |
||||
{ |
||||
return new List<IdentityResource> { |
||||
new IdentityResources.OpenId(), |
||||
new IdentityResource |
||||
{ |
||||
Name = "account-info", |
||||
DisplayName = "Account Info", |
||||
UserClaims = new List<string> |
||||
{ |
||||
"username", |
||||
"email", |
||||
"creation-date", |
||||
"last-seen", |
||||
"account-type", |
||||
"account-status" |
||||
} |
||||
}, |
||||
new IdentityResource |
||||
{ |
||||
Name = "security-info", |
||||
DisplayName = "Security Info", |
||||
UserClaims = new List<string> |
||||
{ |
||||
"recovery-email", |
||||
"recovery-verified", |
||||
"pgp-public-key" |
||||
} |
||||
}, |
||||
new IdentityResource { |
||||
Name = "role", |
||||
DisplayName = "Role", |
||||
UserClaims = new List<string> {"role"} |
||||
} |
||||
}; |
||||
} |
||||
|
||||
public static IEnumerable<ApiResource> GetApiResources(Config config) |
||||
{ |
||||
return new List<ApiResource> { |
||||
new ApiResource { |
||||
Name = config.UserConfig.IdentityServerConfig.APIName, |
||||
DisplayName = "Teknik API", |
||||
Description = "Teknik API Access for end users", |
||||
UserClaims = new List<string> {"role"}, |
||||
ApiSecrets = new List<Secret> {new Secret(config.UserConfig.IdentityServerConfig.APISecret.Sha256()) }, |
||||
Scopes = new List<Scope> { |
||||
new Scope("teknik-api.read", "Teknik API Read Access"), |
||||
new Scope("teknik-api.write", "Teknik API Write Access") |
||||
} |
||||
}, |
||||
new ApiResource { |
||||
Name = "auth-api", |
||||
DisplayName = "Auth Server API", |
||||
Description = "Auth Server API Access for managing the Auth Server", |
||||
Scopes = new List<Scope> { |
||||
new Scope() |
||||
{ |
||||
Name = "auth-api", |
||||
ShowInDiscoveryDocument = false, |
||||
Required = true |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
} |
||||
} |
||||
|
||||
internal class Policies |
||||
{ |
||||
public static IEnumerable<Policy> Get() |
||||
{ |
||||
return new List<Policy> |
||||
{ |
||||
new Policy |
||||
{ |
||||
Name = "Internal", |
||||
Scopes = { "auth-api" } |
||||
} |
||||
}; |
||||
} |
||||
} |
||||
|
||||
internal class Policy |
||||
{ |
||||
public string Name { get; set; } |
||||
public ICollection<string> Scopes { get; set; } |
||||
|
||||
public Policy() |
||||
{ |
||||
Name = string.Empty; |
||||
Scopes = new List<string>(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
|
||||
๏ปฟbody { |
||||
margin-top: 15px; |
||||
} |
||||
.navbar-header { |
||||
position: relative; |
||||
top: -4px; |
||||
} |
||||
.navbar-brand > .icon-banner { |
||||
position: relative; |
||||
top: -2px; |
||||
display: inline; |
||||
} |
||||
label { |
||||
font-weight: normal !important; |
||||
} |
||||
.icon { |
||||
position: relative; |
||||
top: -10px; |
||||
} |
||||
.logged-out iframe { |
||||
display: none; |
||||
width: 0; |
||||
height: 0; |
||||
} |
||||
.page-consent .client-logo { |
||||
float: left; |
||||
} |
||||
.page-consent .client-logo img { |
||||
width: 80px; |
||||
height: 80px; |
||||
} |
||||
.page-consent .consent-buttons { |
||||
margin-top: 25px; |
||||
} |
||||
.page-consent .consent-form .consent-scopecheck { |
||||
display: inline-block; |
||||
margin-right: 5px; |
||||
} |
||||
.page-consent .consent-form .consent-description { |
||||
margin-left: 25px; |
||||
} |
||||
.page-consent .consent-form .consent-description label { |
||||
font-weight: normal; |
||||
} |
||||
.page-consent .consent-form .consent-remember { |
||||
padding-left: 16px; |
||||
} |
||||
.grants .page-header { |
||||
margin-bottom: 10px; |
||||
} |
||||
.grants .grant { |
||||
margin-top: 20px; |
||||
padding-bottom: 20px; |
||||
border-bottom: 1px solid lightgray; |
||||
} |
||||
.grants .grant img { |
||||
width: 100px; |
||||
height: 100px; |
||||
} |
||||
.grants .grant .clientname { |
||||
font-size: 140%; |
||||
font-weight: bold; |
||||
} |
||||
.grants .grant .granttype { |
||||
font-size: 120%; |
||||
font-weight: bold; |
||||
} |
||||
.grants .grant .created { |
||||
font-size: 120%; |
||||
font-weight: bold; |
||||
} |
||||
.grants .grant .expires { |
||||
font-size: 120%; |
||||
font-weight: bold; |
||||
} |
||||
.grants .grant li { |
||||
list-style-type: none; |
||||
display: inline; |
||||
} |
||||
.grants .grant li:after { |
||||
content: ', '; |
||||
} |
||||
.grants .grant li:last-child:after { |
||||
content: ''; |
||||
} |
@ -0,0 +1,307 @@
@@ -0,0 +1,307 @@
|
||||
๏ปฟusing IdentityModel; |
||||
using IdentityServer4.Services; |
||||
using IdentityServer4.Stores; |
||||
using IdentityServer4.Test; |
||||
using Microsoft.AspNetCore.Http; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Security.Claims; |
||||
using System.Security.Principal; |
||||
using System.Threading.Tasks; |
||||
using Microsoft.AspNetCore.Authentication; |
||||
using IdentityServer4.Events; |
||||
using IdentityServer4.Extensions; |
||||
using IdentityServer4.Models; |
||||
using Microsoft.AspNetCore.Identity; |
||||
using Teknik.IdentityServer.Security; |
||||
using Teknik.IdentityServer.Services; |
||||
using Teknik.IdentityServer.ViewModels; |
||||
using Teknik.IdentityServer.Options; |
||||
using Teknik.IdentityServer.Models; |
||||
using Microsoft.Extensions.Logging; |
||||
using Teknik.Logging; |
||||
using Teknik.Configuration; |
||||
|
||||
namespace Teknik.IdentityServer.Controllers |
||||
{ |
||||
public class AccountController : DefaultController |
||||
{ |
||||
private readonly UserManager<ApplicationUser> _userManager; |
||||
private readonly SignInManager<ApplicationUser> _signInManager; |
||||
private readonly IIdentityServerInteractionService _interaction; |
||||
private readonly IEventService _events; |
||||
private readonly AccountService _account; |
||||
|
||||
public AccountController( |
||||
ILogger<Logger> logger, |
||||
Config config, |
||||
IIdentityServerInteractionService interaction, |
||||
IClientStore clientStore, |
||||
IHttpContextAccessor httpContextAccessor, |
||||
IAuthenticationSchemeProvider schemeProvider, |
||||
IEventService events, |
||||
UserManager<ApplicationUser> userManager, |
||||
SignInManager<ApplicationUser> signInManager) : base(logger, config) |
||||
{ |
||||
// if the TestUserStore is not in DI, then we'll just use the global users collection
|
||||
_userManager = userManager; |
||||
_signInManager = signInManager; |
||||
_interaction = interaction; |
||||
_events = events; |
||||
_account = new AccountService(interaction, httpContextAccessor, schemeProvider, clientStore); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Show login page
|
||||
/// </summary>
|
||||
[HttpGet] |
||||
public async Task<IActionResult> Login(string returnUrl) |
||||
{ |
||||
// build a model so we know what to show on the login page
|
||||
var vm = await _account.BuildLoginViewModelAsync(returnUrl); |
||||
|
||||
return View(vm); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Handle postback from username/password login
|
||||
/// </summary>
|
||||
[HttpPost] |
||||
[ValidateAntiForgeryToken] |
||||
public async Task<IActionResult> Login(LoginViewModel model, string button, string returnUrl = null) |
||||
{ |
||||
if (button != "login") |
||||
{ |
||||
// the user clicked the "cancel" button
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl); |
||||
if (context != null) |
||||
{ |
||||
// if the user cancels, send a result back into IdentityServer as if they
|
||||
// denied the consent (even if this client does not require consent).
|
||||
// this will send back an access denied OIDC error response to the client.
|
||||
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied); |
||||
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
return Redirect(returnUrl); |
||||
} |
||||
else |
||||
{ |
||||
// since we don't have a valid context, then we just go back to the home page
|
||||
return Redirect("~/"); |
||||
} |
||||
} |
||||
|
||||
if (ModelState.IsValid) |
||||
{ |
||||
// Check to see if the user is banned
|
||||
var foundUser = await _userManager.FindByNameAsync(model.Username); |
||||
if (foundUser != null) |
||||
{ |
||||
if (foundUser.AccountStatus == Utilities.AccountStatus.Banned) |
||||
{ |
||||
// Redirect to banned page
|
||||
return RedirectToAction(nameof(Banned)); |
||||
} |
||||
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, false); |
||||
|
||||
if (result.Succeeded) |
||||
{ |
||||
// make sure the returnUrl is still valid, and if so redirect back to authorize endpoint or a local page
|
||||
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl)) |
||||
{ |
||||
return Redirect(returnUrl); |
||||
} |
||||
|
||||
return Redirect("~/"); |
||||
} |
||||
if (result.RequiresTwoFactor) |
||||
{ |
||||
// Redirect to 2FA page
|
||||
return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe }); |
||||
} |
||||
if (result.IsLockedOut) |
||||
{ |
||||
// Redirect to locked out page
|
||||
return RedirectToAction(nameof(Lockout)); |
||||
} |
||||
} |
||||
|
||||
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); |
||||
|
||||
ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); |
||||
} |
||||
|
||||
// something went wrong, show form with error
|
||||
var vm = await _account.BuildLoginViewModelAsync(model); |
||||
return View(vm); |
||||
} |
||||
|
||||
[HttpGet] |
||||
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null) |
||||
{ |
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); |
||||
|
||||
if (user == null) |
||||
{ |
||||
throw new ApplicationException($"Unable to load two-factor authentication user."); |
||||
} |
||||
|
||||
var model = new LoginWith2faViewModel { RememberMe = rememberMe }; |
||||
ViewData["ReturnUrl"] = returnUrl; |
||||
|
||||
return View(model); |
||||
} |
||||
|
||||
[HttpPost] |
||||
[ValidateAntiForgeryToken] |
||||
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null) |
||||
{ |
||||
if (!ModelState.IsValid) |
||||
{ |
||||
return View(model); |
||||
} |
||||
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); |
||||
if (user == null) |
||||
{ |
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
} |
||||
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); |
||||
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine); |
||||
|
||||
if (result.Succeeded) |
||||
{ |
||||
return RedirectToLocal(returnUrl); |
||||
} |
||||
else if (result.IsLockedOut) |
||||
{ |
||||
return RedirectToAction(nameof(Lockout)); |
||||
} |
||||
else |
||||
{ |
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code."); |
||||
return View(); |
||||
} |
||||
} |
||||
|
||||
[HttpGet] |
||||
public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null) |
||||
{ |
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); |
||||
if (user == null) |
||||
{ |
||||
throw new ApplicationException($"Unable to load two-factor authentication user."); |
||||
} |
||||
|
||||
ViewData["ReturnUrl"] = returnUrl; |
||||
|
||||
return View(); |
||||
} |
||||
|
||||
[HttpPost] |
||||
[ValidateAntiForgeryToken] |
||||
public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null) |
||||
{ |
||||
if (!ModelState.IsValid) |
||||
{ |
||||
return View(model); |
||||
} |
||||
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); |
||||
if (user == null) |
||||
{ |
||||
throw new ApplicationException($"Unable to load two-factor authentication user."); |
||||
} |
||||
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty); |
||||
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); |
||||
|
||||
if (result.Succeeded) |
||||
{ |
||||
return RedirectToLocal(returnUrl); |
||||
} |
||||
if (result.IsLockedOut) |
||||
{ |
||||
return RedirectToAction(nameof(Lockout)); |
||||
} |
||||
else |
||||
{ |
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); |
||||
return View(); |
||||
} |
||||
} |
||||
|
||||
[HttpGet] |
||||
public IActionResult Lockout() |
||||
{ |
||||
return View(); |
||||
} |
||||
|
||||
[HttpGet] |
||||
public IActionResult Banned() |
||||
{ |
||||
return View(); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Show logout page
|
||||
/// </summary>
|
||||
[HttpGet] |
||||
public async Task<IActionResult> Logout(string logoutId) |
||||
{ |
||||
// build a model so the logout page knows what to display
|
||||
var vm = await _account.BuildLogoutViewModelAsync(logoutId); |
||||
|
||||
if (vm.ShowLogoutPrompt == false) |
||||
{ |
||||
// if the request for logout was properly authenticated from IdentityServer, then
|
||||
// we don't need to show the prompt and can just log the user out directly.
|
||||
return await Logout(vm); |
||||
} |
||||
|
||||
return View(vm); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Handle logout page postback
|
||||
/// </summary>
|
||||
[HttpPost] |
||||
[ValidateAntiForgeryToken] |
||||
public async Task<IActionResult> Logout(LogoutInputModel model) |
||||
{ |
||||
// get context information (client name, post logout redirect URI and iframe for federated signout)
|
||||
var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId); |
||||
|
||||
if (User?.Identity.IsAuthenticated == true) |
||||
{ |
||||
await _signInManager.SignOutAsync(); |
||||
|
||||
// raise the logout event
|
||||
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); |
||||
} |
||||
|
||||
return View("LoggedOut", vm); |
||||
return RedirectToLocal(model.ReturnURL); |
||||
} |
||||
|
||||
private IActionResult RedirectToLocal(string returnUrl) |
||||
{ |
||||
if (Url.IsLocalUrl(returnUrl)) |
||||
{ |
||||
return Redirect(returnUrl); |
||||
} |
||||
else |
||||
{ |
||||
return RedirectToAction(nameof(HomeController.Index), "Home"); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
๏ปฟusing IdentityServer4.Services; |
||||
using IdentityServer4.Stores; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
using Microsoft.Extensions.Logging; |
||||
using System.Threading.Tasks; |
||||
using Teknik.Configuration; |
||||
using Teknik.IdentityServer.Models; |
||||
using Teknik.IdentityServer.Security; |
||||
using Teknik.IdentityServer.Services; |
||||
using Teknik.Logging; |
||||
|
||||
namespace Teknik.IdentityServer.Controllers |
||||
{ |
||||
/// <summary>
|
||||
/// This controller processes the consent UI
|
||||
/// </summary>
|
||||
public class ConsentController : DefaultController |
||||
{ |
||||
private readonly ConsentService _consent; |
||||
|
||||
public ConsentController( |
||||
ILogger<Logger> logger, |
||||
Config config, |
||||
IIdentityServerInteractionService interaction, |
||||
IClientStore clientStore, |
||||
IResourceStore resourceStore) : base(logger, config) |
||||
{ |
||||
_consent = new ConsentService(interaction, clientStore, resourceStore, logger); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Shows the consent screen
|
||||
/// </summary>
|
||||
/// <param name="returnUrl"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet] |
||||
public async Task<IActionResult> Index(string returnUrl) |
||||
{ |
||||
var vm = await _consent.BuildViewModelAsync(returnUrl); |
||||
if (vm != null) |
||||
{ |
||||
return View("Index", vm); |
||||
} |
||||
|
||||
return View("Error"); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Handles the consent screen postback
|
||||
/// </summary>
|
||||
[HttpPost] |
||||
[ValidateAntiForgeryToken] |
||||
public async Task<IActionResult> Index(ConsentInputModel model) |
||||
{ |
||||
var result = await _consent.ProcessConsent(model); |
||||
|
||||
if (result.IsRedirect) |
||||
{ |
||||
return Redirect(result.RedirectUri); |
||||
} |
||||
|
||||
if (result.HasValidationError) |
||||
{ |
||||
ModelState.AddModelError("", result.ValidationError); |
||||
} |
||||
|
||||
if (result.ShowView) |
||||
{ |
||||
return View("Index", result.ViewModel); |
||||
} |
||||
|
||||
return View("Error"); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
๏ปฟusing Microsoft.AspNetCore.Authorization; |
||||
using Microsoft.AspNetCore.Hosting; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
using Microsoft.Extensions.Logging; |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using Teknik.Configuration; |
||||
using Teknik.Logging; |
||||
using Teknik.Utilities; |
||||
|
||||
namespace Teknik.IdentityServer.Controllers |
||||
{ |
||||
public class DefaultController : Controller |
||||
{ |
||||
protected readonly ILogger<Logger> _logger; |
||||
protected readonly Config _config; |
||||
|
||||
public DefaultController(ILogger<Logger> logger, Config config) |
||||
{ |
||||
_logger = logger; |
||||
_config = config; |
||||
|
||||
ViewBag.Title = "Teknik Authentication"; |
||||
ViewBag.Description = "Teknik Authentication Service"; |
||||
} |
||||
|
||||
// Get the Favicon
|
||||
[HttpGet] |
||||
[AllowAnonymous] |
||||
[ResponseCache(Duration = 31536000, Location = ResponseCacheLocation.Any)] |
||||
public IActionResult Favicon([FromServices] IHostingEnvironment env) |
||||
{ |
||||
string imageFile = FileHelper.MapPath(env, Constants.FAVICON_PATH); |
||||
FileStream fs = new FileStream(imageFile, FileMode.Open, FileAccess.Read); |
||||
return File(fs, "image/x-icon"); |
||||
} |
||||
|
||||
// Get the Robots.txt
|
||||
[HttpGet] |
||||
[AllowAnonymous] |
||||
public IActionResult Robots([FromServices] IHostingEnvironment env) |
||||
{ |
||||
//string file = FileHelper.MapPath(env, Constants.ROBOTS_PATH);
|
||||
return File(Constants.ROBOTS_PATH, "text/plain"); |
||||
} |
||||
|
||||
protected IActionResult GenerateActionResult(object json) |
||||
{ |
||||
return GenerateActionResult(json, View()); |
||||
} |
||||
|
||||
protected IActionResult GenerateActionResult(object json, IActionResult result) |
||||
{ |
||||
if (Request.IsAjaxRequest()) |
||||
{ |
||||
return Json(json); |
||||
} |
||||
return result; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,237 @@
@@ -0,0 +1,237 @@
|
||||
๏ปฟusing IdentityServer4.Services; |
||||
using Microsoft.AspNetCore.Authorization; |
||||
using Microsoft.AspNetCore.Diagnostics; |
||||
using Microsoft.AspNetCore.Http; |
||||
using Microsoft.AspNetCore.Http.Extensions; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
using Microsoft.Extensions.Logging; |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Net.Mail; |
||||
using System.Text; |
||||
using System.Threading.Tasks; |
||||
using Teknik.Configuration; |
||||
using Teknik.IdentityServer.ViewModels; |
||||
using Teknik.Logging; |
||||
using Teknik.Utilities; |
||||
|
||||
namespace Teknik.IdentityServer.Controllers |
||||
{ |
||||
public class ErrorController : DefaultController |
||||
{ |
||||
private readonly IIdentityServerInteractionService _interaction; |
||||
|
||||
public ErrorController(ILogger<Logger> logger, Config config, IIdentityServerInteractionService interaction) : base(logger, config) |
||||
{ |
||||
_interaction = interaction; |
||||
} |
||||
|
||||
public IActionResult HttpError(int statusCode) |
||||
{ |
||||
switch (statusCode) |
||||
{ |
||||
case 401: |
||||
return Http401(); |
||||
case 403: |
||||
return Http403(); |
||||
case 404: |
||||
return Http404(); |
||||
default: |
||||
return HttpGeneral(statusCode); |
||||
} |
||||
} |
||||
|
||||
public IActionResult HttpGeneral(int statusCode) |
||||
{ |
||||
ViewBag.Title = statusCode + " - " + _config.Title; |
||||
|
||||
LogError(LogLevel.Error, "HTTP Error Code: " + statusCode); |
||||
|
||||
ErrorViewModel model = new ErrorViewModel(); |
||||
model.StatusCode = statusCode; |
||||
|
||||
return GenerateActionResult(CreateErrorObj("Http", statusCode, "Invalid HTTP Response"), View("~/Views/Error/HttpGeneral.cshtml", model)); |
||||
} |
||||
|
||||
[AllowAnonymous] |
||||
public IActionResult Http401() |
||||
{ |
||||
Response.StatusCode = StatusCodes.Status401Unauthorized; |
||||
|
||||
ViewBag.Title = "401 - " + _config.Title; |
||||
ViewBag.Description = "Unauthorized"; |
||||
|
||||
LogError(LogLevel.Error, "Unauthorized"); |
||||
|
||||
ErrorViewModel model = new ErrorViewModel(); |
||||
model.StatusCode = StatusCodes.Status401Unauthorized; |
||||
|
||||
return GenerateActionResult(CreateErrorObj("Http", StatusCodes.Status401Unauthorized, "Unauthorized"), View("~/Views/Error/Http401.cshtml", model)); |
||||
} |
||||
|
||||
[AllowAnonymous] |
||||
public IActionResult Http403() |
||||
{ |
||||
Response.StatusCode = StatusCodes.Status403Forbidden; |
||||
|
||||
ViewBag.Title = "403 - " + _config.Title; |
||||
ViewBag.Description = "Access Denied"; |
||||
|
||||
LogError(LogLevel.Error, "Access Denied"); |
||||
|
||||
ErrorViewModel model = new ErrorViewModel(); |
||||
model.StatusCode = StatusCodes.Status403Forbidden; |
||||
|
||||
return GenerateActionResult(CreateErrorObj("Http", StatusCodes.Status403Forbidden, "Access Denied"), View("~/Views/Error/Http403.cshtml", model)); |
||||
} |
||||
|
||||
[AllowAnonymous] |
||||
public IActionResult Http404() |
||||
{ |
||||
Response.StatusCode = StatusCodes.Status404NotFound; |
||||
|
||||
ViewBag.Title = "404 - " + _config.Title; |
||||
ViewBag.Description = "Uh Oh, can't find it!"; |
||||
|
||||
LogError(LogLevel.Warning, "Page Not Found"); |
||||
|
||||
ErrorViewModel model = new ErrorViewModel(); |
||||
model.StatusCode = StatusCodes.Status404NotFound; |
||||
|
||||
return GenerateActionResult(CreateErrorObj("Http", StatusCodes.Status404NotFound, "Page Not Found"), View("~/Views/Error/Http404.cshtml", model)); |
||||
} |
||||
|
||||
[AllowAnonymous] |
||||
public IActionResult Http500(Exception exception) |
||||
{ |
||||
if (HttpContext != null) |
||||
{ |
||||
var ex = HttpContext.Features.Get<IExceptionHandlerFeature>(); |
||||
if (ex != null) |
||||
{ |
||||
exception = ex.Error; |
||||
} |
||||
HttpContext.Session.Set("Exception", exception); |
||||
} |
||||
|
||||
Response.StatusCode = StatusCodes.Status500InternalServerError; |
||||
|
||||
ViewBag.Title = "500 - " + _config.Title; |
||||
ViewBag.Description = "Something Borked"; |
||||
|
||||
LogError(LogLevel.Error, "Server Error", exception); |
||||
|
||||
ErrorViewModel model = new ErrorViewModel(); |
||||
model.StatusCode = StatusCodes.Status500InternalServerError; |
||||
model.Exception = exception; |
||||
|
||||
return GenerateActionResult(CreateErrorObj("Http", StatusCodes.Status500InternalServerError, exception.Message), View("~/Views/Error/Http500.cshtml", model)); |
||||
} |
||||
|
||||
[AllowAnonymous] |
||||
public async Task<IActionResult> IdentityError(string errorId) |
||||
{ |
||||
var message = await _interaction.GetErrorContextAsync(errorId); |
||||
|
||||
Response.StatusCode = StatusCodes.Status500InternalServerError; |
||||
|
||||
ViewBag.Title = "Identity Error - " + _config.Title; |
||||
ViewBag.Description = "The Identity Service threw an error"; |
||||
|
||||
LogError(LogLevel.Error, "Identity Error: " + message.Error); |
||||
|
||||
IdentityErrorViewModel model = new IdentityErrorViewModel(); |
||||
model.Title = message.Error; |
||||
model.Description = message.ErrorDescription; |
||||
|
||||
return GenerateActionResult(CreateErrorObj("Http", StatusCodes.Status500InternalServerError, message.Error), View("~/Views/Error/IdentityError.cshtml", model)); |
||||
} |
||||
|
||||
[HttpPost] |
||||
[AllowAnonymous] |
||||
[ValidateAntiForgeryToken] |
||||
public IActionResult SubmitErrorReport(SubmitReportViewModel model) |
||||
{ |
||||
try |
||||
{ |
||||
string exceptionMsg = model.Exception; |
||||
|
||||
// Try to grab the actual exception that occured
|
||||
Exception ex = HttpContext.Session.Get<Exception>("Exception"); |
||||
if (ex != null) |
||||
{ |
||||
exceptionMsg = string.Format(@"
|
||||
Exception: {0} |
||||
|
||||
Source: {1} |
||||
|
||||
Stack Trace: |
||||
|
||||
{2} |
||||
", ex.GetFullMessage(true), ex.Source, ex.StackTrace);
|
||||
} |
||||
|
||||
// Let's also email the message to support
|
||||
SmtpClient client = new SmtpClient(); |
||||
client.Host = _config.ContactConfig.EmailAccount.Host; |
||||
client.Port = _config.ContactConfig.EmailAccount.Port; |
||||
client.EnableSsl = _config.ContactConfig.EmailAccount.SSL; |
||||
client.DeliveryMethod = SmtpDeliveryMethod.Network; |
||||
client.UseDefaultCredentials = true; |
||||
client.Credentials = new System.Net.NetworkCredential(_config.ContactConfig.EmailAccount.Username, _config.ContactConfig.EmailAccount.Password); |
||||
client.Timeout = 5000; |
||||
|
||||
MailMessage mail = new MailMessage(new MailAddress(_config.NoReplyEmail, _config.NoReplyEmail), new MailAddress(_config.SupportEmail, "Teknik Support")); |
||||
mail.Sender = new MailAddress(_config.ContactConfig.EmailAccount.EmailAddress); |
||||
mail.Subject = "[Exception] Application Exception Occured"; |
||||
mail.Body = @"
|
||||
An exception has occured at: " + model.CurrentUrl + @" |
||||
|
||||
---------------------------------------- |
||||
User Message: |
||||
|
||||
" + model.Message + @" |
||||
|
||||
---------------------------------------- |
||||
" + exceptionMsg;
|
||||
mail.BodyEncoding = UTF8Encoding.UTF8; |
||||
mail.DeliveryNotificationOptions = DeliveryNotificationOptions.Never; |
||||
|
||||
client.Send(mail); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
return Json(new { error = "Error submitting report. Exception: " + ex.Message }); |
||||
} |
||||
|
||||
return Json(new { result = "true" }); |
||||
} |
||||
|
||||
private object CreateErrorObj(string type, int statusCode, string message) |
||||
{ |
||||
return new { error = new { type = type, status = statusCode, message = message } }; |
||||
} |
||||
|
||||
private void LogError(LogLevel level, string message) |
||||
{ |
||||
LogError(level, message, null); |
||||
} |
||||
|
||||
private void LogError(LogLevel level, string message, Exception exception) |
||||
{ |
||||
if (Request != null) |
||||
{ |
||||
message += " | Url: " + Request.GetDisplayUrl(); |
||||
|
||||
message += " | Referred Url: " + Request.Headers["Referer"].ToString(); |
||||
|
||||
message += " | Method: " + Request.Method; |
||||
|
||||
message += " | User Agent: " + Request.Headers["User-Agent"].ToString(); |
||||
} |
||||
|
||||
_logger.Log(level, message, exception); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
๏ปฟusing IdentityServer4.Services; |
||||
using IdentityServer4.Stores; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using Microsoft.AspNetCore.Authorization; |
||||
using Teknik.IdentityServer.Security; |
||||
using Teknik.IdentityServer.ViewModels; |
||||
using Teknik.Logging; |
||||
using Microsoft.Extensions.Logging; |
||||
using Teknik.Configuration; |
||||
|
||||
namespace Teknik.IdentityServer.Controllers |
||||
{ |
||||
/// <summary>
|
||||
/// This sample controller allows a user to revoke grants given to clients
|
||||
/// </summary>
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")] |
||||
public class GrantsController : DefaultController |
||||
{ |
||||
private readonly IIdentityServerInteractionService _interaction; |
||||
private readonly IClientStore _clients; |
||||
private readonly IResourceStore _resources; |
||||
|
||||
public GrantsController( |
||||
ILogger<Logger> logger, |
||||
Config config, |
||||
IIdentityServerInteractionService interaction, |
||||
IClientStore clients, |
||||
IResourceStore resources) : base(logger, config) |
||||
{ |
||||
_interaction = interaction; |
||||
_clients = clients; |
||||
_resources = resources; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Show list of grants
|
||||
/// </summary>
|
||||
[HttpGet] |
||||
public async Task<IActionResult> Index() |
||||
{ |
||||
return View("Index", await BuildViewModelAsync()); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Handle postback to revoke a client
|
||||
/// </summary>
|
||||
[HttpPost] |
||||
[ValidateAntiForgeryToken] |
||||
public async Task<IActionResult> Revoke(string clientId) |
||||
{ |
||||
await _interaction.RevokeUserConsentAsync(clientId); |
||||
return RedirectToAction("Index"); |
||||
} |
||||
|
||||
private async Task<GrantsViewModel> BuildViewModelAsync() |
||||
{ |
||||
var grants = await _interaction.GetAllUserConsentsAsync(); |
||||
|
||||
var list = new List<GrantViewModel>(); |
||||
foreach(var grant in grants) |
||||
{ |
||||
var client = await _clients.FindClientByIdAsync(grant.ClientId); |
||||
if (client != null) |
||||
{ |
||||
var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes); |
||||
|
||||
var item = new GrantViewModel() |
||||
{ |
||||
ClientId = client.ClientId, |
||||
ClientName = client.ClientName ?? client.ClientId, |
||||
ClientLogoUrl = client.LogoUri, |
||||
ClientUrl = client.ClientUri, |
||||
Created = grant.CreationTime, |
||||
Expires = grant.Expiration, |
||||
IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(), |
||||
ApiGrantNames = resources.ApiResources.Select(x => x.DisplayName ?? x.Name).ToArray() |
||||
}; |
||||
|
||||
list.Add(item); |
||||
} |
||||
} |
||||
|
||||
return new GrantsViewModel |
||||
{ |
||||
Grants = list |
||||
}; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
๏ปฟusing IdentityServer4.Services; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
using Microsoft.Extensions.Logging; |
||||
using System.Threading.Tasks; |
||||
using Teknik.Configuration; |
||||
using Teknik.IdentityServer.Security; |
||||
using Teknik.IdentityServer.ViewModels; |
||||
using Teknik.Logging; |
||||
|
||||
namespace Teknik.IdentityServer.Controllers |
||||
{ |
||||
public class HomeController : DefaultController |
||||
{ |
||||
private readonly IIdentityServerInteractionService _interaction; |
||||
|
||||
public HomeController( |
||||
ILogger<Logger> logger, |
||||
Config config, |
||||
IIdentityServerInteractionService interaction) : base(logger, config) |
||||
{ |
||||
_interaction = interaction; |
||||
} |
||||
|
||||
public IActionResult Index() |
||||
{ |
||||
return View(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,420 @@
@@ -0,0 +1,420 @@
|
||||
๏ปฟusing System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Text; |
||||
using System.Threading.Tasks; |
||||
using Microsoft.AspNetCore.Authorization; |
||||
using Microsoft.AspNetCore.Identity; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
using Microsoft.Extensions.Logging; |
||||
using Teknik.Configuration; |
||||
using Teknik.IdentityServer.Models; |
||||
using Teknik.IdentityServer.Models.Manage; |
||||
using Teknik.Logging; |
||||
|
||||
namespace Teknik.IdentityServer.Controllers |
||||
{ |
||||
[Authorize(Policy = "Internal", AuthenticationSchemes = "Bearer")] |
||||
[Route("[controller]/[action]")]
|
||||
[ApiController] |
||||
public class ManageController : DefaultController |
||||
{ |
||||
private readonly UserManager<ApplicationUser> _userManager; |
||||
private readonly SignInManager<ApplicationUser> _signInManager; |
||||
|
||||
public ManageController( |
||||
ILogger<Logger> logger, |
||||
Config config, |
||||
UserManager<ApplicationUser> userManager, |
||||
SignInManager<ApplicationUser> signInManager) : base(logger, config) |
||||
{ |
||||
_userManager = userManager; |
||||
_signInManager = signInManager; |
||||
} |
||||
|
||||
[HttpPost] |
||||
public async Task<IActionResult> CreateUser(NewUserModel model) |
||||
{ |
||||
if (string.IsNullOrEmpty(model.Username)) |
||||
return new JsonResult(new { success = false, message = "Username is required" }); |
||||
if (string.IsNullOrEmpty(model.Password)) |
||||
return new JsonResult(new { success = false, message = "Password is required" }); |
||||
|
||||
var identityUser = new ApplicationUser(model.Username) |
||||
{ |
||||
Id = Guid.NewGuid().ToString(), |
||||
UserName = model.Username, |
||||
AccountStatus = model.AccountStatus, |
||||
AccountType = model.AccountType, |
||||
Email = model.RecoveryEmail, |
||||
EmailConfirmed = model.RecoveryVerified, |
||||
PGPPublicKey = model.PGPPublicKey |
||||
}; |
||||
var result = await _userManager.CreateAsync(identityUser, model.Password); |
||||
if (result.Succeeded) |
||||
{ |
||||
return new JsonResult(new { success = true }); |
||||
} |
||||
|
||||
return new JsonResult(new { success = false, message = "Unable to create user.", identityErrors = result.Errors }); |
||||
} |
||||
|
||||
[HttpPost] |
||||
public async Task<IActionResult> DeleteUser(DeleteUserModel model) |
||||
{ |
||||
if (string.IsNullOrEmpty(model.Username)) |
||||
return new JsonResult(new { success = false, message = "Username is required" }); |
||||
|
||||
var foundUser = await _userManager.FindByNameAsync(model.Username); |
||||
if (foundUser != null) |
||||
{ |
||||
var result = await _userManager.DeleteAsync(foundUser); |
||||
if (result.Succeeded) |
||||
return new JsonResult(new { success = true }); |
||||
else |
||||
return new JsonResult(new { success = false, message = "Unable to delete user.", identityErrors = result.Errors }); |
||||
} |
||||
|
||||
return new JsonResult(new { success = false, message = "User does not exist." }); |
||||
} |
||||
|
||||
[HttpGet] |
||||
public async Task<IActionResult> UserExists(string username) |
||||
{ |
||||
if (string.IsNullOrEmpty(username)) |
||||
return new JsonResult(new { success = false, message = "Username is required" }); |
||||
|
||||
var foundUser = await _userManager.FindByNameAsync(username); |
||||
return new JsonResult(new { success = true, data = foundUser != null }); |
||||
} |
||||
|
||||
[HttpGet] |
||||
public async Task<IActionResult> GetUserInfo(string username) |
||||
{ |
||||
if (string.IsNullOrEmpty(username)) |
||||
return new JsonResult(new { success = false, message = "Username is required" }); |
||||
|
||||
var foundUser = await _userManager.FindByNameAsync(username); |
||||
if (foundUser != null) |
||||
{ |
||||
return new JsonResult(new { success = true, data = foundUser.ToJson() }); |
||||
} |
||||
|
||||
return new JsonResult(new { success = false, message = "User does not exist." }); |
||||
} |
||||
|
||||
[HttpPost] |
||||
public async Task<IActionResult> CheckPassword(CheckPasswordModel model) |
||||
{ |
||||
if (string.IsNullOrEmpty(model.Username)) |
||||
return new JsonResult(new { success = false, message = "Username is required" }); |
||||
if (string.IsNullOrEmpty(model.Password)) |
||||
return new JsonResult(new { success = false, message = "Password is required" }); |
||||
|
||||
var foundUser = await _userManager.FindByNameAsync(model.Username); |
||||
if (foundUser != null) |
||||
{ |
||||
bool valid = await _userManager.CheckPasswordAsync(foundUser, model.Password); |
||||
return new JsonResult(new { success = true, data = valid }); |
||||
} |
||||
|
||||
return new JsonResult(new { success = false, message = "User does not exist." }); |
||||
} |
||||
|
||||
[HttpPost] |
||||
public async Task<IActionResult> GeneratePasswordResetToken(GeneratePasswordResetTokenModel model) |
||||
{ |
||||
if (string.IsNullOrEmpty(model.Username)) |
||||
return new JsonResult(new { success = false, message = "Username is required" }); |
||||
|
||||
var foundUser = await _userManager.FindByNameAsync(model.Username); |
||||
if (foundUser != null) |
||||
{ |
||||
string token = await _userManager.GeneratePasswordResetTokenAsync(foundUser); |
||||
return new JsonResult(new { success = true, data = token }); |
||||
} |
||||
|
||||
return new JsonResult(new { success = false, message = "User does not exist." }); |
||||
} |
||||
|
||||
[HttpPost] |
||||
public async Task<IActionResult> ResetPassword(ResetPasswordModel model) |
||||
{ |
||||
if (string.IsNullOrEmpty(model.Username)) |
||||
return new JsonResult(new { success = false, message = "Username is required" }); |
||||
if (string.IsNullOrEmpty(model.Token)) |
||||
return new JsonResult(new { success = false, message = "Token is required" }); |
||||
if (string.IsNullOrEmpty(model.Password)) |
||||
return new JsonResult(new { success = false, message = "Password is required" }); |
||||
|
||||
var foundUser = await _userManager.FindByNameAsync(model.Username); |
||||
if (foundUser != null) |
||||
< |