Browse Source

Added Identity Server for authentication

tags/3.0.0^2
Teknikode 10 months ago
parent
commit
62e4476897
100 changed files with 6908 additions and 51 deletions
  1. 3
    2
      .gitignore
  2. 48
    48
      Configuration/Config.cs
  3. 1
    0
      Configuration/Configuration.csproj
  4. 30
    0
      Configuration/IdentityServerConfig.cs
  5. 2
    0
      Configuration/UserConfig.cs
  6. 1
    1
      GitService/GitService.csproj
  7. 11
    0
      IdentityServer/ApplicationDbContext.cs
  8. 143
    0
      IdentityServer/Configuration.cs
  9. 85
    0
      IdentityServer/Content/common.css
  10. 307
    0
      IdentityServer/Controllers/AccountController.cs
  11. 75
    0
      IdentityServer/Controllers/ConsentController.cs
  12. 64
    0
      IdentityServer/Controllers/DefaultController.cs
  13. 237
    0
      IdentityServer/Controllers/ErrorController.cs
  14. 92
    0
      IdentityServer/Controllers/GrantsController.cs
  15. 29
    0
      IdentityServer/Controllers/HomeController.cs
  16. 420
    0
      IdentityServer/Controllers/ManageController.cs
  17. 242
    0
      IdentityServer/Data/Migrations/ApplicationDb/20181015060219_InitialApplicationDbContextMigration.Designer.cs
  18. 225
    0
      IdentityServer/Data/Migrations/ApplicationDb/20181015060219_InitialApplicationDbContextMigration.cs
  19. 240
    0
      IdentityServer/Data/Migrations/ApplicationDb/ApplicationDbContextModelSnapshot.cs
  20. 679
    0
      IdentityServer/Data/Migrations/IdentityServer/ConfigurationDb/20180930040544_InitialIdentityServerConfigurationDbMigration.Designer.cs
  21. 602
    0
      IdentityServer/Data/Migrations/IdentityServer/ConfigurationDb/20180930040544_InitialIdentityServerConfigurationDbMigration.cs
  22. 677
    0
      IdentityServer/Data/Migrations/IdentityServer/ConfigurationDb/ConfigurationDbContextModelSnapshot.cs
  23. 57
    0
      IdentityServer/Data/Migrations/IdentityServer/PersistedGrantDb/20180930040529_InitialIdentityServerPersistedGrantDbMigration.Designer.cs
  24. 39
    0
      IdentityServer/Data/Migrations/IdentityServer/PersistedGrantDb/20180930040529_InitialIdentityServerPersistedGrantDbMigration.cs
  25. 55
    0
      IdentityServer/Data/Migrations/IdentityServer/PersistedGrantDb/PersistedGrantDbContextModelSnapshot.cs
  26. 34
    0
      IdentityServer/IdentityServer.csproj
  27. BIN
      IdentityServer/Images/favicon.ico
  28. 23
    0
      IdentityServer/Images/logo-black.svg
  29. 23
    0
      IdentityServer/Images/logo-blue.svg
  30. 118
    0
      IdentityServer/Middleware/BlacklistMiddleware.cs
  31. 72
    0
      IdentityServer/Middleware/CORSMiddleware.cs
  32. 66
    0
      IdentityServer/Middleware/CSPMiddleware.cs
  33. 105
    0
      IdentityServer/Middleware/ErrorHandlerMiddleware.cs
  34. 68
    0
      IdentityServer/Middleware/PerformanceMonitorMiddleware.cs
  35. 61
    0
      IdentityServer/Middleware/SecurityHeadersMiddleware.cs
  36. 40
    0
      IdentityServer/Middleware/SetupHttpContextMiddleware.cs
  37. 74
    0
      IdentityServer/Models/ApplicationUser.cs
  38. 12
    0
      IdentityServer/Models/ConsentInputModel.cs
  39. 14
    0
      IdentityServer/Models/LoginInputModel.cs
  40. 8
    0
      IdentityServer/Models/LogoutInputModel.cs
  41. 13
    0
      IdentityServer/Models/Manage/CheckPasswordModel.cs
  42. 11
    0
      IdentityServer/Models/Manage/CreateClientModel.cs
  43. 12
    0
      IdentityServer/Models/Manage/DeleteUserModel.cs
  44. 12
    0
      IdentityServer/Models/Manage/Disable2FAModel.cs
  45. 13
    0
      IdentityServer/Models/Manage/Enable2FAModel.cs
  46. 12
    0
      IdentityServer/Models/Manage/GeneratePasswordResetTokenModel.cs
  47. 12
    0
      IdentityServer/Models/Manage/GenerateRecoveryCodesModel.cs
  48. 25
    0
      IdentityServer/Models/Manage/NewUserModel.cs
  49. 12
    0
      IdentityServer/Models/Manage/Reset2FAKeyModel.cs
  50. 14
    0
      IdentityServer/Models/Manage/ResetPasswordModel.cs
  51. 14
    0
      IdentityServer/Models/Manage/UpdateAccountStatusModel.cs
  52. 14
    0
      IdentityServer/Models/Manage/UpdateAccountTypeModel.cs
  53. 13
    0
      IdentityServer/Models/Manage/UpdateEmailModel.cs
  54. 13
    0
      IdentityServer/Models/Manage/UpdateEmailVerifiedModel.cs
  55. 14
    0
      IdentityServer/Models/Manage/UpdatePGPPublicKeyModel.cs
  56. 14
    0
      IdentityServer/Models/Manage/UpdatePasswordModel.cs
  57. 13
    0
      IdentityServer/Models/Manage/VerifyEmailModel.cs
  58. 16
    0
      IdentityServer/Models/ProcessConsentResult.cs
  59. 16
    0
      IdentityServer/Options/AccountOptions.cs
  60. 12
    0
      IdentityServer/Options/ConsentOptions.cs
  61. 27
    0
      IdentityServer/Program.cs
  62. 27
    0
      IdentityServer/Properties/PublishProfiles/Teknik Identity Development.pubxml
  63. 35
    0
      IdentityServer/Properties/launchSettings.json
  64. 49
    0
      IdentityServer/Scripts/Error.js
  65. 6
    0
      IdentityServer/Scripts/signout-redirect.js
  66. 62
    0
      IdentityServer/Security/PasswordHasher.cs
  67. 39
    0
      IdentityServer/Security/SecurityHeadersAttribute.cs
  68. 60
    0
      IdentityServer/Security/TeknikRedirectUriValidator.cs
  69. 107
    0
      IdentityServer/Services/AccountService.cs
  70. 190
    0
      IdentityServer/Services/ConsentService.cs
  71. 245
    0
      IdentityServer/Startup.cs
  72. 49
    0
      IdentityServer/TeknikProfileService.cs
  73. 16
    0
      IdentityServer/ViewModels/ConsentViewModel.cs
  74. 12
    0
      IdentityServer/ViewModels/ErrorViewModel.cs
  75. 22
    0
      IdentityServer/ViewModels/GrantsViewModel.cs
  76. 10
    0
      IdentityServer/ViewModels/IdentityErrorViewModel.cs
  77. 13
    0
      IdentityServer/ViewModels/LoggedOutViewModel.cs
  78. 10
    0
      IdentityServer/ViewModels/LoginViewModel.cs
  79. 18
    0
      IdentityServer/ViewModels/LoginWith2faViewModel.cs
  80. 12
    0
      IdentityServer/ViewModels/LoginWithRecoveryCodeViewModel.cs
  81. 9
    0
      IdentityServer/ViewModels/LogoutViewModel.cs
  82. 12
    0
      IdentityServer/ViewModels/ScopeViewModel.cs
  83. 11
    0
      IdentityServer/ViewModels/SubmitReportViewModel.cs
  84. 20
    0
      IdentityServer/ViewModels/ViewModelBase.cs
  85. 14
    0
      IdentityServer/Views/Account/AccessDenied.cshtml
  86. 14
    0
      IdentityServer/Views/Account/Banned.cshtml
  87. 8
    0
      IdentityServer/Views/Account/Lockout.cshtml
  88. 35
    0
      IdentityServer/Views/Account/LoggedOut.cshtml
  89. 61
    0
      IdentityServer/Views/Account/Login.cshtml
  90. 46
    0
      IdentityServer/Views/Account/LoginWith2fa.cshtml
  91. 31
    0
      IdentityServer/Views/Account/LoginWithRecoveryCode.cshtml
  92. 23
    0
      IdentityServer/Views/Account/Logout.cshtml
  93. 82
    0
      IdentityServer/Views/Consent/Index.cshtml
  94. 34
    0
      IdentityServer/Views/Consent/_ScopeListItem.cshtml
  95. 47
    0
      IdentityServer/Views/Error/Exception.cshtml
  96. 15
    0
      IdentityServer/Views/Error/Http401.cshtml
  97. 16
    0
      IdentityServer/Views/Error/Http403.cshtml
  98. 16
    0
      IdentityServer/Views/Error/Http404.cshtml
  99. 48
    0
      IdentityServer/Views/Error/Http500.cshtml
  100. 0
    0
      IdentityServer/Views/Error/HttpGeneral.cshtml

