Browse Source

Added Identity Server for authentication

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

5
.gitignore vendored

@ -262,6 +262,7 @@ __pycache__/ @@ -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

96
Configuration/Config.cs

@ -19,38 +19,38 @@ namespace Teknik.Configuration @@ -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 @@ -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
Configuration/Configuration.csproj

@ -5,6 +5,7 @@ @@ -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
Configuration/IdentityServerConfig.cs

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

2
Configuration/UserConfig.cs

@ -13,6 +13,7 @@ namespace Teknik.Configuration @@ -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 @@ -27,6 +28,7 @@ namespace Teknik.Configuration
PremiumAccountPrice = 0;
PaymentType = "Donation";
InviteCodeRequired = false;
IdentityServerConfig = new IdentityServerConfig();
}
}
}

2
GitService/GitService.csproj

@ -8,7 +8,7 @@ @@ -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
IdentityServer/ApplicationDbContext.cs

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

143
IdentityServer/Configuration.cs

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

85
IdentityServer/Content/common.css

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

307
IdentityServer/Controllers/AccountController.cs

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

75
IdentityServer/Controllers/ConsentController.cs

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

64
IdentityServer/Controllers/DefaultController.cs

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

237
IdentityServer/Controllers/ErrorController.cs

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

92
IdentityServer/Controllers/GrantsController.cs

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

29
IdentityServer/Controllers/HomeController.cs

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

420
IdentityServer/Controllers/ManageController.cs

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