diff --git a/.gitignore b/.gitignore
index 6c50423..3042a41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -262,4 +262,5 @@ __pycache__/
keys/
dist/
-appsettings.json
\ No newline at end of file
+appsettings.json
+keystore/
\ No newline at end of file
diff --git a/Notesnook.sln b/Notesnook.sln
index c104961..75c9443 100644
--- a/Notesnook.sln
+++ b/Notesnook.sln
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Data", "Stree
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Messenger", "Streetwriters.Messenger\Streetwriters.Messenger.csproj", "{BDA80415-6C8D-4481-AC31-E5B4D73E9629}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Identity", "Streetwriters.Identity\Streetwriters.Identity.csproj", "{6800DEE0-768C-4BEB-B78C-08829EC5A106}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -36,5 +38,9 @@ Global
{BDA80415-6C8D-4481-AC31-E5B4D73E9629}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDA80415-6C8D-4481-AC31-E5B4D73E9629}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BDA80415-6C8D-4481-AC31-E5B4D73E9629}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6800DEE0-768C-4BEB-B78C-08829EC5A106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6800DEE0-768C-4BEB-B78C-08829EC5A106}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6800DEE0-768C-4BEB-B78C-08829EC5A106}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6800DEE0-768C-4BEB-B78C-08829EC5A106}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index b17a35d..dec29ac 100644
--- a/README.md
+++ b/README.md
@@ -43,12 +43,18 @@ To run the `Streetwriters.Messenger` project:
dotnet run --project Streetwriters.Messenger/Streetwriters.Messenger.csproj
```
+To run the `Streetwriters.Identity` project:
+
+```bash
+dotnet run --project Streetwriters.Identity/Streetwriters.Identity.csproj
+```
+
## TODO Self-hosting
**Note: Self-hosting the Notesnook Sync Server is not yet possible. We are working to enable full on-premise self hosting so stay tuned!**
- [x] Open source the Sync server
-- [ ] Open source the Identity server
+- [x] Open source the Identity server
- [x] Open source the SSE Messaging infrastructure
- [ ] Fully Dockerize all services
- [ ] Publish on DockerHub
diff --git a/Streetwriters.Identity/Config.cs b/Streetwriters.Identity/Config.cs
new file mode 100644
index 0000000..2af8c2d
--- /dev/null
+++ b/Streetwriters.Identity/Config.cs
@@ -0,0 +1,86 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using IdentityServer4;
+using IdentityServer4.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Streetwriters.Identity
+{
+ public static class Config
+ {
+ public const string EMAIL_GRANT_TYPE = "email";
+ public const string MFA_GRANT_TYPE = "mfa";
+ public const string MFA_PASSWORD_GRANT_TYPE = "mfa_password";
+
+ public const string MFA_GRANT_TYPE_SCOPE = "auth:grant_types:mfa";
+ public const string MFA_PASSWORD_GRANT_TYPE_SCOPE = "auth:grant_types:mfa_password";
+
+ public static IEnumerable IdentityResources =>
+ new List {
+ new IdentityResources.OpenId(),
+ };
+
+ public static IEnumerable ApiResources =>
+ new List
+ {
+ new ApiResource("notesnook", "Notesnook API", new string[] { "verified" })
+ {
+ ApiSecrets = { new Secret(Environment.GetEnvironmentVariable("NOTESNOOK_API_SECRET")?.Sha256()) },
+ Scopes = { "notesnook.sync" }
+ },
+ // local API
+ new ApiResource(IdentityServerConstants.LocalApi.ScopeName)
+ };
+
+ public static IEnumerable ApiScopes =>
+ new List
+ {
+ new ApiScope("notesnook.sync", "Notesnook Syncing Access"),
+ new ApiScope(IdentityServerConstants.LocalApi.ScopeName),
+ new ApiScope(MFA_GRANT_TYPE_SCOPE, "Multi-factor authentication access"),
+ new ApiScope(MFA_PASSWORD_GRANT_TYPE_SCOPE, "Multi-factor authentication password step access")
+ };
+
+ public static IEnumerable Clients =>
+ new List
+ {
+ new Client
+ {
+ ClientName = "Notesnook",
+ ClientId = "notesnook",
+ AllowedGrantTypes = { GrantType.ResourceOwnerPassword, MFA_GRANT_TYPE, MFA_PASSWORD_GRANT_TYPE, EMAIL_GRANT_TYPE, },
+ RequirePkce = false,
+ RequireClientSecret = false,
+ RequireConsent = false,
+ AccessTokenType = AccessTokenType.Reference,
+ AllowOfflineAccess = true,
+ UpdateAccessTokenClaimsOnRefresh = true,
+ RefreshTokenUsage = TokenUsage.OneTimeOnly,
+ RefreshTokenExpiration = TokenExpiration.Absolute,
+ AccessTokenLifetime = 3600,
+
+ // scopes that client has access to
+ AllowedScopes = { "notesnook.sync", "offline_access", "openid", IdentityServerConstants.LocalApi.ScopeName, "mfa" },
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Controllers/AccountController.cs b/Streetwriters.Identity/Controllers/AccountController.cs
new file mode 100644
index 0000000..039d981
--- /dev/null
+++ b/Streetwriters.Identity/Controllers/AccountController.cs
@@ -0,0 +1,340 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using AspNetCore.Identity.Mongo.Model;
+using IdentityServer4.Configuration;
+using IdentityServer4.Stores;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using MongoDB.Bson;
+using Streetwriters.Common;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Messages;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Enums;
+using Streetwriters.Identity.Handlers;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+using static IdentityServer4.IdentityServerConstants;
+
+namespace Streetwriters.Identity.Controllers
+{
+ [ApiController]
+ [DisplayName("Account")]
+ [Route("account")]
+ [Authorize(LocalApi.PolicyName)]
+ public class AccountController : IdentityControllerBase
+ {
+ private IPersistedGrantStore PersistedGrantStore { get; set; }
+ private ITokenGenerationService TokenGenerationService { get; set; }
+ private IUserClaimsPrincipalFactory PrincipalFactory { get; set; }
+ private IdentityServerOptions ISOptions { get; set; }
+ public AccountController(UserManager _userManager, IEmailSender _emailSender,
+ SignInManager _signInManager, RoleManager _roleManager, IPersistedGrantStore store,
+ ITokenGenerationService tokenGenerationService, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
+ {
+ PersistedGrantStore = store;
+ TokenGenerationService = tokenGenerationService;
+ }
+
+ [HttpGet("confirm")]
+ [AllowAnonymous]
+ [ResponseCache(NoStore = true)]
+ public async Task ConfirmToken(string userId, string code, string clientId, TokenType type)
+ {
+ var client = Clients.FindClientById(clientId);
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.FindByIdAsync(userId);
+ if (!await IsUserValidAsync(user, clientId)) return BadRequest($"Unable to find user with ID '{userId}'.");
+
+ switch (type)
+ {
+ case TokenType.CONFRIM_EMAIL:
+ {
+ if (await UserManager.IsEmailConfirmedAsync(user)) return Ok("Email already verified.");
+
+ var result = await UserManager.ConfirmEmailAsync(user, code);
+ if (!result.Succeeded) return BadRequest(result.Errors.ToErrors());
+
+ foreach (var handler in ClientHandlers.Handlers)
+ {
+ if (await UserManager.IsInRoleAsync(user, client.Id))
+ {
+ await handler.Value.OnEmailConfirmed(userId);
+ // if (client.WelcomeEmailTemplateId != null)
+ // await EmailSender.SendWelcomeEmailAsync(user.Email, client);
+ }
+ }
+ var redirectUrl = $"{ClientHandlers.GetClientHandler(client.Type)?.EmailConfirmedRedirectURL}?userId={userId}";
+ return RedirectPermanent(redirectUrl);
+ }
+ // case TokenType.CHANGE_EMAIL:
+ // {
+ // var newEmail = user.Claims.Find((c) => c.ClaimType == "new_email");
+ // if (newEmail == null) return BadRequest("Email change was not requested.");
+
+ // var result = await UserManager.ChangeEmailAsync(user, newEmail.ClaimValue.ToString(), code);
+ // if (result.Succeeded)
+ // {
+ // await UserManager.RemoveClaimAsync(user, newEmail.ToClaim());
+ // return Ok("Email changed.");
+ // }
+ // return BadRequest("Could not change email.");
+ // }
+ case TokenType.RESET_PASSWORD:
+ {
+ if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", code))
+ return BadRequest("Invalid token.");
+
+ var authorizationCode = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode");
+ var redirectUrl = $"{ClientHandlers.GetClientHandler(client.Type)?.AccountRecoveryRedirectURL}?userId={userId}&code={authorizationCode}";
+ return RedirectPermanent(redirectUrl);
+ }
+ default:
+ return BadRequest("Invalid type.");
+ }
+
+ }
+
+ [HttpPost("verify")]
+ public async Task SendVerificationEmail()
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.GetUserAsync(User);
+ if (!await IsUserValidAsync(user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
+
+ var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
+ var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL, Request.Scheme);
+ await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
+ return Ok();
+ }
+
+ [HttpPost("unregister")]
+ public async Task UnregisterAccountAync([FromForm] DeleteAccountForm form)
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.GetUserAsync(User);
+ if (!await IsUserValidAsync(user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
+
+ if (!await UserManager.CheckPasswordAsync(user, form.Password))
+ {
+ return Unauthorized();
+ }
+
+ await UserManager.RemoveFromRoleAsync(user, client.Id);
+ return Ok();
+ }
+
+ [HttpGet]
+ public async Task GetUserAccount()
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.GetUserAsync(User);
+ if (!await IsUserValidAsync(user, client.Id))
+ return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
+
+ return Ok(new UserModel
+ {
+ UserId = user.Id.ToString(),
+ Email = user.Email,
+ IsEmailConfirmed = user.EmailConfirmed,
+ // PhoneNumber = user.PhoneNumberConfirmed ? user.PhoneNumber : null,
+ MFA = new MFAConfig
+ {
+ IsEnabled = user.TwoFactorEnabled,
+ PrimaryMethod = MFAService.GetPrimaryMethod(user),
+ SecondaryMethod = MFAService.GetSecondaryMethod(user),
+ RemainingValidCodes = await MFAService.GetRemainingValidCodesAsync(user)
+ }
+ });
+ }
+
+ [HttpPost("recover")]
+ [AllowAnonymous]
+ public async Task ResetUserPassword([FromForm] ResetPasswordForm form)
+ {
+ var client = Clients.FindClientById(form.ClientId);
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.FindByEmailAsync(form.Email);
+ if (!await IsUserValidAsync(user, form.ClientId)) return Ok();
+
+ var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword");
+ var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD, Request.Scheme);
+#if DEBUG
+ return Ok(callbackUrl);
+#else
+ await Slogger.Info("ResetUserPassword", user.Email, callbackUrl);
+ await EmailSender.SendPasswordResetEmailAsync(user.Email, callbackUrl, client);
+ return Ok();
+#endif
+ }
+
+ [HttpPost("logout")]
+ public async Task Logout()
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.GetUserAsync(User);
+ if (!await IsUserValidAsync(user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
+
+ var subjectId = User.FindFirstValue("sub");
+ var jti = User.FindFirstValue("jti");
+
+ var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter
+ {
+ ClientId = client.Id,
+ SubjectId = subjectId
+ });
+ grants = grants.Where((grant) => grant.Data.Contains(jti));
+ if (grants.Any())
+ {
+ foreach (var grant in grants)
+ {
+ await PersistedGrantStore.RemoveAsync(grant.Key);
+ }
+ }
+ return Ok();
+ }
+
+ [HttpPost("token")]
+ [AllowAnonymous]
+ public async Task GetAccessTokenFromCode([FromForm] GetAccessTokenForm form)
+ {
+ if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId.");
+ var user = await UserManager.FindByIdAsync(form.UserId);
+ if (!await IsUserValidAsync(user, form.ClientId))
+ return BadRequest($"Unable to find user with ID '{form.UserId}'.");
+
+ if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode", form.Code))
+ return BadRequest("Invalid authorization_code.");
+ var token = await TokenGenerationService.CreateAccessTokenAsync(user, form.ClientId);
+ return Ok(new
+ {
+ access_token = token,
+ expires_in = 18000
+ });
+ }
+
+ [HttpPatch]
+ public async Task UpdateAccount([FromForm] UpdateUserForm form)
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.GetUserAsync(User);
+ if (!await IsUserValidAsync(user, client.Id))
+ return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
+
+ switch (form.Type)
+ {
+ case "change_email":
+ {
+ var code = await UserManager.GenerateChangeEmailTokenAsync(user, form.NewEmail);
+ // var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CHANGE_EMAIL, Request.Scheme);
+ await EmailSender.SendChangeEmailConfirmationAsync(user.Email, code, client);
+ await UserManager.AddClaimAsync(user, new Claim("new_email", form.NewEmail));
+ return Ok();
+ }
+ case "change_password":
+ {
+ var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword);
+ if (result.Succeeded)
+ {
+ await SendPasswordChangedMessageAsync(user.Id.ToString());
+ return Ok();
+ }
+ return BadRequest(result.Errors.ToErrors());
+ }
+ case "reset_password":
+ {
+ var result = await UserManager.RemovePasswordAsync(user);
+ if (result.Succeeded)
+ {
+ result = await UserManager.AddPasswordAsync(user, form.NewPassword);
+ if (result.Succeeded)
+ {
+ await SendPasswordChangedMessageAsync(user.Id.ToString());
+ return Ok();
+ }
+ }
+ return BadRequest(result.Errors.ToErrors());
+ }
+ }
+ return BadRequest("Invalid type.");
+ }
+
+ [HttpPost("sessions/clear")]
+ public async Task ClearUserSessions([FromQuery] bool all, [FromForm] string refresh_token)
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.GetUserAsync(User);
+ if (!await IsUserValidAsync(user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id.ToString()}'.");
+
+ var jti = User.FindFirstValue("jti");
+
+ var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter
+ {
+ ClientId = client.Id,
+ SubjectId = user.Id.ToString()
+ });
+ foreach (var grant in grants)
+ {
+ if (!all && (grant.Data.Contains(jti) || grant.Data.Contains(refresh_token))) continue;
+ await PersistedGrantStore.RemoveAsync(grant.Key);
+ }
+ return Ok();
+ }
+
+ private async Task SendPasswordChangedMessageAsync(string userId)
+ {
+ await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage
+ {
+ UserId = userId,
+ OriginTokenId = User.FindFirstValue("jti"),
+ Message = new Message
+ {
+ Type = "userPasswordChanged"
+ }
+ });
+ }
+
+ public async Task IsUserValidAsync(User user, string clientId)
+ {
+ return user != null && await UserManager.IsInRoleAsync(user, clientId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Controllers/IdentityControllerBase.cs b/Streetwriters.Identity/Controllers/IdentityControllerBase.cs
new file mode 100644
index 0000000..7918b91
--- /dev/null
+++ b/Streetwriters.Identity/Controllers/IdentityControllerBase.cs
@@ -0,0 +1,67 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Collections.Generic;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using AspNetCore.Identity.Mongo.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+
+namespace Streetwriters.Identity.Controllers
+{
+ public abstract class IdentityControllerBase : ControllerBase
+ {
+ protected UserManager UserManager { get; set; }
+ protected SignInManager SignInManager { get; set; }
+ protected RoleManager RoleManager { get; set; }
+ protected IEmailSender EmailSender { get; set; }
+ protected UrlEncoder UrlEncoder { get; set; }
+ protected IMFAService MFAService { get; set; }
+ public IdentityControllerBase(
+ UserManager _userManager,
+ IEmailSender _emailSender,
+ SignInManager _signInManager,
+ RoleManager _roleManager,
+ IMFAService _mfaService
+ )
+ {
+ UserManager = _userManager;
+ SignInManager = _signInManager;
+ RoleManager = _roleManager;
+ EmailSender = _emailSender;
+ MFAService = _mfaService;
+ UrlEncoder = UrlEncoder.Default;
+ }
+
+ public override BadRequestObjectResult BadRequest(object error)
+ {
+ if (error is IEnumerable errors)
+ {
+ return base.BadRequest(new { errors });
+ }
+ return base.BadRequest(new { error });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Controllers/MFAController.cs b/Streetwriters.Identity/Controllers/MFAController.cs
new file mode 100644
index 0000000..95859e2
--- /dev/null
+++ b/Streetwriters.Identity/Controllers/MFAController.cs
@@ -0,0 +1,138 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using AspNetCore.Identity.Mongo.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Streetwriters.Common;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+using Streetwriters.Identity.Services;
+using static IdentityServer4.IdentityServerConstants;
+
+namespace Streetwriters.Identity.Controllers
+{
+ [ApiController]
+ [Route("mfa")]
+ [Authorize(LocalApi.PolicyName)]
+ public class MFAController : IdentityControllerBase
+ {
+ public MFAController(UserManager _userManager, IEmailSender _emailSender,
+ SignInManager _signInManager, RoleManager _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) { }
+
+ [HttpPost]
+ public async Task SetupAuthenticator([FromForm] MultiFactorSetupForm form)
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.GetUserAsync(User);
+
+ try
+ {
+ switch (form.Type)
+ {
+ case "app":
+ var authenticatorDetails = await MFAService.GetAuthenticatorDetailsAsync(user, client);
+ return Ok(authenticatorDetails);
+ case "sms":
+ case "email":
+ await MFAService.SendOTPAsync(user, client, form, true);
+ return Ok();
+ default:
+ return BadRequest("Invalid authenticator type.");
+ }
+ }
+ catch (Exception ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ [HttpDelete]
+ public async Task Disable2FA()
+ {
+ var user = await UserManager.GetUserAsync(User);
+
+ if (!await UserManager.GetTwoFactorEnabledAsync(user))
+ {
+ return BadRequest("Cannot disable 2FA as it's not currently enabled");
+ }
+
+ if (await MFAService.DisableMFAAsync(user))
+ {
+ return Ok();
+ }
+
+ return BadRequest("Failed to disable 2FA.");
+ }
+
+ [HttpGet("codes")]
+ public async Task GetRecoveryCodes()
+ {
+ var user = await UserManager.GetUserAsync(User);
+ if (!await UserManager.GetTwoFactorEnabledAsync(user)) return BadRequest("Please enable 2FA.");
+ return Ok(await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 16));
+ }
+
+ [HttpPost("send")]
+ [Authorize("mfa")]
+ [Authorize(LocalApi.PolicyName)]
+ public async Task RequestCode([FromForm] string type)
+ {
+ var client = Clients.FindClientById(User.FindFirstValue("client_id"));
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ var user = await UserManager.FindByIdAsync(User.FindFirstValue("sub"));
+ if (user == null) return Ok(); // We cannot expose that the user doesn't exist.
+
+ await MFAService.SendOTPAsync(user, client, new MultiFactorSetupForm
+ {
+ Type = type,
+ PhoneNumber = user.PhoneNumber
+ });
+ return Ok();
+ }
+
+ [HttpPatch]
+ public async Task EnableAuthenticator([FromForm] MultiFactorEnableForm form)
+ {
+ var user = await UserManager.GetUserAsync(User);
+
+ if (!await MFAService.VerifyOTPAsync(user, form.VerificationCode, form.Type))
+ return BadRequest("Invalid verification code.");
+
+ if (form.IsFallback)
+ await MFAService.SetSecondaryMethodAsync(user, form.Type);
+ else
+ await MFAService.EnableMFAAsync(user, form.Type);
+
+ return Ok();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Controllers/SignupController.cs b/Streetwriters.Identity/Controllers/SignupController.cs
new file mode 100644
index 0000000..f6957a3
--- /dev/null
+++ b/Streetwriters.Identity/Controllers/SignupController.cs
@@ -0,0 +1,115 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using AspNetCore.Identity.Mongo.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Streetwriters.Common;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Enums;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+
+namespace Streetwriters.Identity.Controllers
+{
+ [ApiController]
+ [Route("signup")]
+ public class SignupController : IdentityControllerBase
+ {
+ public SignupController(UserManager _userManager, IEmailSender _emailSender,
+ SignInManager _signInManager, RoleManager _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
+ { }
+
+ private async Task AddClientRoleAsync(string clientId)
+ {
+ if (await RoleManager.FindByNameAsync(clientId) == null)
+ await RoleManager.CreateAsync(new MongoRole(clientId));
+ }
+
+ [HttpPost]
+ [AllowAnonymous]
+ public async Task Signup([FromForm] SignupForm form)
+ {
+ var client = Clients.FindClientById(form.ClientId);
+ if (client == null) return BadRequest("Invalid client_id.");
+
+ await AddClientRoleAsync(client.Id);
+
+ // email addresses must be case-insensitive
+ form.Email = form.Email.ToLowerInvariant();
+ form.Username = form.Username?.ToLowerInvariant();
+
+ var result = await UserManager.CreateAsync(new User
+ {
+ Email = form.Email,
+ EmailConfirmed = false,
+ UserName = form.Username ?? form.Email,
+ }, form.Password);
+
+ if (result.Errors.Any((e) => e.Code == "DuplicateEmail"))
+ {
+ var user = await UserManager.FindByEmailAsync(form.Email);
+
+ if (!await UserManager.IsInRoleAsync(user, client.Id))
+ {
+ if (!await UserManager.CheckPasswordAsync(user, form.Password))
+ {
+ // TODO
+ await UserManager.RemovePasswordAsync(user);
+ await UserManager.AddPasswordAsync(user, form.Password);
+ }
+ await UserManager.AddToRoleAsync(user, client.Id);
+ }
+ else
+ {
+ return BadRequest(new string[] { "Email is invalid or already taken." });
+ }
+
+ return Ok(new
+ {
+ userId = user.Id.ToString()
+ });
+ }
+
+ if (result.Succeeded)
+ {
+ var user = await UserManager.FindByEmailAsync(form.Email);
+
+ await UserManager.AddToRoleAsync(user, client.Id);
+ // await UserManager.AddClaimAsync(user, new Claim("verified", "false"));
+
+ var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
+ var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL, Request.Scheme);
+ await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
+
+ return Ok(new
+ {
+ userId = user.Id.ToString()
+ });
+ }
+
+ return BadRequest(result.Errors.ToErrors());
+ }
+ }
+}
diff --git a/Streetwriters.Identity/Enums/TokenTypes.cs b/Streetwriters.Identity/Enums/TokenTypes.cs
new file mode 100644
index 0000000..2cbb05c
--- /dev/null
+++ b/Streetwriters.Identity/Enums/TokenTypes.cs
@@ -0,0 +1,28 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+namespace Streetwriters.Identity.Enums
+{
+ public enum TokenType
+ {
+ CONFRIM_EMAIL = 0,
+ RESET_PASSWORD = 1,
+ CHANGE_EMAIL = 2,
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Extensions/HttpContextExtensions.cs b/Streetwriters.Identity/Extensions/HttpContextExtensions.cs
new file mode 100644
index 0000000..a35bafc
--- /dev/null
+++ b/Streetwriters.Identity/Extensions/HttpContextExtensions.cs
@@ -0,0 +1,56 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Text;
+using Ng.Services;
+
+namespace Microsoft.AspNetCore.Http
+{
+ public static class HttpContextExtensions
+ {
+ static UserAgentService userAgentService = new UserAgentService();
+ public static string GetClientInfo(this HttpContext httpContext)
+ {
+ var clientIp = httpContext.Connection.RemoteIpAddress;
+ var country = httpContext.Request.Headers["CF-IPCountry"];
+ var userAgent = httpContext.Request.Headers["User-Agent"];
+ var builder = new StringBuilder();
+
+ builder.AppendLine($"Date: {DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")}");
+
+ if (clientIp != null)
+ builder.AppendLine($"IP: {clientIp.ToString()}");
+
+ if (!string.IsNullOrEmpty(country))
+ builder.AppendLine($"Country: {country.ToString()}");
+
+ if (!string.IsNullOrEmpty(userAgent))
+ {
+ var ua = userAgentService.Parse(userAgent);
+ if (!string.IsNullOrEmpty(ua.Browser))
+ builder.AppendLine($"Browser: {ua.Browser} {ua.BrowserVersion}");
+ if (!string.IsNullOrEmpty(ua.Platform))
+ builder.AppendLine($"Platform: {ua.Platform}");
+ }
+
+ return builder.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Extensions/IEnumerableExtensions.cs b/Streetwriters.Identity/Extensions/IEnumerableExtensions.cs
new file mode 100644
index 0000000..d5561bb
--- /dev/null
+++ b/Streetwriters.Identity/Extensions/IEnumerableExtensions.cs
@@ -0,0 +1,42 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Streetwriters.Identity.Controllers;
+
+namespace System.Collections.Generic
+{
+ public static class IEnumberableExtensions
+ {
+ public static IEnumerable ToErrors(this IEnumerable collection)
+ {
+ return collection.Select((e) => e.Description);
+ }
+
+ public static string GetClaimValue(this IEnumerable claims, string type)
+ {
+ return claims.FirstOrDefault((c) => c.Type == type)?.Value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Extensions/IntExtensions.cs b/Streetwriters.Identity/Extensions/IntExtensions.cs
new file mode 100644
index 0000000..92ca5af
--- /dev/null
+++ b/Streetwriters.Identity/Extensions/IntExtensions.cs
@@ -0,0 +1,34 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Text;
+using Ng.Services;
+
+namespace System
+{
+ public static class IntExtensions
+ {
+ public static string Pluralize(this int value, string singular, string plural)
+ {
+ // if (value == null) return $"0 {plural}";
+ return value == 1 ? $"{value} {singular}" : $"{value} {plural}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Extensions/MongoDBTicketStore.cs b/Streetwriters.Identity/Extensions/MongoDBTicketStore.cs
new file mode 100644
index 0000000..9c90eee
--- /dev/null
+++ b/Streetwriters.Identity/Extensions/MongoDBTicketStore.cs
@@ -0,0 +1,73 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.Extensions.Caching.Memory;
+using System;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class MemoryCacheTicketStore : ITicketStore
+ {
+ private const string KeyPrefix = "AuthSessionStore";
+ private IMemoryCache _cache;
+
+ public MemoryCacheTicketStore()
+ {
+ _cache = new MemoryCache(new MemoryCacheOptions());
+ }
+
+ Task ITicketStore.RemoveAsync(string key)
+ {
+ _cache.Remove(key);
+ return Task.FromResult(true);
+ }
+
+ Task ITicketStore.RenewAsync(string key, AuthenticationTicket ticket)
+ {
+ var options = new MemoryCacheEntryOptions();
+ var expiresUtc = ticket.Properties.ExpiresUtc;
+ if (expiresUtc.HasValue)
+ {
+ options.SetAbsoluteExpiration(expiresUtc.Value);
+ }
+ options.SetSlidingExpiration(TimeSpan.FromHours(1));
+
+ _cache.Set(key, ticket, options);
+
+ return Task.FromResult(true);
+ }
+
+ Task ITicketStore.RetrieveAsync(string key)
+ {
+ AuthenticationTicket ticket;
+ _cache.TryGetValue(key, out ticket);
+ return Task.FromResult(ticket);
+ }
+
+ async Task ITicketStore.StoreAsync(AuthenticationTicket ticket)
+ {
+ var id = Guid.NewGuid();
+ var key = KeyPrefix + id;
+ await ((ITicketStore)this).RenewAsync(key, ticket);
+ return key;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Extensions/UrlExtensions.cs b/Streetwriters.Identity/Extensions/UrlExtensions.cs
new file mode 100644
index 0000000..28ed67d
--- /dev/null
+++ b/Streetwriters.Identity/Extensions/UrlExtensions.cs
@@ -0,0 +1,48 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Streetwriters.Common;
+using Streetwriters.Identity.Controllers;
+using Streetwriters.Identity.Enums;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public static class UrlHelperExtensions
+ {
+ public static string TokenLink(this IUrlHelper urlHelper, string userId, string code, string clientId, TokenType type, string scheme)
+ {
+
+ return urlHelper.ActionLink(
+#if DEBUG
+ host: $"{Servers.IdentityServer.Hostname}:{Servers.IdentityServer.Port}",
+#else
+ host: Servers.IdentityServer.Domain,
+#endif
+ action: nameof(AccountController.ConfirmToken),
+ controller: "Account",
+ values: new { userId, code, clientId, type },
+ protocol: scheme);
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Handlers/ClientHandlers.cs b/Streetwriters.Identity/Handlers/ClientHandlers.cs
new file mode 100644
index 0000000..b9828d8
--- /dev/null
+++ b/Streetwriters.Identity/Handlers/ClientHandlers.cs
@@ -0,0 +1,39 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Collections.Generic;
+using Streetwriters.Common;
+using Streetwriters.Common.Enums;
+using Streetwriters.Identity.Interfaces;
+
+namespace Streetwriters.Identity.Handlers
+{
+ public class ClientHandlers
+ {
+ public static Dictionary Handlers { get; set; } = new Dictionary
+ {
+ { ApplicationType.NOTESNOOK, new NotesnookHandler() }
+ };
+
+ public static IAppHandler GetClientHandler(ApplicationType type)
+ {
+ return Handlers[type];
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Handlers/NotesnookHandler.cs b/Streetwriters.Identity/Handlers/NotesnookHandler.cs
new file mode 100644
index 0000000..b440865
--- /dev/null
+++ b/Streetwriters.Identity/Handlers/NotesnookHandler.cs
@@ -0,0 +1,58 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Threading.Tasks;
+using Streetwriters.Common;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Messages;
+using Streetwriters.Identity.Interfaces;
+
+namespace Streetwriters.Identity.Handlers
+{
+ public class NotesnookHandler : IAppHandler
+ {
+ public string Host { get; }
+ public string EmailConfirmedRedirectURL { get; }
+ public string AccountRecoveryRedirectURL { get; }
+
+ public NotesnookHandler()
+ {
+#if DEBUG
+ Host = "http://localhost:3000";
+#else
+ Host = "https://app.notesnook.com";
+#endif
+ EmailConfirmedRedirectURL = $"{this.Host}/account/verified";
+ AccountRecoveryRedirectURL = $"{this.Host}/account/recovery";
+ }
+ public async Task OnEmailConfirmed(string userId)
+ {
+ await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage
+ {
+ UserId = userId,
+ Message = new Message
+ {
+ Type = "emailConfirmed",
+ Data = null
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Handlers/TokenResponseHandler.cs b/Streetwriters.Identity/Handlers/TokenResponseHandler.cs
new file mode 100644
index 0000000..e8c3e07
--- /dev/null
+++ b/Streetwriters.Identity/Handlers/TokenResponseHandler.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+using IdentityServer4.Services;
+using IdentityServer4.Stores;
+using IdentityServer4.Validation;
+using Microsoft.Extensions.Logging;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+
+namespace IdentityServer4.ResponseHandling
+{
+ ///
+ /// The default token response generator
+ ///
+ ///
+ public class TokenResponseHandler : TokenResponseGenerator, ITokenResponseGenerator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The clock.
+ /// The token service.
+ /// The refresh token service.
+ /// The scope parser.
+ /// The resources.
+ /// The clients.
+ /// The logger.
+ public TokenResponseHandler(ISystemClock clock, ITokenService tokenService, IRefreshTokenService refreshTokenService, IScopeParser scopeParser, IResourceStore resources, IClientStore clients, ILogger logger)
+ : base(clock, tokenService, refreshTokenService, scopeParser, resources, clients, logger)
+ {
+
+ }
+
+ protected override async Task ProcessRefreshTokenRequestAsync(TokenRequestValidationResult request)
+ {
+ var response = await base.ProcessRefreshTokenRequestAsync(request);
+ // Fixes: https://github.com/IdentityServer/IdentityServer3/issues/3621
+ response.IdentityToken = null;
+ return response;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Helpers/PasswordHelper.cs b/Streetwriters.Identity/Helpers/PasswordHelper.cs
new file mode 100644
index 0000000..df039e7
--- /dev/null
+++ b/Streetwriters.Identity/Helpers/PasswordHelper.cs
@@ -0,0 +1,38 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Text;
+using Sodium;
+
+namespace Streetwriters.Identity.Helpers
+{
+ internal class PasswordHelper
+ {
+ public static bool VerifyPassword(string password, string hash)
+ {
+ return PasswordHash.ArgonHashStringVerify(hash, password);
+ }
+
+ public static string CreatePasswordHash(string password)
+ {
+ return PasswordHash.ArgonHashString(password, 3, 65536);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Interfaces/IAppHandler.cs b/Streetwriters.Identity/Interfaces/IAppHandler.cs
new file mode 100644
index 0000000..ca8b3cb
--- /dev/null
+++ b/Streetwriters.Identity/Interfaces/IAppHandler.cs
@@ -0,0 +1,31 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Threading.Tasks;
+
+namespace Streetwriters.Identity.Interfaces
+{
+ public interface IAppHandler
+ {
+ string Host { get; }
+ string EmailConfirmedRedirectURL { get; }
+ string AccountRecoveryRedirectURL { get; }
+ Task OnEmailConfirmed(string userId);
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Interfaces/IEmailSender.cs b/Streetwriters.Identity/Interfaces/IEmailSender.cs
new file mode 100644
index 0000000..03e8ef7
--- /dev/null
+++ b/Streetwriters.Identity/Interfaces/IEmailSender.cs
@@ -0,0 +1,34 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Threading.Tasks;
+using Streetwriters.Common.Interfaces;
+
+namespace Streetwriters.Identity.Interfaces
+{
+ public interface IEmailSender
+ {
+ Task SendWelcomeEmailAsync(string email, IClient client);
+ Task SendConfirmationEmailAsync(string email, string callbackUrl, IClient client);
+ Task SendChangeEmailConfirmationAsync(string email, string code, IClient client);
+ Task SendPasswordResetEmailAsync(string email, string callbackUrl, IClient client);
+ Task Send2FACodeEmailAsync(string email, string code, IClient client);
+ Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client);
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Interfaces/IEmailTemplate.cs b/Streetwriters.Identity/Interfaces/IEmailTemplate.cs
new file mode 100644
index 0000000..25cc23a
--- /dev/null
+++ b/Streetwriters.Identity/Interfaces/IEmailTemplate.cs
@@ -0,0 +1,31 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+namespace Streetwriters.Identity.Interfaces
+{
+ public interface IEmailTemplate
+ {
+ string Subject { get; set; }
+ string Html { get; set; }
+ string Text { get; set; }
+ int? Id { get; set; }
+ object Data { get; set; }
+ long? SendAt { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Interfaces/IMFAService.cs b/Streetwriters.Identity/Interfaces/IMFAService.cs
new file mode 100644
index 0000000..1025234
--- /dev/null
+++ b/Streetwriters.Identity/Interfaces/IMFAService.cs
@@ -0,0 +1,40 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Threading.Tasks;
+using Streetwriters.Common.Interfaces;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Models;
+
+namespace Streetwriters.Identity.Interfaces
+{
+ public interface IMFAService
+ {
+ Task EnableMFAAsync(User user, string primaryMethod);
+ Task DisableMFAAsync(User user);
+ Task SetSecondaryMethodAsync(User user, string secondaryMethod);
+ string GetPrimaryMethod(User user);
+ string GetSecondaryMethod(User user);
+ Task GetRemainingValidCodesAsync(User user);
+ bool IsValidMFAMethod(string method);
+ Task GetAuthenticatorDetailsAsync(User user, IClient client);
+ Task SendOTPAsync(User user, IClient client, MultiFactorSetupForm form, bool isSetup = false);
+ Task VerifyOTPAsync(User user, string code, string method);
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Interfaces/ISMSSender.cs b/Streetwriters.Identity/Interfaces/ISMSSender.cs
new file mode 100644
index 0000000..7f765d6
--- /dev/null
+++ b/Streetwriters.Identity/Interfaces/ISMSSender.cs
@@ -0,0 +1,30 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Threading.Tasks;
+using Streetwriters.Common.Interfaces;
+
+namespace Streetwriters.Identity.Interfaces
+{
+ public interface ISMSSender
+ {
+ string SendOTP(string number, IClient client);
+ bool VerifyOTP(string id, string code);
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Interfaces/ITokenGenerationService.cs b/Streetwriters.Identity/Interfaces/ITokenGenerationService.cs
new file mode 100644
index 0000000..1343e69
--- /dev/null
+++ b/Streetwriters.Identity/Interfaces/ITokenGenerationService.cs
@@ -0,0 +1,33 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Security.Claims;
+using System.Threading.Tasks;
+using IdentityServer4.Validation;
+using Streetwriters.Common.Models;
+
+namespace Streetwriters.Identity.Interfaces
+{
+ public interface ITokenGenerationService
+ {
+ Task CreateAccessTokenAsync(User user, string clientId);
+ Task CreateAccessTokenFromValidatedRequestAsync(ValidatedTokenRequest validatedRequest, User user, string[] scopes, int lifetime = 60);
+ Task TransformTokenRequestAsync(ValidatedTokenRequest request, User user, string grantType, string[] scopes, int lifetime = 20 * 60);
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/MessageHandlers/CreateSubscription.cs b/Streetwriters.Identity/MessageHandlers/CreateSubscription.cs
new file mode 100644
index 0000000..d1b5ec3
--- /dev/null
+++ b/Streetwriters.Identity/MessageHandlers/CreateSubscription.cs
@@ -0,0 +1,56 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Threading.Tasks;
+using Streetwriters.Common.Messages;
+using Streetwriters.Common.Models;
+using Streetwriters.Common;
+using System.Text.Json;
+using Streetwriters.Data.Repositories;
+using Streetwriters.Data.Interfaces;
+using Streetwriters.Common.Interfaces;
+using System;
+using Microsoft.AspNetCore.Identity;
+using Streetwriters.Common.Enums;
+using System.Security.Claims;
+using System.Linq;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Services;
+
+namespace Streetwriters.Identity.MessageHandlers
+{
+ public class CreateSubscription
+ {
+ public static async Task Process(CreateSubscriptionMessage message, UserManager userManager)
+ {
+ var user = await userManager.FindByIdAsync(message.UserId);
+ var client = Clients.FindClientByAppId(message.AppId);
+ if (client == null || user == null) return;
+
+ IdentityUserClaim statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == $"{client.Id}:status");
+ Claim subscriptionClaim = UserService.SubscriptionTypeToClaim(client.Id, message.Type);
+ if (statusClaim?.ClaimValue == subscriptionClaim.Value) return;
+ if (statusClaim != null)
+ await userManager.ReplaceClaimAsync(user, statusClaim.ToClaim(), subscriptionClaim);
+ else
+ await userManager.AddClaimAsync(user, subscriptionClaim);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/MessageHandlers/DeleteSubscription.cs b/Streetwriters.Identity/MessageHandlers/DeleteSubscription.cs
new file mode 100644
index 0000000..a413918
--- /dev/null
+++ b/Streetwriters.Identity/MessageHandlers/DeleteSubscription.cs
@@ -0,0 +1,53 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Threading.Tasks;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Messages;
+using Streetwriters.Common.Interfaces;
+using Streetwriters.Common.Models;
+using Streetwriters.Common;
+using System.Text.Json;
+using System.IO;
+using Streetwriters.Data.Repositories;
+using Streetwriters.Data.Interfaces;
+using Microsoft.AspNetCore.Identity;
+using System.Linq;
+using IdentityServer4.Stores;
+using Streetwriters.Identity.Interfaces;
+
+namespace Streetwriters.Identity.MessageHandlers
+{
+ public class DeleteSubscription
+ {
+ public static async Task Process(DeleteSubscriptionMessage message, UserManager userManager)
+ {
+ var user = await userManager.FindByIdAsync(message.UserId);
+ var client = Clients.FindClientByAppId(message.AppId);
+ if (client != null || user != null) return;
+
+ IdentityUserClaim statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == $"{client.Id}:status");
+ if (statusClaim != null)
+ {
+ await userManager.RemoveClaimAsync(user, statusClaim.ToClaim());
+ }
+ }
+ }
+}
diff --git a/Streetwriters.Identity/Models/AuthenticatorDetails.cs b/Streetwriters.Identity/Models/AuthenticatorDetails.cs
new file mode 100644
index 0000000..a149420
--- /dev/null
+++ b/Streetwriters.Identity/Models/AuthenticatorDetails.cs
@@ -0,0 +1,34 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+namespace Streetwriters.Identity.Models
+{
+ public class AuthenticatorDetails
+ {
+ public string SharedKey
+ {
+ get; set;
+ }
+
+ public string AuthenticatorUri
+ {
+ get; set;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/ChangeEmailForm.cs b/Streetwriters.Identity/Models/ChangeEmailForm.cs
new file mode 100644
index 0000000..2c69ffb
--- /dev/null
+++ b/Streetwriters.Identity/Models/ChangeEmailForm.cs
@@ -0,0 +1,39 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Streetwriters.Identity.Models
+{
+ public class ChangeEmailForm
+ {
+ [Required]
+ [BindProperty(Name = "email")]
+ [EmailAddress]
+ public string NewEmail
+ {
+ get; set;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/DeleteAccountForm.cs b/Streetwriters.Identity/Models/DeleteAccountForm.cs
new file mode 100644
index 0000000..9a8b999
--- /dev/null
+++ b/Streetwriters.Identity/Models/DeleteAccountForm.cs
@@ -0,0 +1,33 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+
+namespace Streetwriters.Identity.Models
+{
+ public class DeleteAccountForm
+ {
+ [Required]
+ public string Password
+ {
+ get; set;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/EmailTemplate.cs b/Streetwriters.Identity/Models/EmailTemplate.cs
new file mode 100644
index 0000000..660fb23
--- /dev/null
+++ b/Streetwriters.Identity/Models/EmailTemplate.cs
@@ -0,0 +1,33 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using Streetwriters.Identity.Interfaces;
+
+namespace Streetwriters.Identity.Models
+{
+ public class EmailTemplate : IEmailTemplate
+ {
+ public int? Id { get; set; }
+ public object Data { get; set; }
+ public long? SendAt { get; set; }
+ public string Subject { get; set; }
+ public string Html { get; set; }
+ public string Text { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/GetAccessTokenForm.cs b/Streetwriters.Identity/Models/GetAccessTokenForm.cs
new file mode 100644
index 0000000..c2a53c9
--- /dev/null
+++ b/Streetwriters.Identity/Models/GetAccessTokenForm.cs
@@ -0,0 +1,49 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Streetwriters.Identity.Models
+{
+ public class GetAccessTokenForm
+ {
+ [Required]
+ [BindProperty(Name = "authorization_code")]
+ public string Code
+ {
+ get; set;
+ }
+
+ [Required]
+ [BindProperty(Name = "user_id")]
+ public string UserId
+ {
+ get; set;
+ }
+
+ [Required]
+ [BindProperty(Name = "client_id")]
+ public string ClientId
+ {
+ get; set;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/MFAPasswordRequiredResponse.cs b/Streetwriters.Identity/Models/MFAPasswordRequiredResponse.cs
new file mode 100644
index 0000000..35e7c8c
--- /dev/null
+++ b/Streetwriters.Identity/Models/MFAPasswordRequiredResponse.cs
@@ -0,0 +1,29 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Text.Json.Serialization;
+
+namespace Streetwriters.Identity.Models
+{
+ public class MFAPasswordRequiredResponse
+ {
+ [JsonPropertyName("token")]
+ public string Token { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/MFARequiredResponse.cs b/Streetwriters.Identity/Models/MFARequiredResponse.cs
new file mode 100644
index 0000000..c374770
--- /dev/null
+++ b/Streetwriters.Identity/Models/MFARequiredResponse.cs
@@ -0,0 +1,35 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Text.Json.Serialization;
+
+namespace Streetwriters.Identity.Models
+{
+ public class MFARequiredResponse
+ {
+ [JsonPropertyName("primaryMethod")]
+ public string PrimaryMethod { get; set; }
+ [JsonPropertyName("secondaryMethod")]
+ public string SecondaryMethod { get; set; }
+ [JsonPropertyName("token")]
+ public string Token { get; set; }
+ [JsonPropertyName("phoneNumber")]
+ public string PhoneNumber { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/MessageBirdOptions.cs b/Streetwriters.Identity/Models/MessageBirdOptions.cs
new file mode 100644
index 0000000..1dd3569
--- /dev/null
+++ b/Streetwriters.Identity/Models/MessageBirdOptions.cs
@@ -0,0 +1,30 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Streetwriters.Identity.Models
+{
+ public class MessageBirdOptions
+ {
+ public string AccessKey { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/MultiFactorEnableForm.cs b/Streetwriters.Identity/Models/MultiFactorEnableForm.cs
new file mode 100644
index 0000000..333e572
--- /dev/null
+++ b/Streetwriters.Identity/Models/MultiFactorEnableForm.cs
@@ -0,0 +1,43 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Streetwriters.Identity.Models
+{
+ public class MultiFactorEnableForm
+ {
+ [Required]
+ [DataType(DataType.Text)]
+ [Display(Name = "Authenticator type")]
+ [BindProperty(Name = "type")]
+ public string Type { get; set; }
+
+ [Required]
+ [StringLength(6, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Verification Code")]
+ [BindProperty(Name = "code")]
+ public string VerificationCode { get; set; }
+
+ [BindProperty(Name = "isFallback")]
+ public bool IsFallback { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/MultiFactorSetupForm.cs b/Streetwriters.Identity/Models/MultiFactorSetupForm.cs
new file mode 100644
index 0000000..37c5768
--- /dev/null
+++ b/Streetwriters.Identity/Models/MultiFactorSetupForm.cs
@@ -0,0 +1,37 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Streetwriters.Identity.Models
+{
+ public class MultiFactorSetupForm
+ {
+ [Required]
+ [Display(Name = "Authenticator type")]
+ [BindProperty(Name = "type")]
+ public string Type { get; set; }
+
+ [Display(Name = "Phone number")]
+ [BindProperty(Name = "phoneNumber")]
+ public string PhoneNumber { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/ResetPasswordForm.cs b/Streetwriters.Identity/Models/ResetPasswordForm.cs
new file mode 100644
index 0000000..fa95424
--- /dev/null
+++ b/Streetwriters.Identity/Models/ResetPasswordForm.cs
@@ -0,0 +1,42 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Streetwriters.Identity.Models
+{
+ public class ResetPasswordForm
+ {
+ [Required]
+ [BindProperty(Name = "email")]
+ public string Email
+ {
+ get; set;
+ }
+
+ [Required]
+ [BindProperty(Name = "client_id")]
+ public string ClientId
+ {
+ get; set;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/SignupForm.cs b/Streetwriters.Identity/Models/SignupForm.cs
new file mode 100644
index 0000000..4e248ea
--- /dev/null
+++ b/Streetwriters.Identity/Models/SignupForm.cs
@@ -0,0 +1,57 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Streetwriters.Identity.Models
+{
+ public class SignupForm
+ {
+ [Required]
+ [StringLength(120, ErrorMessage = "Password must be longer than or equal to 8 characters.", MinimumLength = 8)]
+ [BindProperty(Name = "password")]
+ public string Password
+ {
+ get; set;
+ }
+
+ [Required]
+ [BindProperty(Name = "email")]
+ [EmailAddress]
+ public string Email
+ {
+ get; set;
+ }
+
+ [BindProperty(Name = "username")]
+ public string Username
+ {
+ get; set;
+ }
+
+ [Required]
+ [BindProperty(Name = "client_id")]
+ public string ClientId
+ {
+ get; set;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/SmtpOptions.cs b/Streetwriters.Identity/Models/SmtpOptions.cs
new file mode 100644
index 0000000..c5df27c
--- /dev/null
+++ b/Streetwriters.Identity/Models/SmtpOptions.cs
@@ -0,0 +1,33 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Streetwriters.Identity.Models
+{
+ public class SmtpOptions
+ {
+ public string Username { get; set; }
+ public string Password { get; set; }
+ public string Host { get; set; }
+ public int Port { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/TwoFactorLoginForm.cs b/Streetwriters.Identity/Models/TwoFactorLoginForm.cs
new file mode 100644
index 0000000..5bd075e
--- /dev/null
+++ b/Streetwriters.Identity/Models/TwoFactorLoginForm.cs
@@ -0,0 +1,41 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Streetwriters.Identity.Models
+{
+ public class TwoFactorLoginForm
+ {
+ [Required]
+ [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Authenticator code")]
+ [BindProperty(Name = "code")]
+ public string Code { get; set; }
+
+ [BindProperty(Name = "rememberMachine")]
+ public bool RememberMachine { get; set; }
+
+ [BindProperty(Name = "isRecoveryCode")]
+ public bool IsRecoveryCode { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Models/UpdateUserForm.cs b/Streetwriters.Identity/Models/UpdateUserForm.cs
new file mode 100644
index 0000000..0822601
--- /dev/null
+++ b/Streetwriters.Identity/Models/UpdateUserForm.cs
@@ -0,0 +1,53 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Streetwriters.Identity.Models
+{
+ public class UpdateUserForm
+ {
+ [Required]
+ [BindProperty(Name = "type")]
+ public string Type
+ {
+ get; set;
+ }
+
+ [BindProperty(Name = "old_password")]
+ public string OldPassword
+ {
+ get; set;
+ }
+
+ [BindProperty(Name = "new_password")]
+ public string NewPassword
+ {
+ get; set;
+ }
+
+ [BindProperty(Name = "new_email")]
+ public string NewEmail
+ {
+ get; set;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Program.cs b/Streetwriters.Identity/Program.cs
new file mode 100644
index 0000000..5b3a0d0
--- /dev/null
+++ b/Streetwriters.Identity/Program.cs
@@ -0,0 +1,52 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Streetwriters.Common;
+
+namespace Streetwriters.Identity
+{
+ public class Program
+ {
+ public static async Task Main(string[] args)
+ {
+ IHost host = CreateHostBuilder(args).Build();
+ await host.RunAsync();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureLogging((options) =>
+ {
+ options.AddConsole();
+ options.AddSimpleConsole();
+ })
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup().UseUrls(Servers.IdentityServer.ToString());
+ });
+ }
+}
diff --git a/Streetwriters.Identity/Properties/launchSettings.json b/Streetwriters.Identity/Properties/launchSettings.json
new file mode 100644
index 0000000..34d594e
--- /dev/null
+++ b/Streetwriters.Identity/Properties/launchSettings.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:10770",
+ "sslPort": 44374
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "weatherforecast",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Streetwriters.Identity": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "weatherforecast",
+ "applicationUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Streetwriters.Identity/Services/CustomIntrospectionResponseGenerator.cs b/Streetwriters.Identity/Services/CustomIntrospectionResponseGenerator.cs
new file mode 100644
index 0000000..994d029
--- /dev/null
+++ b/Streetwriters.Identity/Services/CustomIntrospectionResponseGenerator.cs
@@ -0,0 +1,67 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Threading.Tasks;
+using IdentityServer4.Endpoints.Results;
+using IdentityServer4.Models;
+using IdentityServer4.ResponseHandling;
+using IdentityServer4.Services;
+using IdentityServer4.Validation;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Logging;
+using Streetwriters.Common.Models;
+
+namespace Streetwriters.Identity.Services
+{
+ public class CustomIntrospectionResponseGenerator : IntrospectionResponseGenerator
+ {
+ private UserManager UserManager { get; }
+ public CustomIntrospectionResponseGenerator(IEventService events, ILogger logger, UserManager userManager) : base(events, logger)
+ {
+ UserManager = userManager;
+ }
+
+ public override async Task> ProcessAsync(IntrospectionRequestValidationResult validationResult)
+ {
+ var result = await base.ProcessAsync(validationResult);
+
+ if (result.TryGetValue("sub", out object userId))
+ {
+ var user = await UserManager.FindByIdAsync(userId.ToString());
+
+ var verifiedClaim = user.Claims.Find((c) => c.ClaimType == "verified");
+ if (verifiedClaim != null)
+ await UserManager.RemoveClaimAsync(user, verifiedClaim.ToClaim());
+ var hcliClaim = user.Claims.Find((c) => c.ClaimType == "hcli");
+ if (hcliClaim != null)
+ await UserManager.RemoveClaimAsync(user, hcliClaim.ToClaim());
+
+ user.Claims.ForEach((claim) =>
+ {
+ if (claim.ClaimType == "verified" || claim.ClaimType == "hcli") return;
+ result.TryAdd(claim.ClaimType, claim.ClaimValue);
+ });
+ result.TryAdd("verified", user.EmailConfirmed.ToString().ToLowerInvariant());
+ }
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Services/CustomRefreshTokenService.cs b/Streetwriters.Identity/Services/CustomRefreshTokenService.cs
new file mode 100644
index 0000000..dbec9d2
--- /dev/null
+++ b/Streetwriters.Identity/Services/CustomRefreshTokenService.cs
@@ -0,0 +1,43 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Threading.Tasks;
+using IdentityServer4.Models;
+using IdentityServer4.Services;
+using IdentityServer4.Stores;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+
+namespace Streetwriters.Identity.Services
+{
+ public class CustomRefreshTokenService : DefaultRefreshTokenService
+ {
+ public CustomRefreshTokenService(IRefreshTokenStore refreshTokenStore, IProfileService profile, ISystemClock clock, ILogger logger) : base(refreshTokenStore, profile, clock, logger)
+ {
+ }
+
+ protected override Task AcceptConsumedTokenAsync(RefreshToken refreshToken)
+ {
+ // Allow refresh token replay for 1 day.
+ // if (refreshToken.ConsumedTime?.ToUniversalTime().AddDays(1) < DateTime.UtcNow) return Task.FromResult(false);
+ return Task.FromResult(true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Services/EmailSender.cs b/Streetwriters.Identity/Services/EmailSender.cs
new file mode 100644
index 0000000..effea5b
--- /dev/null
+++ b/Streetwriters.Identity/Services/EmailSender.cs
@@ -0,0 +1,275 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Threading.Tasks;
+using Streetwriters.Identity.Interfaces;
+using SendGrid;
+using SendGrid.Helpers.Mail;
+using Streetwriters.Common;
+using Streetwriters.Common.Interfaces;
+using Streetwriters.Identity.Models;
+using System;
+using System.Net.Http;
+using System.Text.Json;
+using System.Net.Http.Headers;
+using System.Text.Json.Serialization;
+using MailKit.Net.Smtp;
+using MailKit;
+using MimeKit;
+using System.IO;
+using Scriban;
+using WebMarkupMin.Core;
+using WebMarkupMin.Core.Loggers;
+using MimeKit.Cryptography;
+using Org.BouncyCastle.Bcpg.OpenPgp;
+using System.Linq;
+using System.Threading;
+using Org.BouncyCastle.Bcpg;
+using System.Text;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Streetwriters.Identity.Services
+{
+ public class EmailSender : IEmailSender, IAsyncDisposable
+ {
+ IOptions SmtpOptions { get; set; }
+ NNGnuPGContext NNGnuPGContext { get; set; }
+ public EmailSender(IConfiguration configuration, IOptions smtpOptions)
+ {
+ SmtpOptions = smtpOptions;
+ NNGnuPGContext = new NNGnuPGContext(configuration.GetSection("PgpKeySettings"));
+ }
+
+ EmailTemplate Email2FATemplate = new EmailTemplate
+ {
+ Html = ReadMinifiedHtmlFile("Templates/Email2FACode.html"),
+ Text = File.ReadAllText("Templates/Email2FACode.txt"),
+ Subject = "Your {{app_name}} account 2FA code",
+ };
+
+ EmailTemplate ConfirmEmailTemplate = new EmailTemplate
+ {
+ Html = ReadMinifiedHtmlFile("Templates/ConfirmEmail.html"),
+ Text = File.ReadAllText("Templates/ConfirmEmail.txt"),
+ Subject = "Confirm your {{app_name}} account",
+ };
+
+ EmailTemplate ConfirmChangeEmailTemplate = new EmailTemplate
+ {
+ Html = ReadMinifiedHtmlFile("Templates/EmailChangeConfirmation.html"),
+ Text = File.ReadAllText("Templates/EmailChangeConfirmation.txt"),
+ Subject = "Change {{app_name}} account email address",
+ };
+
+ EmailTemplate PasswordResetEmailTemplate = new EmailTemplate
+ {
+ Html = ReadMinifiedHtmlFile("Templates/ResetAccountPassword.html"),
+ Text = File.ReadAllText("Templates/ResetAccountPassword.txt"),
+ Subject = "Reset {{app_name}} account password",
+ };
+
+ EmailTemplate FailedLoginAlertTemplate = new EmailTemplate
+ {
+ Html = ReadMinifiedHtmlFile("Templates/FailedLoginAlert.html"),
+ Text = File.ReadAllText("Templates/FailedLoginAlert.txt"),
+ Subject = "Failed login attempt on your {{app_name}} account",
+ };
+
+ SmtpClient mailClient;
+ public EmailSender()
+ {
+ mailClient = new SmtpClient();
+ }
+
+ public async Task Send2FACodeEmailAsync(string email, string code, IClient client)
+ {
+ var template = new EmailTemplate
+ {
+ Html = Email2FATemplate.Html,
+ Text = Email2FATemplate.Text,
+ Subject = Email2FATemplate.Subject,
+ Data = new
+ {
+ app_name = client.Name,
+ code = code
+ }
+ };
+ await SendEmailAsync(email, template, client);
+ }
+
+
+ public Task SendWelcomeEmailAsync(string email, IClient client)
+ {
+ // EmailTemplate template = new EmailTemplate
+ // {
+ // Id = client.WelcomeEmailTemplateId,
+ // Data = new { },
+ // SendAt = DateTimeOffset.UtcNow.AddHours(2).ToUnixTimeSeconds()
+ // };
+ // return SendEmailAsync(email, template, client);
+ return Task.CompletedTask;
+ }
+
+ public async Task SendConfirmationEmailAsync(string email, string callbackUrl, IClient client)
+ {
+ var template = new EmailTemplate
+ {
+ Html = ConfirmEmailTemplate.Html,
+ Text = ConfirmEmailTemplate.Text,
+ Subject = ConfirmEmailTemplate.Subject,
+ Data = new
+ {
+ app_name = client.Name,
+ confirm_link = callbackUrl
+ }
+ };
+ await SendEmailAsync(email, template, client);
+ }
+
+ public async Task SendChangeEmailConfirmationAsync(string email, string callbackUrl, IClient client)
+ {
+ var template = new EmailTemplate
+ {
+ Html = ConfirmChangeEmailTemplate.Html,
+ Text = ConfirmChangeEmailTemplate.Text,
+ Subject = ConfirmChangeEmailTemplate.Subject,
+ Data = new
+ {
+ app_name = client.Name,
+ confirm_link = callbackUrl
+ }
+ };
+ await SendEmailAsync(email, template, client);
+ }
+
+ public async Task SendPasswordResetEmailAsync(string email, string callbackUrl, IClient client)
+ {
+ var template = new EmailTemplate
+ {
+ Html = PasswordResetEmailTemplate.Html,
+ Text = PasswordResetEmailTemplate.Text,
+ Subject = PasswordResetEmailTemplate.Subject,
+ Data = new
+ {
+ app_name = client.Name,
+ reset_link = callbackUrl
+ }
+ };
+ await SendEmailAsync(email, template, client);
+ }
+
+
+ public async Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client)
+ {
+ var template = new EmailTemplate
+ {
+ Html = FailedLoginAlertTemplate.Html,
+ Text = FailedLoginAlertTemplate.Text,
+ Subject = FailedLoginAlertTemplate.Subject,
+ Data = new
+ {
+ app_name = client.Name,
+ device_info = deviceInfo.Replace("\n", " ")
+ }
+ };
+ await SendEmailAsync(email, template, client);
+ }
+
+ private async Task SendEmailAsync(string email, IEmailTemplate template, IClient client)
+ {
+ try
+ {
+ if (!mailClient.IsConnected)
+ await mailClient.ConnectAsync(SmtpOptions.Value.Host, SmtpOptions.Value.Port, MailKit.Security.SecureSocketOptions.StartTls);
+
+ if (!mailClient.IsAuthenticated)
+ await mailClient.AuthenticateAsync(SmtpOptions.Value.Username, SmtpOptions.Value.Password);
+
+ var message = new MimeMessage();
+ var sender = new MailboxAddress(client.SenderName, client.SenderEmail);
+ message.From.Add(sender);
+ message.To.Add(new MailboxAddress("", email));
+ message.ReplyTo.Add(new MailboxAddress("Streetwriters", "support@streetwriters.co"));
+ message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data);
+
+ var builder = new BodyBuilder();
+
+ builder.TextBody = await Template.Parse(template.Text).RenderAsync(template.Data);
+ builder.HtmlBody = await Template.Parse(template.Html).RenderAsync(template.Data);
+
+ var key = NNGnuPGContext.GetSigningKey(sender);
+ if (key != null)
+ {
+ using (MemoryStream outputStream = new MemoryStream())
+ {
+ using (Stream armoredStream = new ArmoredOutputStream(outputStream))
+ {
+ key.PublicKey.Encode(armoredStream);
+ }
+ outputStream.Seek(0, SeekOrigin.Begin);
+ builder.Attachments.Add($"{client.Id}_pub.asc", Encoding.ASCII.GetBytes(Encoding.ASCII.GetString(outputStream.ToArray())));
+ }
+ }
+
+ message.Body = MultipartSigned.Create(NNGnuPGContext, sender, DigestAlgorithm.Sha256, builder.ToMessageBody());
+ await mailClient.SendAsync(message);
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine(ex.Message);
+ }
+ }
+
+ async ValueTask IAsyncDisposable.DisposeAsync()
+ {
+ await mailClient.DisconnectAsync(true);
+ mailClient.Dispose();
+ }
+
+
+ static string ReadMinifiedHtmlFile(string path)
+ {
+ var settings = new HtmlMinificationSettings()
+ {
+ WhitespaceMinificationMode = WhitespaceMinificationMode.Medium
+ };
+ var cssMinifier = new KristensenCssMinifier();
+ var jsMinifier = new CrockfordJsMinifier();
+
+ var minifier = new HtmlMinifier(settings, cssMinifier, jsMinifier, new NullLogger());
+
+ return minifier.Minify(File.ReadAllText(path), false).MinifiedContent;
+ }
+ }
+
+ class NNGnuPGContext : GnuPGContext
+ {
+ IConfiguration PgpKeySettings { get; set; }
+ public NNGnuPGContext(IConfiguration pgpKeySettings)
+ {
+ PgpKeySettings = pgpKeySettings;
+ }
+
+ protected override string GetPasswordForKey(PgpSecretKey key)
+ {
+ return PgpKeySettings[key.KeyId.ToString("X")];
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Services/MFAService.cs b/Streetwriters.Identity/Services/MFAService.cs
new file mode 100644
index 0000000..ff37cee
--- /dev/null
+++ b/Streetwriters.Identity/Services/MFAService.cs
@@ -0,0 +1,241 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Interfaces;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+
+namespace Streetwriters.Identity.Services
+{
+ internal class MFAService : IMFAService
+ {
+ const string PRIMARY_METHOD_CLAIM = "mfa:primary";
+ const string SECONDARY_METHOD_CLAIM = "mfa:secondary";
+ const string SMS_ID_CLAIM = "mfa:sms:id";
+
+ private UserManager UserManager { get; set; }
+ private IEmailSender EmailSender { get; set; }
+ private ISMSSender SMSSender { get; set; }
+ public MFAService(UserManager _userManager, IEmailSender emailSender, ISMSSender smsSender)
+ {
+ UserManager = _userManager;
+ EmailSender = emailSender;
+ SMSSender = smsSender;
+ }
+
+ public async Task EnableMFAAsync(User user, string primaryMethod)
+ {
+ var result = await UserManager.SetTwoFactorEnabledAsync(user, true);
+ if (!result.Succeeded) return;
+
+ await this.RemovePrimaryMethodAsync(user);
+ await UserManager.AddClaimAsync(user, new Claim(MFAService.PRIMARY_METHOD_CLAIM, primaryMethod));
+ }
+
+ public async Task DisableMFAAsync(User user)
+ {
+ var result = await UserManager.SetTwoFactorEnabledAsync(user, false);
+ if (!result.Succeeded) return false;
+
+ await this.RemovePrimaryMethodAsync(user);
+ await this.RemoveSecondaryMethodAsync(user);
+
+ await UserManager.ResetAuthenticatorKeyAsync(user);
+ return true;
+ }
+
+ public async Task SetSecondaryMethodAsync(User user, string secondaryMethod)
+ {
+ await this.ReplaceClaimAsync(user, MFAService.SECONDARY_METHOD_CLAIM, secondaryMethod);
+ }
+
+ private async Task ReplaceClaimAsync(User user, string claimType, string claimValue)
+ {
+ await this.RemoveClaimAsync(user, claimType);
+ await UserManager.AddClaimAsync(user, new Claim(claimType, claimValue));
+ }
+
+ public string GetPrimaryMethod(User user)
+ {
+ return this.GetClaimValue(user, MFAService.PRIMARY_METHOD_CLAIM);
+ }
+
+ public string GetSecondaryMethod(User user)
+ {
+ return this.GetClaimValue(user, MFAService.SECONDARY_METHOD_CLAIM);
+ }
+
+ public string GetClaimValue(User user, string claimType)
+ {
+ var claim = user.Claims.FirstOrDefault((c) => c.ClaimType == claimType);
+ return claim != null ? claim.ClaimValue : null;
+ }
+
+ public Task GetRemainingValidCodesAsync(User user)
+ {
+ return UserManager.CountRecoveryCodesAsync(user);
+ }
+
+ public bool IsValidMFAMethod(string method)
+ {
+ return method == MFAMethods.App || method == MFAMethods.Email || method == MFAMethods.SMS || method == MFAMethods.RecoveryCode;
+ }
+
+ private Task RemoveSecondaryMethodAsync(User user)
+ {
+ return this.RemoveClaimAsync(user, MFAService.SECONDARY_METHOD_CLAIM);
+ }
+
+ private Task RemovePrimaryMethodAsync(User user)
+ {
+ return this.RemoveClaimAsync(user, MFAService.PRIMARY_METHOD_CLAIM);
+ }
+
+ private async Task RemoveClaimAsync(User user, string claimType)
+ {
+ var claim = user.Claims.FirstOrDefault((c) => c.ClaimType == claimType);
+ if (claim != null) await UserManager.RemoveClaimAsync(user, claim.ToClaim());
+ }
+
+ public async Task GetAuthenticatorDetailsAsync(User user, IClient client)
+ {
+ // Load the authenticator key & QR code URI to display on the form
+ var unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
+ if (string.IsNullOrEmpty(unformattedKey))
+ {
+ await UserManager.ResetAuthenticatorKeyAsync(user);
+ unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
+ }
+
+ return new AuthenticatorDetails
+ {
+ SharedKey = FormatKey(unformattedKey),
+ AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey, client.Name)
+ };
+ }
+
+ public async Task SendOTPAsync(User user, IClient client, MultiFactorSetupForm form, bool isSetup = false)
+ {
+ var method = form.Type;
+ if (method != MFAMethods.Email && method != MFAMethods.SMS) throw new Exception("Invalid method.");
+
+
+ if (isSetup &&
+ method == MFAMethods.SMS &&
+ !UserService.IsUserPremium(client.Id, user))
+ throw new Exception("Due to the high costs of SMS, currently 2FA via SMS is only available for Pro users.");
+
+ // if (!user.EmailConfirmed) throw new Exception("Please confirm your email before activating 2FA by email.");
+ await GetAuthenticatorDetailsAsync(user, client);
+
+ switch (method)
+ {
+ case "email":
+ string emailOTP = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider);
+ await EmailSender.Send2FACodeEmailAsync(user.Email, emailOTP, client);
+ break;
+ case "sms":
+ await UserManager.SetPhoneNumberAsync(user, form.PhoneNumber);
+ var id = SMSSender.SendOTP(form.PhoneNumber, client);
+ await this.ReplaceClaimAsync(user, MFAService.SMS_ID_CLAIM, id);
+ break;
+
+ }
+ }
+
+ public async Task VerifyOTPAsync(User user, string code, string method)
+ {
+ if (method == MFAMethods.SMS)
+ {
+ var id = this.GetClaimValue(user, MFAService.SMS_ID_CLAIM);
+ if (string.IsNullOrEmpty(id)) throw new Exception("Could not find associated SMS verify id. Please try sending the code again.");
+ if (SMSSender.VerifyOTP(id, code))
+ {
+ // Auto confirm user phone number if not confirmed
+ if (!await UserManager.IsPhoneNumberConfirmedAsync(user))
+ {
+ var token = await UserManager.GenerateChangePhoneNumberTokenAsync(user, user.PhoneNumber);
+ await UserManager.VerifyChangePhoneNumberTokenAsync(user, token, user.PhoneNumber);
+ }
+ await this.RemoveClaimAsync(user, MFAService.SMS_ID_CLAIM);
+ return true;
+ }
+ return false;
+ }
+ else if (method == MFAMethods.Email)
+ {
+ if (await UserManager.VerifyTwoFactorTokenAsync(user, GetProvider(method), code))
+ {
+ // Auto confirm user email if not confirmed
+ if (!await UserManager.IsEmailConfirmedAsync(user))
+ {
+ var token = await UserManager.GenerateEmailConfirmationTokenAsync(user);
+ await UserManager.ConfirmEmailAsync(user, token);
+ }
+ return true;
+ }
+ return false;
+ }
+ else
+ return await UserManager.VerifyTwoFactorTokenAsync(user, GetProvider(method), code);
+ }
+
+ private string GetProvider(string method)
+ {
+ return method == MFAMethods.Email || method == MFAMethods.SMS ? TokenOptions.DefaultPhoneProvider : UserManager.Options.Tokens.AuthenticatorTokenProvider;
+ }
+
+ 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();
+ }
+
+ private string GenerateQrCodeUri(string email, string unformattedKey, string issuer)
+ {
+ const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
+
+ return string.Format(
+ AuthenticatorUriFormat,
+ UrlEncoder.Default.Encode(issuer),
+ UrlEncoder.Default.Encode(email),
+ unformattedKey);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Services/PasswordHasher.cs b/Streetwriters.Identity/Services/PasswordHasher.cs
new file mode 100644
index 0000000..9dbb4c9
--- /dev/null
+++ b/Streetwriters.Identity/Services/PasswordHasher.cs
@@ -0,0 +1,47 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using Microsoft.AspNetCore.Identity;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Helpers;
+
+namespace Streetwriters.Identity.Services
+{
+ public class Argon2PasswordHasher : IPasswordHasher where TUser : User
+ {
+ public string HashPassword(TUser user, string password)
+ {
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+
+ return PasswordHelper.CreatePasswordHash(password);
+ }
+
+ public PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
+ {
+ if (hashedPassword == null)
+ throw new ArgumentNullException(nameof(hashedPassword));
+ if (providedPassword == null)
+ throw new ArgumentNullException(nameof(providedPassword));
+
+ return PasswordHelper.VerifyPassword(providedPassword, hashedPassword) ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Services/ProfileService.cs b/Streetwriters.Identity/Services/ProfileService.cs
new file mode 100644
index 0000000..b0acbff
--- /dev/null
+++ b/Streetwriters.Identity/Services/ProfileService.cs
@@ -0,0 +1,61 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using IdentityModel;
+using IdentityServer4.Models;
+using IdentityServer4.Services;
+using Microsoft.AspNetCore.Identity;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Models;
+using Streetwriters.Data.Repositories;
+
+namespace Streetwriters.Identity.Services
+{
+ public class ProfileService : IProfileService
+ {
+ protected UserManager UserManager { get; set; }
+
+ public ProfileService(UserManager userManager)
+ {
+ UserManager = userManager;
+ }
+
+ public async Task GetProfileDataAsync(ProfileDataRequestContext context)
+ {
+ User user = await UserManager.GetUserAsync(context.Subject);
+ if (user == null) return;
+
+ IList roles = await UserManager.GetRolesAsync(user);
+ IList claims = user.Claims.Select((c) => c.ToClaim()).ToList();
+
+ context.IssuedClaims.AddRange(roles.Select((r) => new Claim(JwtClaimTypes.Role, r)));
+ context.IssuedClaims.AddRange(claims);
+ }
+
+ public Task IsActiveAsync(IsActiveContext context)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Services/SMSSender.cs b/Streetwriters.Identity/Services/SMSSender.cs
new file mode 100644
index 0000000..341c946
--- /dev/null
+++ b/Streetwriters.Identity/Services/SMSSender.cs
@@ -0,0 +1,59 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Common.Interfaces;
+using MessageBird;
+using MessageBird.Objects;
+using Microsoft.Extensions.Options;
+using Streetwriters.Identity.Models;
+
+namespace Streetwriters.Identity.Services
+{
+ public class SMSSender : ISMSSender
+ {
+ private Client client;
+ public SMSSender(IOptions messageBirdOptions)
+ {
+ client = Client.CreateDefault(messageBirdOptions.Value.AccessKey);
+ }
+
+ public string SendOTP(string number, IClient app)
+ {
+ VerifyOptionalArguments optionalArguments = new VerifyOptionalArguments
+ {
+ Originator = app.Name,
+ Reference = app.Name,
+ Type = MessageType.Sms,
+ Template = $"Your {app.Name} 2FA code is: %token. Valid for 5 minutes.",
+ TokenLength = 6,
+ Timeout = 60 * 5
+ };
+ Verify verify = client.CreateVerify(number, optionalArguments);
+ if (verify.Status == VerifyStatus.Sent) return verify.Id;
+ return null;
+ }
+
+ public bool VerifyOTP(string id, string code)
+ {
+ Verify verify = client.SendVerifyToken(id, code);
+ return verify.Status == VerifyStatus.Verified;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Services/TokenGenerationService.cs b/Streetwriters.Identity/Services/TokenGenerationService.cs
new file mode 100644
index 0000000..2918408
--- /dev/null
+++ b/Streetwriters.Identity/Services/TokenGenerationService.cs
@@ -0,0 +1,116 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using IdentityModel;
+using IdentityServer4;
+using IdentityServer4.Configuration;
+using IdentityServer4.Models;
+using IdentityServer4.Services;
+using IdentityServer4.Stores;
+using IdentityServer4.Validation;
+using Microsoft.AspNetCore.Identity;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Interfaces;
+
+namespace Streetwriters.Identity.Helpers
+{
+ public class TokenGenerationService : ITokenGenerationService
+ {
+ private IPersistedGrantStore PersistedGrantStore { get; set; }
+ private ITokenService TokenService { get; set; }
+ private IUserClaimsPrincipalFactory PrincipalFactory { get; set; }
+ private IdentityServerOptions ISOptions { get; set; }
+ private IdentityServerTools Tools { get; set; }
+ private IResourceStore ResourceStore { get; set; }
+ public TokenGenerationService(ITokenService tokenService,
+ IUserClaimsPrincipalFactory principalFactory,
+ IdentityServerOptions identityServerOptions,
+ IPersistedGrantStore persistedGrantStore,
+ IdentityServerTools tools,
+ IResourceStore resourceStore)
+ {
+ TokenService = tokenService;
+ PrincipalFactory = principalFactory;
+ ISOptions = identityServerOptions;
+ PersistedGrantStore = persistedGrantStore;
+ Tools = tools;
+ ResourceStore = resourceStore;
+ }
+
+ public async Task CreateAccessTokenAsync(User user, string clientId)
+ {
+ var IdentityPricipal = await PrincipalFactory.CreateAsync(user);
+ var IdentityUser = new IdentityServerUser(user.Id.ToString());
+ IdentityUser.AdditionalClaims = IdentityPricipal.Claims.ToArray();
+ IdentityUser.DisplayName = user.UserName;
+ IdentityUser.AuthenticationTime = System.DateTime.UtcNow;
+ IdentityUser.IdentityProvider = IdentityServerConstants.LocalIdentityProvider;
+ var Request = new TokenCreationRequest
+ {
+ Subject = IdentityUser.CreatePrincipal(),
+ IncludeAllIdentityClaims = true,
+ ValidatedRequest = new ValidatedRequest()
+ };
+ Request.ValidatedRequest.Subject = Request.Subject;
+ Request.ValidatedRequest.SetClient(Config.Clients.FirstOrDefault((c) => c.ClientId == clientId));
+ Request.ValidatedRequest.AccessTokenType = AccessTokenType.Reference;
+ Request.ValidatedRequest.AccessTokenLifetime = 18000;
+ Request.ValidatedResources = new ResourceValidationResult(new Resources(Config.IdentityResources, Config.ApiResources, Config.ApiScopes));
+ Request.ValidatedRequest.Options = ISOptions;
+ Request.ValidatedRequest.ClientClaims = IdentityUser.AdditionalClaims;
+ var accessToken = await TokenService.CreateAccessTokenAsync(Request);
+ return await TokenService.CreateSecurityTokenAsync(accessToken);
+ }
+
+ public async Task TransformTokenRequestAsync(ValidatedTokenRequest request, User user, string grantType, string[] scopes, int lifetime = 20 * 60)
+ {
+ var principal = await PrincipalFactory.CreateAsync(user);
+ var identityUser = new IdentityServerUser(user.Id.ToString());
+ identityUser.DisplayName = user.UserName;
+ identityUser.AuthenticationTime = System.DateTime.UtcNow;
+ identityUser.IdentityProvider = IdentityServerConstants.LocalIdentityProvider;
+ identityUser.AdditionalClaims = principal.Claims.ToArray();
+
+ request.AccessTokenType = AccessTokenType.Jwt;
+ request.AccessTokenLifetime = lifetime;
+ request.GrantType = grantType;
+ request.ValidatedResources = await ResourceStore.CreateResourceValidationResult(new ParsedScopesResult()
+ {
+ ParsedScopes = scopes.Select((scope) => new ParsedScopeValue(scope)).ToArray()
+ });
+ return identityUser.CreatePrincipal();
+ }
+
+ public async Task CreateAccessTokenFromValidatedRequestAsync(ValidatedTokenRequest validatedRequest, User user, string[] scopes, int lifetime = 20 * 60)
+ {
+ var request = new TokenCreationRequest
+ {
+ Subject = await this.TransformTokenRequestAsync(validatedRequest, user, validatedRequest.GrantType, scopes, lifetime),
+ IncludeAllIdentityClaims = true,
+ ValidatedRequest = validatedRequest,
+ ValidatedResources = validatedRequest.ValidatedResources
+ };
+ var accessToken = await TokenService.CreateAccessTokenAsync(request);
+ return await TokenService.CreateSecurityTokenAsync(accessToken);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Services/UserService.cs b/Streetwriters.Identity/Services/UserService.cs
new file mode 100644
index 0000000..c32a107
--- /dev/null
+++ b/Streetwriters.Identity/Services/UserService.cs
@@ -0,0 +1,84 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Linq;
+using System.Security.Claims;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Models;
+
+namespace Streetwriters.Identity.Services
+{
+ public class UserService
+ {
+ public static SubscriptionType GetUserSubscriptionStatus(string clientId, User user)
+ {
+ var claimKey = GetClaimKey(clientId);
+ var status = user.Claims.FirstOrDefault((c) => c.ClaimType == claimKey).ClaimValue;
+ switch (status)
+ {
+ case "basic":
+ return SubscriptionType.BASIC;
+ case "trial":
+ return SubscriptionType.TRIAL;
+ case "premium":
+ return SubscriptionType.PREMIUM;
+ case "premium_canceled":
+ return SubscriptionType.PREMIUM_CANCELED;
+ case "premium_expired":
+ return SubscriptionType.PREMIUM_EXPIRED;
+ default:
+ return SubscriptionType.BASIC;
+ }
+ }
+
+
+
+ public static bool IsUserPremium(string clientId, User user)
+ {
+ var status = GetUserSubscriptionStatus(clientId, user);
+ string[] allowedClaims = { "trial", "premium", "premium_canceled" };
+
+ return status == SubscriptionType.TRIAL || status == SubscriptionType.PREMIUM || status == SubscriptionType.PREMIUM_CANCELED;
+ }
+
+ public static Claim SubscriptionTypeToClaim(string clientId, SubscriptionType type)
+ {
+ var claimKey = GetClaimKey(clientId);
+ switch (type)
+ {
+ case SubscriptionType.BASIC:
+ return new Claim(claimKey, "basic");
+ case SubscriptionType.TRIAL:
+ return new Claim(claimKey, "trial");
+ case SubscriptionType.PREMIUM:
+ return new Claim(claimKey, "premium");
+ case SubscriptionType.PREMIUM_CANCELED:
+ return new Claim(claimKey, "premium_canceled");
+ case SubscriptionType.PREMIUM_EXPIRED:
+ return new Claim(claimKey, "premium_expired");
+ }
+ return null;
+ }
+
+ public static string GetClaimKey(string clientId)
+ {
+ return $"{clientId}:status";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Startup.cs b/Streetwriters.Identity/Startup.cs
new file mode 100644
index 0000000..427fcc5
--- /dev/null
+++ b/Streetwriters.Identity/Startup.cs
@@ -0,0 +1,219 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.IO;
+using AspNetCore.Identity.Mongo;
+using IdentityServer4.ResponseHandling;
+using IdentityServer4.Services;
+using IdentityServer4.Validation;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.IdentityModel.Logging;
+using Microsoft.IdentityModel.Tokens;
+using Streetwriters.Common;
+using Streetwriters.Common.Extensions;
+using Streetwriters.Common.Messages;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Helpers;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+using Streetwriters.Identity.Services;
+using Streetwriters.Identity.Validation;
+
+namespace Streetwriters.Identity
+{
+ public class Startup
+ {
+
+ public Startup(IConfiguration configuration, IWebHostEnvironment environment)
+ {
+ Configuration = configuration;
+ WebHostEnvironment = environment;
+ }
+
+ private IConfiguration Configuration { get; }
+ private IWebHostEnvironment WebHostEnvironment { get; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ var connectionString = Configuration["MongoDbSettings:ConnectionString"];
+
+ services.Configure(Configuration.GetSection("SmtpSettings"));
+ services.Configure(Configuration.GetSection("MessageBirdSettings"));
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient, Argon2PasswordHasher>();
+
+ services.AddCors();
+
+ //services.AddSingleton();
+ services.AddIdentityMongoDbProvider(options =>
+ {
+ // Password settings.
+ options.Password.RequireDigit = false;
+ options.Password.RequireLowercase = false;
+ options.Password.RequireNonAlphanumeric = false;
+ options.Password.RequireUppercase = false;
+ options.Password.RequiredLength = 8;
+ options.Password.RequiredUniqueChars = 0;
+
+ // Lockout settings.
+ options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
+ options.Lockout.MaxFailedAccessAttempts = 5;
+ options.Lockout.AllowedForNewUsers = true;
+
+ // User settings.
+ //options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._";
+ options.User.RequireUniqueEmail = true;
+ }, (options) =>
+ {
+ options.RolesCollection = "roles";
+ options.UsersCollection = "users";
+ // options.MigrationCollection = "migration";
+ options.ConnectionString = connectionString;
+ }).AddDefaultTokenProviders();
+
+ var builder = services.AddIdentityServer(
+ options =>
+ {
+ options.Events.RaiseSuccessEvents = true;
+ options.Events.RaiseFailureEvents = true;
+ options.Events.RaiseErrorEvents = true;
+ options.IssuerUri = Servers.IdentityServer.ToString();
+ })
+ .AddExtensionGrantValidator()
+ .AddExtensionGrantValidator()
+ .AddExtensionGrantValidator()
+ .AddOperationalStore(options =>
+ {
+ options.ConnectionString = connectionString;
+ })
+ .AddConfigurationStore(options =>
+ {
+ options.ConnectionString = connectionString;
+ })
+ .AddAspNetIdentity()
+ .AddInMemoryClients(Config.Clients)
+ .AddInMemoryApiResources(Config.ApiResources)
+ .AddInMemoryApiScopes(Config.ApiScopes)
+ .AddInMemoryIdentityResources(Config.IdentityResources)
+ .AddKeyManagement()
+ .AddFileSystemPersistence(Path.Combine(WebHostEnvironment.ContentRootPath, @"keystore"));
+
+ services.Configure(options =>
+ {
+ options.TokenLifespan = TimeSpan.FromHours(2);
+ });
+
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("mfa", policy =>
+ {
+ policy.AddAuthenticationSchemes("Bearer+jwt");
+ policy.RequireClaim("scope", Config.MFA_GRANT_TYPE_SCOPE);
+ });
+ });
+
+
+ services.AddLocalApiAuthentication();
+ services.AddAuthentication()
+ .AddJwtBearer("Bearer+jwt", options =>
+ {
+ options.MapInboundClaims = false;
+ options.Authority = Servers.IdentityServer.ToString();
+ options.RequireHttpsMetadata = false;
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidTypes = new[] { "at+jwt" },
+ ValidateAudience = false,
+ ValidateIssuerSigningKey = true,
+ ValidateIssuer = true,
+ };
+ });
+
+ services.AddTransient();
+ services.AddControllers();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+
+ services.AddHealthChecks();
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (!env.IsDevelopment())
+ {
+ app.UseForwardedHeaders(new ForwardedHeadersOptions
+ {
+ ForwardedForHeaderName = "CF_CONNECTING_IP",
+ ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
+ });
+ }
+
+ app.UseCors("notesnook");
+
+ app.UseRouting();
+
+ app.UseIdentityServer();
+
+ app.UseAuthorization();
+ app.UseAuthentication();
+
+ app.UseWamp(WampServers.IdentityServer, (realm, server) =>
+ {
+ realm.Subscribe(server.Topics.CreateSubscriptionTopic, async (CreateSubscriptionMessage message) =>
+ {
+ using (var serviceScope = app.ApplicationServices.CreateScope())
+ {
+ var services = serviceScope.ServiceProvider;
+ var userManager = services.GetRequiredService>();
+ await MessageHandlers.CreateSubscription.Process(message, userManager);
+ }
+ });
+ realm.Subscribe(server.Topics.DeleteSubscriptionTopic, async (DeleteSubscriptionMessage message) =>
+ {
+ using (var serviceScope = app.ApplicationServices.CreateScope())
+ {
+ var services = serviceScope.ServiceProvider;
+ var userManager = services.GetRequiredService>();
+ await MessageHandlers.DeleteSubscription.Process(message, userManager);
+ }
+ });
+ });
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ endpoints.MapHealthChecks("/health");
+ });
+ }
+ }
+}
diff --git a/Streetwriters.Identity/Streetwriters.Identity.csproj b/Streetwriters.Identity/Streetwriters.Identity.csproj
new file mode 100644
index 0000000..93fa154
--- /dev/null
+++ b/Streetwriters.Identity/Streetwriters.Identity.csproj
@@ -0,0 +1,46 @@
+
+
+
+ net7.0
+ Streetwriters.Identity.Program
+ 10.0
+ linux-x64
+ true
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
diff --git a/Streetwriters.Identity/Templates/ConfirmEmail.html b/Streetwriters.Identity/Templates/ConfirmEmail.html
new file mode 100644
index 0000000..b4c59e6
--- /dev/null
+++ b/Streetwriters.Identity/Templates/ConfirmEmail.html
@@ -0,0 +1,823 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Confirm your email to activate your
+ {{app_name}} account.
+
+
+
+
+
+
+
+
+
+
+ {{app_name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hey there!
+
+
+
+
+
+ Thank you so much for signing up on
+ {{app_name}}!
+
+
+
+
+
+ Please confirm your email by
+ clicking
+ here.
+
+ This email has been sent to you
+ because you signed up on {{app_name}}
+ a service of Streetwriters
+ (Private) Ltd.
+
+
+ 1st Floor, Valley Plaza, Mardowal
+ Chowk, Naushera
+
+
+ Khushab,
+ Punjab
+ 41100 Pakistan
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Streetwriters.Identity/Templates/ConfirmEmail.txt b/Streetwriters.Identity/Templates/ConfirmEmail.txt
new file mode 100644
index 0000000..478b662
--- /dev/null
+++ b/Streetwriters.Identity/Templates/ConfirmEmail.txt
@@ -0,0 +1,17 @@
+Confirm your email to activate your {{app_name}} account.
+
+************
+{{app_name}}
+************
+
+Hey there!
+
+Thank you so much for signing up on {{app_name}}!
+
+Please confirm your {{app_name}} account by going to this link: {{confirm_link}}.
+
+-----------
+
+This email has been sent to you because you signed up on {{app_name}} a service of Streetwriters (Private) Ltd.
+1st Floor, Valley Plaza, Mardowal Chowk, Naushera
+Khushab, Punjab 41100 Pakistan
\ No newline at end of file
diff --git a/Streetwriters.Identity/Templates/Email2FACode.html b/Streetwriters.Identity/Templates/Email2FACode.html
new file mode 100644
index 0000000..2a9b0ae
--- /dev/null
+++ b/Streetwriters.Identity/Templates/Email2FACode.html
@@ -0,0 +1,435 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Your 2FA code for {{app_name}} is
+ {{code}}
+
+
+
+
+
+
+
+
+
+
+ {{app_name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please use the following 2FA code to
+ login to your account:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{code}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ If you did not request to a 2FA
+ code, please report this to us
+ at support@streetwriters.co
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This email has been sent to you
+ because you signed up on {{app_name}}
+ - a service by Streetwriters
+ (Private) Ltd.
+
+
+ 1st Floor, Valley Plaza, Mardowal
+ Chowk, Naushera
+
+
+ Khushab, Punjab 41100
+ Pakistan
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Streetwriters.Identity/Templates/Email2FACode.txt b/Streetwriters.Identity/Templates/Email2FACode.txt
new file mode 100644
index 0000000..bf52dfd
--- /dev/null
+++ b/Streetwriters.Identity/Templates/Email2FACode.txt
@@ -0,0 +1,17 @@
+Your 2FA code for {{app_name}} is: {{code}}
+
+************
+{{app_name}}
+************
+
+Please use the following 2FA code to login to your account:
+
+{{code}}
+
+If you did not request to a 2FA code, please report this to us at support@streetwriters.co
+
+-------------
+
+This email has been sent to you because you signed up on ** - a service by Streetwriters (Private) Ltd.
+1st Floor, Valley Plaza, Mardowal Chowk, Naushera
+Khushab, Punjab 41100 Pakistan
\ No newline at end of file
diff --git a/Streetwriters.Identity/Templates/EmailChangeConfirmation.html b/Streetwriters.Identity/Templates/EmailChangeConfirmation.html
new file mode 100644
index 0000000..94b0c5f
--- /dev/null
+++ b/Streetwriters.Identity/Templates/EmailChangeConfirmation.html
@@ -0,0 +1,807 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Confirm your new email to change it for
+ your {{app_name}} account.
+
+
+
+
+
+
+
+
+
+
+ {{app_name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hey there!
+
+
+
+
+
+ Please confirm your new email by
+ clicking
+ here
+ or the button below.
+
+ This email has been sent to you
+ because you signed up on {{app_name}}
+ a service of Streetwriters
+ (Private) Ltd.
+
+
+ 1st Floor, Valley Plaza, Mardowal
+ Chowk, Naushera
+
+
+ Khushab,
+ Punjab
+ 41100 Pakistan
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Streetwriters.Identity/Templates/EmailChangeConfirmation.txt b/Streetwriters.Identity/Templates/EmailChangeConfirmation.txt
new file mode 100644
index 0000000..5e1d3d8
--- /dev/null
+++ b/Streetwriters.Identity/Templates/EmailChangeConfirmation.txt
@@ -0,0 +1,15 @@
+Confirm your new email to change it for your {{app_name}} account.
+
+************
+{{app_name}}
+************
+
+Hey there!
+
+Please confirm your new email by going to this link: {{confirm_link}}
+
+------------
+
+This email has been sent to you because you signed up on {{app_name}} a service of Streetwriters (Private) Ltd.
+1st Floor, Valley Plaza, Mardowal Chowk, Naushera
+Khushab, Punjab 41100 Pakistan
\ No newline at end of file
diff --git a/Streetwriters.Identity/Templates/FailedLoginAlert.html b/Streetwriters.Identity/Templates/FailedLoginAlert.html
new file mode 100644
index 0000000..a4c0d4e
--- /dev/null
+++ b/Streetwriters.Identity/Templates/FailedLoginAlert.html
@@ -0,0 +1,505 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{app_name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ We detected a failed login attempt
+ on your {{app_name}} account.
+
+
+
+
+
+ {{device_info}}
+
+
+
+
+
+ If this was not you
+ please immediately change your
+ password & 2FA methods.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This email has been sent to you
+ because you signed up on {{app_name}}
+ - a service by Streetwriters
+ (Private) Ltd.
+
+
+ 1st Floor, Valley Plaza, Mardowal
+ Chowk, Naushera
+
+
+ Khushab, Punjab 41100
+ Pakistan
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Streetwriters.Identity/Templates/FailedLoginAlert.txt b/Streetwriters.Identity/Templates/FailedLoginAlert.txt
new file mode 100644
index 0000000..bf98c59
--- /dev/null
+++ b/Streetwriters.Identity/Templates/FailedLoginAlert.txt
@@ -0,0 +1,15 @@
+************
+{{app_name}}
+************
+
+We detected a failed login attempt on your {{app_name}} account.
+
+{{device_info}}
+
+If this was not you *please immediately reset your password & 2FA methods*.
+
+-------------
+
+This email has been sent to you because you signed up on *{{app_name}}* - a service by Streetwriters (Private) Ltd.
+1st Floor, Valley Plaza, Mardowal Chowk, Naushera
+Khushab, Punjab 41100 Pakistan
\ No newline at end of file
diff --git a/Streetwriters.Identity/Templates/ResetAccountPassword.html b/Streetwriters.Identity/Templates/ResetAccountPassword.html
new file mode 100644
index 0000000..6b93a5c
--- /dev/null
+++ b/Streetwriters.Identity/Templates/ResetAccountPassword.html
@@ -0,0 +1,658 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lost access to your {{app_name}}
+ account?
+
+
+
+
+
+
+
+
+
+
+ {{app_name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hey there!
+
+
+
+
+
+ You requested to
+ reset your {{app_name}} account
+ password.
+
+
+
+
+
+ Please
+ click here
+ to reset your account password and
+ recover your account.
+
+ If you did not request to reset
+ your account password, you can
+ safely ignore this email.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .
+
+
+
+ This email has been sent to you
+ because you signed up on {{app_name}}
+ - a service by Streetwriters
+ (Private) Ltd.
+
+
+ 1st Floor, Valley Plaza, Mardowal
+ Chowk, Naushera
+
+
+ Khushab, Punjab 41100
+ Pakistan
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Streetwriters.Identity/Templates/ResetAccountPassword.txt b/Streetwriters.Identity/Templates/ResetAccountPassword.txt
new file mode 100644
index 0000000..137456a
--- /dev/null
+++ b/Streetwriters.Identity/Templates/ResetAccountPassword.txt
@@ -0,0 +1,17 @@
+************
+{{app_name}}
+************
+
+Hey there!
+
+You requested to *reset your {{app_name}} account password*.
+
+Please go to this link to reset your account password: {{reset_link}}
+
+If you did not request to reset your account password, you can safely ignore this email.
+
+------------
+
+This email has been sent to you because you signed up on {{app_name}} - a service by Streetwriters (Private) Ltd.
+1st Floor, Valley Plaza, Mardowal Chowk, Naushera
+Khushab, Punjab 41100 Pakistan
\ No newline at end of file
diff --git a/Streetwriters.Identity/Validation/BearerTokenValidator.cs b/Streetwriters.Identity/Validation/BearerTokenValidator.cs
new file mode 100644
index 0000000..023e09c
--- /dev/null
+++ b/Streetwriters.Identity/Validation/BearerTokenValidator.cs
@@ -0,0 +1,62 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System.Linq;
+using IdentityModel;
+using IdentityServer4.Validation;
+using Microsoft.AspNetCore.Http;
+
+namespace Streetwriters.Identity.Validation
+{
+ public class BearerTokenValidator
+ {
+ ///
+ /// Validates the authorization header.
+ ///
+ /// The context.
+ ///
+ public static BearerTokenUsageValidationResult ValidateAuthorizationHeader(HttpContext context)
+ {
+ var authorizationHeader = context.Request.Headers["Authorization"].FirstOrDefault();
+ if (!string.IsNullOrEmpty(authorizationHeader))
+ {
+ var header = authorizationHeader.Trim();
+ if (header.StartsWith(OidcConstants.AuthenticationSchemes.AuthorizationHeaderBearer))
+ {
+ var value = header.Substring(OidcConstants.AuthenticationSchemes.AuthorizationHeaderBearer.Length).Trim();
+ if (!string.IsNullOrEmpty(value))
+ {
+ return new BearerTokenUsageValidationResult
+ {
+ TokenFound = true,
+ Token = value,
+ UsageType = BearerTokenUsageType.AuthorizationHeader
+ };
+ }
+ }
+ else
+ {
+
+ }
+ }
+
+ return new BearerTokenUsageValidationResult();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Validation/CustomResourceOwnerValidator.cs b/Streetwriters.Identity/Validation/CustomResourceOwnerValidator.cs
new file mode 100644
index 0000000..874db66
--- /dev/null
+++ b/Streetwriters.Identity/Validation/CustomResourceOwnerValidator.cs
@@ -0,0 +1,152 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using IdentityServer4;
+using IdentityServer4.AspNetIdentity;
+using IdentityServer4.Models;
+using IdentityServer4.Validation;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Logging;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+using static IdentityModel.OidcConstants;
+
+namespace Streetwriters.Identity.Validation
+{
+ public class CustomResourceOwnerValidator : IResourceOwnerPasswordValidator
+ {
+ private UserManager UserManager { get; set; }
+ private SignInManager SignInManager { get; set; }
+ private IMFAService MFAService { get; set; }
+ private ITokenGenerationService TokenGenerationService { get; set; }
+ private IdentityServerTools Tools { get; set; }
+ public CustomResourceOwnerValidator(UserManager userManager, SignInManager signInManager, IMFAService mfaService, ITokenGenerationService tokenGenerationService, IdentityServerTools tools)
+ {
+ UserManager = userManager;
+ SignInManager = signInManager;
+ MFAService = mfaService;
+ TokenGenerationService = tokenGenerationService;
+ Tools = tools;
+ }
+
+ public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
+ {
+ var user = await UserManager.FindByNameAsync(context.UserName);
+ if (user != null)
+ {
+ var result = await SignInManager.CheckPasswordSignInAsync(user, context.Password, true);
+
+ if (result.IsLockedOut)
+ {
+ var timeLeft = user.LockoutEnd - DateTimeOffset.Now;
+ context.Result.IsError = true;
+ context.Result.Error = "user_locked_out";
+ context.Result.ErrorDescription = $"You have been locked out. Please try again in {Pluralize(timeLeft?.Minutes, "minute", "minutes")} and {Pluralize(timeLeft?.Seconds, "second", "seconds")}.";
+ return;
+ }
+
+ var success = result.Succeeded;
+ var isMultiFactor = await UserManager.GetTwoFactorEnabledAsync(user);
+
+ // We'll ask for 2FA regardless of password being incorrect to prevent an attacker
+ // from knowing whether the password is correct or not.
+ if (isMultiFactor)
+ {
+ var primaryMethod = MFAService.GetPrimaryMethod(user);
+ var secondaryMethod = MFAService.GetSecondaryMethod(user);
+
+ var mfaCode = context.Request.Raw["mfa:code"];
+ var mfaMethod = context.Request.Raw["mfa:method"];
+
+ if (string.IsNullOrEmpty(mfaCode) || !MFAService.IsValidMFAMethod(mfaMethod))
+ {
+ var sendPhoneNumber = primaryMethod == MFAMethods.SMS || secondaryMethod == MFAMethods.SMS;
+
+ var token = await TokenGenerationService.CreateAccessTokenFromValidatedRequestAsync(context.Request, user, new[] { Config.MFA_GRANT_TYPE_SCOPE });
+ context.Result.CustomResponse = new System.Collections.Generic.Dictionary
+ {
+ ["error"] = "mfa_required",
+ ["error_description"] = "Multifactor authentication required.",
+ ["data"] = JsonSerializer.Serialize(new MFARequiredResponse
+ {
+ PhoneNumber = sendPhoneNumber ? Regex.Replace(user.PhoneNumber, @"\d(?!\d{0,3}$)", "*") : null,
+ PrimaryMethod = primaryMethod,
+ SecondaryMethod = secondaryMethod,
+ Token = token,
+ })
+ };
+ context.Result.IsError = true;
+ return;
+ }
+ else if (mfaMethod == MFAMethods.RecoveryCode)
+ {
+ var recoveryCodeResult = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, mfaCode);
+ if (!recoveryCodeResult.Succeeded)
+ {
+ context.Result.IsError = true;
+ context.Result.Error = "invalid_2fa_recovery_code";
+ context.Result.ErrorDescription = recoveryCodeResult.Errors.ToErrors().First();
+ return;
+ }
+ }
+ else
+ {
+ var provider = mfaMethod == MFAMethods.Email || mfaMethod == MFAMethods.SMS ? TokenOptions.DefaultPhoneProvider : UserManager.Options.Tokens.AuthenticatorTokenProvider;
+ var isMFACodeValid = await MFAService.VerifyOTPAsync(user, mfaCode, mfaMethod);
+ if (!isMFACodeValid)
+ {
+ context.Result.IsError = true;
+ context.Result.Error = "invalid_2fa_code";
+ context.Result.ErrorDescription = "Please provide a valid multi factor authentication code.";
+ return;
+ }
+ }
+
+ // if we are here, it means we succeeded.
+ success = true;
+ }
+
+ if (success)
+ {
+ var sub = await UserManager.GetUserIdAsync(user);
+ context.Result = new GrantValidationResult(sub, AuthenticationMethods.Password);
+ return;
+ }
+ }
+
+ context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
+ }
+
+ string Pluralize(int? value, string singular, string plural)
+ {
+ if (value == null) return $"0 {plural}";
+ return value == 1 ? $"{value} {singular}" : $"{value} {plural}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Validation/EmailGrantValidator.cs b/Streetwriters.Identity/Validation/EmailGrantValidator.cs
new file mode 100644
index 0000000..c11160e
--- /dev/null
+++ b/Streetwriters.Identity/Validation/EmailGrantValidator.cs
@@ -0,0 +1,106 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using IdentityServer4;
+using IdentityServer4.Models;
+using IdentityServer4.Stores;
+using IdentityServer4.Validation;
+using Microsoft.AspNetCore.Identity;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+using static IdentityModel.OidcConstants;
+
+namespace Streetwriters.Identity.Validation
+{
+ public class EmailGrantValidator : IExtensionGrantValidator
+ {
+ private UserManager UserManager { get; set; }
+ private SignInManager SignInManager { get; set; }
+ private IMFAService MFAService { get; set; }
+ private ITokenGenerationService TokenGenerationService { get; set; }
+ private JwtRequestValidator JWTRequestValidator { get; set; }
+ private IResourceStore ResourceStore { get; set; }
+ private IUserClaimsPrincipalFactory PrincipalFactory { get; set; }
+ public EmailGrantValidator(UserManager userManager, SignInManager signInManager, IMFAService mfaService, ITokenGenerationService tokenGenerationService,
+ IResourceStore resourceStore, IUserClaimsPrincipalFactory principalFactory)
+ {
+ UserManager = userManager;
+ SignInManager = signInManager;
+ MFAService = mfaService;
+ TokenGenerationService = tokenGenerationService;
+ ResourceStore = resourceStore;
+ PrincipalFactory = principalFactory;
+ }
+
+ public string GrantType => Config.EMAIL_GRANT_TYPE;
+
+
+ public async Task ValidateAsync(ExtensionGrantValidationContext context)
+ {
+ var email = context.Request.Raw["email"];
+ var user = await UserManager.FindByEmailAsync(email);
+ if (user == null)
+ {
+ user = new User
+ {
+ Id = MongoDB.Bson.ObjectId.GenerateNewId(),
+ Email = email,
+ UserName = email,
+ NormalizedEmail = email,
+ NormalizedUserName = email,
+ EmailConfirmed = false,
+ SecurityStamp = ""
+ };
+ }
+ var isMultiFactor = await UserManager.GetTwoFactorEnabledAsync(user);
+
+ var primaryMethod = isMultiFactor ? MFAService.GetPrimaryMethod(user) : MFAMethods.Email;
+ var secondaryMethod = MFAService.GetSecondaryMethod(user);
+ var sendPhoneNumber = primaryMethod == MFAMethods.SMS || secondaryMethod == MFAMethods.SMS;
+
+ context.Result.CustomResponse = new Dictionary
+ {
+ ["additional_data"] = new MFARequiredResponse
+ {
+ PhoneNumber = sendPhoneNumber ? Regex.Replace(user.PhoneNumber, @"\d(?!\d{0,3}$)", "*") : null,
+ PrimaryMethod = primaryMethod,
+ SecondaryMethod = secondaryMethod,
+ }
+ };
+ context.Result.IsError = false;
+ context.Result.Subject = await TokenGenerationService.TransformTokenRequestAsync(context.Request, user, GrantType, new string[] { Config.MFA_GRANT_TYPE_SCOPE });
+ }
+
+
+ string Pluralize(int? value, string singular, string plural)
+ {
+ if (value == null) return $"0 {plural}";
+ return value == 1 ? $"{value} {singular}" : $"{value} {plural}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Validation/LockedOutValidationResult.cs b/Streetwriters.Identity/Validation/LockedOutValidationResult.cs
new file mode 100644
index 0000000..937e38d
--- /dev/null
+++ b/Streetwriters.Identity/Validation/LockedOutValidationResult.cs
@@ -0,0 +1,36 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using IdentityServer4.Validation;
+
+namespace Streetwriters.Identity.Validation
+{
+ public class LockedOutValidationResult : GrantValidationResult
+ {
+ public LockedOutValidationResult(TimeSpan? timeLeft)
+ {
+ base.Error = "locked_out";
+ if (timeLeft.HasValue)
+ base.ErrorDescription = $"You have been locked out. Please try again in {timeLeft?.Minutes.Pluralize("minute", "minutes")} and {timeLeft?.Seconds.Pluralize("second", "seconds")}.";
+ else
+ base.ErrorDescription = $"You have been locked out.";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Validation/MFAGrantValidator.cs b/Streetwriters.Identity/Validation/MFAGrantValidator.cs
new file mode 100644
index 0000000..737aaba
--- /dev/null
+++ b/Streetwriters.Identity/Validation/MFAGrantValidator.cs
@@ -0,0 +1,142 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using IdentityModel;
+using IdentityServer4.Models;
+using IdentityServer4.Stores;
+using IdentityServer4.Validation;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Identity;
+using Ng.Services;
+using Streetwriters.Common;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Interfaces;
+using Streetwriters.Identity.Models;
+using static IdentityModel.OidcConstants;
+
+namespace Streetwriters.Identity.Validation
+{
+ public class MFAGrantValidator : IExtensionGrantValidator
+ {
+ private UserManager UserManager { get; set; }
+ private SignInManager SignInManager { get; set; }
+ private IMFAService MFAService { get; set; }
+ private IHttpContextAccessor HttpContextAccessor { get; set; }
+ private ITokenValidator TokenValidator { get; set; }
+ private ITokenGenerationService TokenGenerationService { get; set; }
+ private IEmailSender EmailSender { get; set; }
+ public MFAGrantValidator(UserManager userManager, SignInManager signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, ITokenGenerationService tokenGenerationService, IEmailSender emailSender)
+ {
+ UserManager = userManager;
+ SignInManager = signInManager;
+ MFAService = mfaService;
+ HttpContextAccessor = httpContextAccessor;
+ TokenValidator = tokenValidator;
+ TokenGenerationService = tokenGenerationService;
+ EmailSender = emailSender;
+ }
+
+ public string GrantType => Config.MFA_GRANT_TYPE;
+
+ public async Task ValidateAsync(ExtensionGrantValidationContext context)
+ {
+ context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
+
+ var httpContext = HttpContextAccessor.HttpContext;
+ var tokenResult = BearerTokenValidator.ValidateAuthorizationHeader(httpContext);
+ if (!tokenResult.TokenFound) return;
+
+ var tokenValidationResult = await TokenValidator.ValidateAccessTokenAsync(tokenResult.Token, Config.MFA_GRANT_TYPE_SCOPE);
+ if (tokenValidationResult.IsError) return;
+
+ var client = Clients.FindClientById(tokenValidationResult.Claims.GetClaimValue("client_id"));
+ if (client == null)
+ {
+ context.Result = new GrantValidationResult(TokenRequestErrors.InvalidClient);
+ return;
+ }
+
+ var userId = tokenValidationResult.Claims.GetClaimValue("sub");
+ var mfaCode = context.Request.Raw["mfa:code"];
+ var mfaMethod = context.Request.Raw["mfa:method"];
+
+ if (string.IsNullOrEmpty(userId)) return;
+
+ var user = await UserManager.FindByIdAsync(userId);
+ if (user == null) return;
+
+ context.Result.Error = "invalid_mfa";
+ context.Result.ErrorDescription = "Please provide a valid multi-factor authentication code.";
+
+ if (string.IsNullOrEmpty(mfaCode)) return;
+ if (string.IsNullOrEmpty(mfaMethod))
+ {
+ context.Result.ErrorDescription = "Please provide a valid multi-factor authentication method.";
+ return;
+ }
+
+ var isLockedOut = await UserManager.IsLockedOutAsync(user);
+ if (isLockedOut)
+ {
+ var timeLeft = user.LockoutEnd - DateTimeOffset.Now;
+ context.Result = new LockedOutValidationResult(timeLeft);
+ return;
+ }
+
+ if (mfaMethod == MFAMethods.RecoveryCode)
+ {
+ var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, mfaCode);
+ if (!result.Succeeded)
+ {
+ await UserManager.AccessFailedAsync(user);
+ context.Result.ErrorDescription = "Please provide a valid multi-factor authentication recovery code.";
+ await EmailSender.SendFailedLoginAlertAsync(user.Email, httpContext.GetClientInfo(), client).ConfigureAwait(false);
+ return;
+ }
+ }
+
+ var provider = mfaMethod == MFAMethods.Email || mfaMethod == MFAMethods.SMS ? TokenOptions.DefaultPhoneProvider : UserManager.Options.Tokens.AuthenticatorTokenProvider;
+ var isMFACodeValid = await MFAService.VerifyOTPAsync(user, mfaCode, mfaMethod);
+ if (!isMFACodeValid)
+ {
+ await UserManager.AccessFailedAsync(user);
+ await EmailSender.SendFailedLoginAlertAsync(user.Email, httpContext.GetClientInfo(), client).ConfigureAwait(false);
+ return;
+ }
+
+ context.Result.IsError = false;
+ context.Result.Subject = await TokenGenerationService.TransformTokenRequestAsync(context.Request, user, GrantType, new string[] { Config.MFA_PASSWORD_GRANT_TYPE_SCOPE });
+ }
+
+
+ string Pluralize(int? value, string singular, string plural)
+ {
+ if (value == null) return $"0 {plural}";
+ return value == 1 ? $"{value} {singular}" : $"{value} {plural}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/Validation/MFAPasswordGrantValidator.cs b/Streetwriters.Identity/Validation/MFAPasswordGrantValidator.cs
new file mode 100644
index 0000000..ab3d50d
--- /dev/null
+++ b/Streetwriters.Identity/Validation/MFAPasswordGrantValidator.cs
@@ -0,0 +1,106 @@
+/*
+This file is part of the Notesnook Sync Server project (https://notesnook.com/)
+
+Copyright (C) 2022 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the Affero GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+Affero GNU General Public License for more details.
+
+You should have received a copy of the Affero GNU General Public License
+along with this program. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using IdentityServer4.Models;
+using IdentityServer4.Validation;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Identity;
+using Streetwriters.Common;
+using Streetwriters.Common.Enums;
+using Streetwriters.Common.Models;
+using Streetwriters.Identity.Interfaces;
+using static IdentityModel.OidcConstants;
+
+namespace Streetwriters.Identity.Validation
+{
+ public class MFAPasswordGrantValidator : IExtensionGrantValidator
+ {
+ private UserManager UserManager { get; set; }
+ private SignInManager SignInManager { get; set; }
+ private IMFAService MFAService { get; set; }
+ private IHttpContextAccessor HttpContextAccessor { get; set; }
+ private ITokenValidator TokenValidator { get; set; }
+ private IEmailSender EmailSender { get; set; }
+
+ public MFAPasswordGrantValidator(UserManager userManager, SignInManager signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, IEmailSender emailSender)
+ {
+ UserManager = userManager;
+ SignInManager = signInManager;
+ MFAService = mfaService;
+ HttpContextAccessor = httpContextAccessor;
+ TokenValidator = tokenValidator;
+ EmailSender = emailSender;
+ }
+ public string GrantType => Config.MFA_PASSWORD_GRANT_TYPE;
+
+ public async Task ValidateAsync(ExtensionGrantValidationContext context)
+ {
+ context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
+
+ var httpContext = HttpContextAccessor.HttpContext;
+ var tokenResult = BearerTokenValidator.ValidateAuthorizationHeader(httpContext);
+ if (!tokenResult.TokenFound) return;
+
+
+ var tokenValidationResult = await TokenValidator.ValidateAccessTokenAsync(tokenResult.Token, Config.MFA_PASSWORD_GRANT_TYPE_SCOPE);
+ if (tokenValidationResult.IsError) return;
+
+
+ var client = Clients.FindClientById(tokenValidationResult.Claims.GetClaimValue("client_id"));
+ if (client == null)
+ {
+ context.Result = new GrantValidationResult(TokenRequestErrors.InvalidClient);
+ return;
+ }
+
+ var userId = tokenValidationResult.Claims.GetClaimValue("sub");
+ var password = context.Request.Raw["password"];
+
+ if (string.IsNullOrEmpty(userId)) return;
+
+ context.Result.Error = "unauthorized";
+ context.Result.ErrorDescription = "Password is incorrect.";
+
+ if (string.IsNullOrEmpty(password)) return;
+
+ var user = await UserManager.FindByIdAsync(userId);
+ if (user == null) return;
+
+ var result = await SignInManager.CheckPasswordSignInAsync(user, password, true);
+ if (!result.Succeeded)
+ {
+ await EmailSender.SendFailedLoginAlertAsync(user.Email, httpContext.GetClientInfo(), client).ConfigureAwait(false);
+ return;
+ }
+ else if (result.IsLockedOut)
+ {
+ var timeLeft = user.LockoutEnd - DateTimeOffset.Now;
+ context.Result = new LockedOutValidationResult(timeLeft);
+ return;
+ }
+
+ var sub = await UserManager.GetUserIdAsync(user);
+ context.Result = new GrantValidationResult(sub, AuthenticationMethods.Password);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Streetwriters.Identity/appsettings.Development.json b/Streetwriters.Identity/appsettings.Development.json
new file mode 100644
index 0000000..8081306
--- /dev/null
+++ b/Streetwriters.Identity/appsettings.Development.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "MongoDbSettings": {
+ "ConnectionString": "mongodb://localhost:27017/identity",
+ "DatabaseName": "identity"
+ }
+}