+ 3
- 2
.gitignore View File

@@ -262,6 +262,7 @@ __pycache__/
/Teknik/App_Data/MachineKey.config
/Teknik/App_Data/ConnectionStrings.config
/Teknik/App_Data/Config.json
/Teknik/appsettings.Production.json
/Teknik/appsettings.Development.json
/Teknik/App_Data/version.json

**/appsettings.*.json
**/tempkey.rsa

+ 48
- 48
Configuration/Config.cs View File

@@ -19,38 +19,38 @@ namespace Teknik.Configuration
private ReaderWriterLockSlim _ConfigFileRWLock;
private JsonSerializerSettings _JsonSettings;

private bool _DevEnvironment;
private bool _Migrate;
private bool _UseCdn;
private string _Title;
private string _Description;
private string _Author;
private string _Host;
private string _SupportEmail;
private string _NoReplyEmail;
private string _BitcoinAddress;
private string _Salt1;
private string _Salt2;
private string _CdnHost;
private string _IPBlacklistFile;
private string _ReferrerBlacklistFile;
private List<string> _PublicKeys;
private UserConfig _UserConfig;
private ContactConfig _ContactConfig;
private EmailConfig _EmailConfig;
private GitConfig _GitConfig;
private UploadConfig _UploadConfig;
private PasteConfig _PasteConfig;
private BlogConfig _BlogConfig;
private ApiConfig _ApiConfig;
private PodcastConfig _PodcastConfig;
private StreamConfig _StreamConfig;
private ShortenerConfig _ShortenerConfig;
private VaultConfig _VaultConfig;
private StatsConfig _StatsConfig;
private LoggingConfig _LoggingConfig;
private PiwikConfig _PiwikConfig;
private IRCConfig _IRCConfig;
private bool _DevEnvironment;
private bool _Migrate;
private bool _UseCdn;
private string _Title;
private string _Description;
private string _Author;
private string _Host;
private string _SupportEmail;
private string _NoReplyEmail;
private string _BitcoinAddress;
private string _Salt1;
private string _Salt2;
private string _CdnHost;
private string _IPBlacklistFile;
private string _ReferrerBlacklistFile;
private List<string> _PublicKeys;
private UserConfig _UserConfig;
private ContactConfig _ContactConfig;
private EmailConfig _EmailConfig;
private GitConfig _GitConfig;
private UploadConfig _UploadConfig;
private PasteConfig _PasteConfig;
private BlogConfig _BlogConfig;
private ApiConfig _ApiConfig;
private PodcastConfig _PodcastConfig;
private StreamConfig _StreamConfig;
private ShortenerConfig _ShortenerConfig;
private VaultConfig _VaultConfig;
private StatsConfig _StatsConfig;
private LoggingConfig _LoggingConfig;
private PiwikConfig _PiwikConfig;
private IRCConfig _IRCConfig;

public bool DevEnvironment { get { return _DevEnvironment; } set { _DevEnvironment = value; } }
public bool Migrate { get { return _Migrate; } set { _Migrate = value; } }
@@ -73,52 +73,52 @@ namespace Teknik.Configuration
public List<string> PublicKeys { get { return _PublicKeys; } set { _PublicKeys = value; } }

// User Configuration
public UserConfig UserConfig { get { return _UserConfig; } set { _UserConfig = value; } }
public UserConfig UserConfig { get { return _UserConfig; } set { _UserConfig = value; } }

// Contact Configuration
public ContactConfig ContactConfig { get { return _ContactConfig; } set { _ContactConfig = value; } }
public ContactConfig ContactConfig { get { return _ContactConfig; } set { _ContactConfig = value; } }

// Mail Server Configuration
public EmailConfig EmailConfig { get { return _EmailConfig; } set { _EmailConfig = value; } }
public EmailConfig EmailConfig { get { return _EmailConfig; } set { _EmailConfig = value; } }

// Git Service Configuration
public GitConfig GitConfig { get { return _GitConfig; } set { _GitConfig = value; } }
public GitConfig GitConfig { get { return _GitConfig; } set { _GitConfig = value; } }

// Blog Configuration
public BlogConfig BlogConfig { get { return _BlogConfig; } set { _BlogConfig = value; } }
public BlogConfig BlogConfig { get { return _BlogConfig; } set { _BlogConfig = value; } }

// Upload Configuration
public UploadConfig UploadConfig { get { return _UploadConfig; } set { _UploadConfig = value; } }
public UploadConfig UploadConfig { get { return _UploadConfig; } set { _UploadConfig = value; } }

// Paste Configuration
public PasteConfig PasteConfig { get { return _PasteConfig; } set { _PasteConfig = value; } }
public PasteConfig PasteConfig { get { return _PasteConfig; } set { _PasteConfig = value; } }

// API Configuration
public ApiConfig ApiConfig { get { return _ApiConfig; } set { _ApiConfig = value; } }
public ApiConfig ApiConfig { get { return _ApiConfig; } set { _ApiConfig = value; } }

// Podcast Configuration
public PodcastConfig PodcastConfig { get { return _PodcastConfig; } set { _PodcastConfig = value; } }
public PodcastConfig PodcastConfig { get { return _PodcastConfig; } set { _PodcastConfig = value; } }

// Stream Configuration
public StreamConfig StreamConfig { get { return _StreamConfig; } set { _StreamConfig = value; } }
public StreamConfig StreamConfig { get { return _StreamConfig; } set { _StreamConfig = value; } }

// Shortener Configuration
public ShortenerConfig ShortenerConfig { get { return _ShortenerConfig; } set { _ShortenerConfig = value; } }
public ShortenerConfig ShortenerConfig { get { return _ShortenerConfig; } set { _ShortenerConfig = value; } }

// Vault Configuration
public VaultConfig VaultConfig { get { return _VaultConfig; } set { _VaultConfig = value; } }
public VaultConfig VaultConfig { get { return _VaultConfig; } set { _VaultConfig = value; } }

// Status Configuration
public StatsConfig StatsConfig { get { return _StatsConfig; } set { _StatsConfig = value; } }
public StatsConfig StatsConfig { get { return _StatsConfig; } set { _StatsConfig = value; } }

// Logging Configuration
public LoggingConfig LoggingConfig { get { return _LoggingConfig; } set { _LoggingConfig = value; } }
public LoggingConfig LoggingConfig { get { return _LoggingConfig; } set { _LoggingConfig = value; } }

// Piwik Configuration
public PiwikConfig PiwikConfig { get { return _PiwikConfig; } set { _PiwikConfig = value; } }
public PiwikConfig PiwikConfig { get { return _PiwikConfig; } set { _PiwikConfig = value; } }

// Piwik Configuration
public IRCConfig IRCConfig { get { return _IRCConfig; } set { _IRCConfig = value; } }
public IRCConfig IRCConfig { get { return _IRCConfig; } set { _IRCConfig = value; } }

public Config()
{

+ 1
- 0
Configuration/Configuration.csproj View File

@@ -5,6 +5,7 @@
<RootNamespace>Teknik.Configuration</RootNamespace>
<AssemblyName>Teknik.Configuration</AssemblyName>
<RuntimeIdentifiers>win-x86;win-x64;linux-x64;linux-arm;osx-x64</RuntimeIdentifiers>
<Configurations>Debug;Release;Test</Configurations>
</PropertyGroup>

<ItemGroup>

+ 30
- 0
Configuration/IdentityServerConfig.cs View File

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

+ 2
- 0
Configuration/UserConfig.cs View File

@@ -13,6 +13,7 @@ namespace Teknik.Configuration
public decimal PremiumAccountPrice { get; set; }
public string PaymentType { get; set; }
public bool InviteCodeRequired { get; set; }
public IdentityServerConfig IdentityServerConfig { get; set; }

public UserConfig()
{
@@ -27,6 +28,7 @@ namespace Teknik.Configuration
PremiumAccountPrice = 0;
PaymentType = "Donation";
InviteCodeRequired = false;
IdentityServerConfig = new IdentityServerConfig();
}
}
}

+ 1
- 1
GitService/GitService.csproj View File

@@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MySql.Data" Version="8.0.11" />
<PackageReference Include="MySql.Data" Version="8.0.12" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
</ItemGroup>


+ 11
- 0
IdentityServer/ApplicationDbContext.cs View File

@@ -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) { }
}
}

+ 143
- 0
IdentityServer/Configuration.cs View File

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

+ 85
- 0
IdentityServer/Content/common.css View File

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

+ 307
- 0
IdentityServer/Controllers/AccountController.cs View File

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

+ 75
- 0
IdentityServer/Controllers/ConsentController.cs View File

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

+ 64
- 0
IdentityServer/Controllers/DefaultController.cs View File

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

+ 237
- 0
IdentityServer/Controllers/ErrorController.cs View File

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

+ 92
- 0
IdentityServer/Controllers/GrantsController.cs View File

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

+ 29
- 0
IdentityServer/Controllers/HomeController.cs View File

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

+ 420
- 0
IdentityServer/Controllers/ManageController.cs View File

@@ -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)
{
var result = await _userManager.ResetPasswordAsync(foundUser, model.Token, model.Password);
if (result.Succeeded)
return new JsonResult(new { success = true });
else
return new JsonResult(new { success = false, message = "Unable to reset password.", identityErrors = result.Errors });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpPost]
public async Task<IActionResult> UpdatePassword(UpdatePasswordModel model)
{
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
if (string.IsNullOrEmpty(model.CurrentPassword))
return new JsonResult(new { success = false, message = "Current Password is required" });
if (string.IsNullOrEmpty(model.NewPassword))
return new JsonResult(new { success = false, message = "New Password is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
if (foundUser != null)
{
var result = await _userManager.ChangePasswordAsync(foundUser, model.CurrentPassword, model.NewPassword);
if (result.Succeeded)
return new JsonResult(new { success = true });
else
return new JsonResult(new { success = false, message = "Unable to update password.", identityErrors = result.Errors });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpPost]
public async Task<IActionResult> UpdateEmail(UpdateEmailModel 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.SetEmailAsync(foundUser, model.Email);
if (result.Succeeded)
{
var token = await _userManager.GenerateEmailConfirmationTokenAsync(foundUser);
return new JsonResult(new { success = true, data = token });
}
else
return new JsonResult(new { success = false, message = "Unable to update email address.", identityErrors = result.Errors });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpPost]
public async Task<IActionResult> VerifyEmail(VerifyEmailModel 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" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
if (foundUser != null)
{
var result = await _userManager.ConfirmEmailAsync(foundUser, model.Token);
if (result.Succeeded)
return new JsonResult(new { success = true });
else
return new JsonResult(new { success = false, message = "Unable to verify email address.", identityErrors = result.Errors });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpPost]
public async Task<IActionResult> UpdateAccountStatus(UpdateAccountStatusModel 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)
{
foundUser.AccountStatus = model.AccountStatus;

var result = await _userManager.UpdateAsync(foundUser);
if (result.Succeeded)
return new JsonResult(new { success = true });
else
return new JsonResult(new { success = false, message = "Unable to update account status.", identityErrors = result.Errors });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpPost]
public async Task<IActionResult> UpdateAccountType(UpdateAccountTypeModel 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)
{
foundUser.AccountType = model.AccountType;

var result = await _userManager.UpdateAsync(foundUser);
if (result.Succeeded)
return new JsonResult(new { success = true });
else
return new JsonResult(new { success = false, message = "Unable to update account type.", identityErrors = result.Errors });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpPost]
public async Task<IActionResult> UpdatePGPPublicKey(UpdatePGPPublicKeyModel 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)
{
foundUser.PGPPublicKey = model.PGPPublicKey;

var result = await _userManager.UpdateAsync(foundUser);
if (result.Succeeded)
return new JsonResult(new { success = true });
else
return new JsonResult(new { success = false, message = "Unable to update pgp public key.", identityErrors = result.Errors });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpGet]
public async Task<IActionResult> Get2FAKey(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)
{
string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(foundUser);

return new JsonResult(new { success = true, data = FormatKey(unformattedKey) });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpPost]
public async Task<IActionResult> Reset2FAKey(Reset2FAKeyModel 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)
{
await _userManager.ResetAuthenticatorKeyAsync(foundUser);
string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(foundUser);

return new JsonResult(new { success = true, data = FormatKey(unformattedKey) });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpPost]
public async Task<IActionResult> Enable2FA(Enable2FAModel model)
{
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
if (string.IsNullOrEmpty(model.Code))
return new JsonResult(new { success = false, message = "Code is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
if (foundUser != null)
{
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);

var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
foundUser, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);

if (is2faTokenValid)
{
var result = await _userManager.SetTwoFactorEnabledAsync(foundUser, true);
if (result.Succeeded)
{
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(foundUser, 10);
return new JsonResult(new { success = true, data = recoveryCodes.ToArray() });
}
else
return new JsonResult(new { success = false, message = "Unable to set Two-Factor Authentication.", identityErrors = result.Errors });
}

return new JsonResult(new { success = false, message = "Verification code is invalid." });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpPost]
public async Task<IActionResult> Disable2FA(Disable2FAModel 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.SetTwoFactorEnabledAsync(foundUser, false);
if (result.Succeeded)
return new JsonResult(new { success = true });
else
return new JsonResult(new { success = false, message = "Unable to disable Two-Factor Authentication.", identityErrors = result.Errors });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

[HttpPost]
public async Task<IActionResult> GenerateRecoveryCodes(GenerateRecoveryCodesModel 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)
{
if (foundUser.TwoFactorEnabled)
{
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(foundUser, 10);

return new JsonResult(new { success = true, data = recoveryCodes.ToArray() });
}

return new JsonResult(new { success = false, message = "Two-Factor Authentication is not enabled." });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition));
}

return result.ToString().ToLowerInvariant();
}
}
}

+ 242
- 0
IdentityServer/Data/Migrations/ApplicationDb/20181015060219_InitialApplicationDbContextMigration.Designer.cs View File

@@ -0,0 +1,242 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Teknik.IdentityServer;

namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20181015060219_InitialApplicationDbContextMigration")]
partial class InitialApplicationDbContextMigration
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-preview2-35157")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();

b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();

b.Property<string>("Name")
.HasMaxLength(256);

b.Property<string>("NormalizedName")
.HasMaxLength(256);

b.HasKey("Id");

b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");

b.ToTable("AspNetRoles");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

b.Property<string>("ClaimType");

b.Property<string>("ClaimValue");

b.Property<string>("RoleId")
.IsRequired();

b.HasKey("Id");

b.HasIndex("RoleId");

b.ToTable("AspNetRoleClaims");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

b.Property<string>("ClaimType");

b.Property<string>("ClaimValue");

b.Property<string>("UserId")
.IsRequired();

b.HasKey("Id");

b.HasIndex("UserId");

b.ToTable("AspNetUserClaims");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");

b.Property<string>("ProviderKey");

b.Property<string>("ProviderDisplayName");

b.Property<string>("UserId")
.IsRequired();

b.HasKey("LoginProvider", "ProviderKey");

b.HasIndex("UserId");

b.ToTable("AspNetUserLogins");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");

b.Property<string>("RoleId");

b.HasKey("UserId", "RoleId");

b.HasIndex("RoleId");

b.ToTable("AspNetUserRoles");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");

b.Property<string>("LoginProvider");

b.Property<string>("Name");

b.Property<string>("Value");

b.HasKey("UserId", "LoginProvider", "Name");

b.ToTable("AspNetUserTokens");
});

modelBuilder.Entity("Teknik.IdentityServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();

b.Property<int>("AccessFailedCount");

b.Property<int>("AccountStatus");

b.Property<int>("AccountType");

b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();

b.Property<DateTime>("CreationDate");

b.Property<string>("Email")
.HasMaxLength(256);

b.Property<bool>("EmailConfirmed");

b.Property<DateTime>("LastSeen");

b.Property<bool>("LockoutEnabled");

b.Property<DateTimeOffset?>("LockoutEnd");

b.Property<string>("NormalizedEmail")
.HasMaxLength(256);

b.Property<string>("NormalizedUserName")
.HasMaxLength(256);

b.Property<string>("PGPPublicKey");

b.Property<string>("PasswordHash");

b.Property<string>("PhoneNumber");

b.Property<bool>("PhoneNumberConfirmed");

b.Property<string>("SecurityStamp");

b.Property<bool>("TwoFactorEnabled");

b.Property<string>("UserName")
.HasMaxLength(256);

b.HasKey("Id");

b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");

b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");

b.ToTable("AspNetUsers");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);

b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

+ 225
- 0
IdentityServer/Data/Migrations/ApplicationDb/20181015060219_InitialApplicationDbContextMigration.cs View File

@@ -0,0 +1,225 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
{
public partial class InitialApplicationDbContextMigration : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Name = table.Column<string>(maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});

migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(nullable: false),
UserName = table.Column<string>(maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true),
Email = table.Column<string>(maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(nullable: false),
PasswordHash = table.Column<string>(nullable: true),
SecurityStamp = table.Column<string>(nullable: true),
ConcurrencyStamp = table.Column<string>(nullable: true),
PhoneNumber = table.Column<string>(nullable: true),
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
TwoFactorEnabled = table.Column<bool>(nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
LockoutEnabled = table.Column<bool>(nullable: false),
AccessFailedCount = table.Column<int>(nullable: false),
CreationDate = table.Column<DateTime>(nullable: false),
LastSeen = table.Column<DateTime>(nullable: false),
AccountType = table.Column<int>(nullable: false),
AccountStatus = table.Column<int>(nullable: false),
PGPPublicKey = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});

migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
RoleId = table.Column<string>(nullable: false),
ClaimType = table.Column<string>(nullable: true),
ClaimValue = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});

migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
UserId = table.Column<string>(nullable: false),
ClaimType = table.Column<string>(nullable: true),
ClaimValue = table.Column<string>(nullable: true)
},
constraints: table =>
{