open source identity server

This commit is contained in:
Abdullah Atta
2022-12-28 17:24:47 +05:00
parent 8fcb5257eb
commit 4e9f82fe48
71 changed files with 7382 additions and 2 deletions

3
.gitignore vendored
View File

@@ -262,4 +262,5 @@ __pycache__/
keys/
dist/
appsettings.json
appsettings.json
keystore/

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<IdentityResource> IdentityResources =>
new List<IdentityResource> {
new IdentityResources.OpenId(),
};
public static IEnumerable<ApiResource> ApiResources =>
new List<ApiResource>
{
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<ApiScope> ApiScopes =>
new List<ApiScope>
{
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<Client> Clients =>
new List<Client>
{
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" },
}
};
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> PrincipalFactory { get; set; }
private IdentityServerOptions ISOptions { get; set; }
public AccountController(UserManager<User> _userManager, IEmailSender _emailSender,
SignInManager<User> _signInManager, RoleManager<MongoRole> _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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<AccountController>.Info("ResetUserPassword", user.Email, callbackUrl);
await EmailSender.SendPasswordResetEmailAsync(user.Email, callbackUrl, client);
return Ok();
#endif
}
[HttpPost("logout")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<bool> IsUserValidAsync(User user, string clientId)
{
return user != null && await UserManager.IsInRoleAsync(user, clientId);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> UserManager { get; set; }
protected SignInManager<User> SignInManager { get; set; }
protected RoleManager<MongoRole> RoleManager { get; set; }
protected IEmailSender EmailSender { get; set; }
protected UrlEncoder UrlEncoder { get; set; }
protected IMFAService MFAService { get; set; }
public IdentityControllerBase(
UserManager<User> _userManager,
IEmailSender _emailSender,
SignInManager<User> _signInManager,
RoleManager<MongoRole> _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<string> errors)
{
return base.BadRequest(new { errors });
}
return base.BadRequest(new { error });
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> _userManager, IEmailSender _emailSender,
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) { }
[HttpPost]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> _userManager, IEmailSender _emailSender,
SignInManager<User> _signInManager, RoleManager<MongoRole> _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<IActionResult> 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());
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
namespace Streetwriters.Identity.Enums
{
public enum TokenType
{
CONFRIM_EMAIL = 0,
RESET_PASSWORD = 1,
CHANGE_EMAIL = 2,
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<string> ToErrors(this IEnumerable<IdentityError> collection)
{
return collection.Select((e) => e.Description);
}
public static string GetClaimValue(this IEnumerable<Claim> claims, string type)
{
return claims.FirstOrDefault((c) => c.Type == type)?.Value;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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}";
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<AuthenticationTicket> ITicketStore.RetrieveAsync(string key)
{
AuthenticationTicket ticket;
_cache.TryGetValue(key, out ticket);
return Task.FromResult(ticket);
}
async Task<string> ITicketStore.StoreAsync(AuthenticationTicket ticket)
{
var id = Guid.NewGuid();
var key = KeyPrefix + id;
await ((ITicketStore)this).RenewAsync(key, ticket);
return key;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<ApplicationType, IAppHandler> Handlers { get; set; } = new Dictionary<ApplicationType, IAppHandler>
{
{ ApplicationType.NOTESNOOK, new NotesnookHandler() }
};
public static IAppHandler GetClientHandler(ApplicationType type)
{
return Handlers[type];
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
});
}
}
}

View File

@@ -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
{
/// <summary>
/// The default token response generator
/// </summary>
/// <seealso cref="IdentityServer4.ResponseHandling.ITokenResponseGenerator" />
public class TokenResponseHandler : TokenResponseGenerator, ITokenResponseGenerator
{
/// <summary>
/// Initializes a new instance of the <see cref="TokenResponseGenerator" /> class.
/// </summary>
/// <param name="clock">The clock.</param>
/// <param name="tokenService">The token service.</param>
/// <param name="refreshTokenService">The refresh token service.</param>
/// <param name="scopeParser">The scope parser.</param>
/// <param name="resources">The resources.</param>
/// <param name="clients">The clients.</param>
/// <param name="logger">The logger.</param>
public TokenResponseHandler(ISystemClock clock, ITokenService tokenService, IRefreshTokenService refreshTokenService, IScopeParser scopeParser, IResourceStore resources, IClientStore clients, ILogger<TokenResponseGenerator> logger)
: base(clock, tokenService, refreshTokenService, scopeParser, resources, clients, logger)
{
}
protected override async Task<TokenResponse> ProcessRefreshTokenRequestAsync(TokenRequestValidationResult request)
{
var response = await base.ProcessRefreshTokenRequestAsync(request);
// Fixes: https://github.com/IdentityServer/IdentityServer3/issues/3621
response.IdentityToken = null;
return response;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
using System.Threading.Tasks;
namespace Streetwriters.Identity.Interfaces
{
public interface IAppHandler
{
string Host { get; }
string EmailConfirmedRedirectURL { get; }
string AccountRecoveryRedirectURL { get; }
Task OnEmailConfirmed(string userId);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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; }
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<bool> DisableMFAAsync(User user);
Task SetSecondaryMethodAsync(User user, string secondaryMethod);
string GetPrimaryMethod(User user);
string GetSecondaryMethod(User user);
Task<int> GetRemainingValidCodesAsync(User user);
bool IsValidMFAMethod(string method);
Task<AuthenticatorDetails> GetAuthenticatorDetailsAsync(User user, IClient client);
Task SendOTPAsync(User user, IClient client, MultiFactorSetupForm form, bool isSetup = false);
Task<bool> VerifyOTPAsync(User user, string code, string method);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityServer4.Validation;
using Streetwriters.Common.Models;
namespace Streetwriters.Identity.Interfaces
{
public interface ITokenGenerationService
{
Task<string> CreateAccessTokenAsync(User user, string clientId);
Task<string> CreateAccessTokenFromValidatedRequestAsync(ValidatedTokenRequest validatedRequest, User user, string[] scopes, int lifetime = 60);
Task<ClaimsPrincipal> TransformTokenRequestAsync(ValidatedTokenRequest request, User user, string grantType, string[] scopes, int lifetime = 20 * 60);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> userManager)
{
var user = await userManager.FindByIdAsync(message.UserId);
var client = Clients.FindClientByAppId(message.AppId);
if (client == null || user == null) return;
IdentityUserClaim<string> 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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> userManager)
{
var user = await userManager.FindByIdAsync(message.UserId);
var client = Clients.FindClientByAppId(message.AppId);
if (client != null || user != null) return;
IdentityUserClaim<string> statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == $"{client.Id}:status");
if (statusClaim != null)
{
await userManager.RemoveClaimAsync(user, statusClaim.ToClaim());
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
namespace Streetwriters.Identity.Models
{
public class AuthenticatorDetails
{
public string SharedKey
{
get; set;
}
public string AuthenticatorUri
{
get; set;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
namespace Streetwriters.Identity.Models
{
public class DeleteAccountForm
{
[Required]
public string Password
{
get; set;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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; }
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
using System.Text.Json.Serialization;
namespace Streetwriters.Identity.Models
{
public class MFAPasswordRequiredResponse
{
[JsonPropertyName("token")]
public string Token { get; set; }
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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; }
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.Mvc;
namespace Streetwriters.Identity.Models
{
public class MessageBirdOptions
{
public string AccessKey { get; set; }
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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; }
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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; }
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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; }
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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; }
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Startup>().UseUrls(Servers.IdentityServer.ToString());
});
}
}

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> UserManager { get; }
public CustomIntrospectionResponseGenerator(IEventService events, ILogger<IntrospectionResponseGenerator> logger, UserManager<User> userManager) : base(events, logger)
{
UserManager = userManager;
}
public override async Task<Dictionary<string, object>> 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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<DefaultRefreshTokenService> logger) : base(refreshTokenStore, profile, clock, logger)
{
}
protected override Task<bool> 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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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> SmtpOptions { get; set; }
NNGnuPGContext NNGnuPGContext { get; set; }
public EmailSender(IConfiguration configuration, IOptions<SmtpOptions> 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", "<br>")
}
};
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")];
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> UserManager { get; set; }
private IEmailSender EmailSender { get; set; }
private ISMSSender SMSSender { get; set; }
public MFAService(UserManager<User> _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<bool> 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<int> 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<AuthenticatorDetails> 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<bool> 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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
using System;
using Microsoft.AspNetCore.Identity;
using Streetwriters.Common.Models;
using Streetwriters.Identity.Helpers;
namespace Streetwriters.Identity.Services
{
public class Argon2PasswordHasher<TUser> : IPasswordHasher<TUser> 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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> UserManager { get; set; }
public ProfileService(UserManager<User> userManager)
{
UserManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
User user = await UserManager.GetUserAsync(context.Subject);
if (user == null) return;
IList<string> roles = await UserManager.GetRolesAsync(user);
IList<Claim> 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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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> 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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> PrincipalFactory { get; set; }
private IdentityServerOptions ISOptions { get; set; }
private IdentityServerTools Tools { get; set; }
private IResourceStore ResourceStore { get; set; }
public TokenGenerationService(ITokenService tokenService,
IUserClaimsPrincipalFactory<User> principalFactory,
IdentityServerOptions identityServerOptions,
IPersistedGrantStore persistedGrantStore,
IdentityServerTools tools,
IResourceStore resourceStore)
{
TokenService = tokenService;
PrincipalFactory = principalFactory;
ISOptions = identityServerOptions;
PersistedGrantStore = persistedGrantStore;
Tools = tools;
ResourceStore = resourceStore;
}
public async Task<string> 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<ClaimsPrincipal> 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<string> 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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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";
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<SmtpOptions>(Configuration.GetSection("SmtpSettings"));
services.Configure<MessageBirdOptions>(Configuration.GetSection("MessageBirdSettings"));
services.AddTransient<IEmailSender, EmailSender>();
services.AddTransient<ISMSSender, SMSSender>();
services.AddTransient<IPasswordHasher<User>, Argon2PasswordHasher<User>>();
services.AddCors();
//services.AddSingleton<IProfileService, UserService>();
services.AddIdentityMongoDbProvider<User>(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<EmailGrantValidator>()
.AddExtensionGrantValidator<MFAGrantValidator>()
.AddExtensionGrantValidator<MFAPasswordGrantValidator>()
.AddOperationalStore(options =>
{
options.ConnectionString = connectionString;
})
.AddConfigurationStore(options =>
{
options.ConnectionString = connectionString;
})
.AddAspNetIdentity<User>()
.AddInMemoryClients(Config.Clients)
.AddInMemoryApiResources(Config.ApiResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddKeyManagement()
.AddFileSystemPersistence(Path.Combine(WebHostEnvironment.ContentRootPath, @"keystore"));
services.Configure<DataProtectionTokenProviderOptions>(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<IMFAService, MFAService>();
services.AddControllers();
services.AddTransient<IIntrospectionResponseGenerator, CustomIntrospectionResponseGenerator>();
services.AddTransient<IProfileService, ProfileService>();
services.AddTransient<ITokenGenerationService, TokenGenerationService>();
services.AddTransient<ITokenResponseGenerator, TokenResponseHandler>();
services.AddTransient<IRefreshTokenService, CustomRefreshTokenService>();
services.AddTransient<IResourceOwnerPasswordValidator, CustomResourceOwnerValidator>();
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<UserManager<User>>();
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<UserManager<User>>();
await MessageHandlers.DeleteSubscription.Process(message, userManager);
}
});
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health");
});
}
}
}

View File

@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<StartupObject>Streetwriters.Identity.Program</StartupObject>
<LangVersion>10.0</LangVersion>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
<PropertyGroup>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="MailKit" Version="3.4.3" />
<PackageReference Include="MessageBird" Version="3.2.0" />
<PackageReference Include="Ng.UserAgentService" Version="1.1.2" />
<PackageReference Include="Scriban" Version="5.5.1" />
<PackageReference Include="SendGrid" Version="9.24.4" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Sodium.Core" Version="1.2.3" />
<PackageReference Include="IdentityServer4.Contrib.MongoDB" Version="4.0.0-rc.2" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Usemam.IdentityServer4.KeyRack" Version="0.2.0" />
<PackageReference Include="Usemam.IdentityServer4.KeyRack.DataProtection" Version="0.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="6.0.0" />
<PackageReference Include="AspNetCore.Identity.Mongo" Version="8.3.3" />
<PackageReference Include="WebMarkupMin.Core" Version="2.13.0" />
<PackageReference Include="WebMarkupMin.NUglify" Version="2.12.0" />
</ItemGroup>
<ItemGroup>
<Content Include="Templates\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Streetwriters.Common\Streetwriters.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,823 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html
data-editor-version="2"
class="sg-campaigns"
xmlns="http://www.w3.org/1999/xhtml"
>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
/>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {
width: 600px;
margin: 0 auto;
}
table {
border-collapse: collapse;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
</style>
<![endif]-->
<style type="text/css">
body,
p,
div {
font-family: arial, helvetica, sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188e6;
text-decoration: none;
}
p {
margin: 0;
padding: 0;
}
table.wrapper {
width: 100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
ul ul ul ul {
list-style-type: disc !important;
}
ol ol {
list-style-type: lower-roman !important;
}
ol ol ol {
list-style-type: lower-latin !important;
}
ol ol ol ol {
list-style-type: decimal !important;
}
@media screen and (max-width: 480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
.social-icon-column {
display: inline-block !important;
}
}
</style>
<style>
@media screen and (max-width: 480px) {
table\0 {
width: 480px !important;
}
}
</style>
<!--user entered Head Start-->
<!--End Head user entered-->
</head>
<body>
<center
class="wrapper"
data-link-color="#1188E6"
data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;"
>
<div class="webkit">
<table
cellpadding="0"
cellspacing="0"
border="0"
width="100%"
class="wrapper"
bgcolor="#FFFFFF"
>
<tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table
width="100%"
role="content-container"
class="outer"
align="center"
cellpadding="0"
cellspacing="0"
border="0"
>
<tr>
<td width="100%">
<table
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
>
<tr>
<td>
<!--[if mso]>
<center>
<table><tr><td width="600">
<![endif]-->
<table
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
style="width: 100%; max-width: 600px"
align="center"
>
<tr>
<td
role="modules-container"
style="
padding: 0px 0px 0px 0px;
color: #000000;
text-align: left;
"
bgcolor="#FFFFFF"
width="100%"
align="left"
>
<table
class="module preheader preheader-hide"
role="module"
data-type="preheader"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="
display: none !important;
mso-hide: all;
visibility: hidden;
opacity: 0;
color: transparent;
height: 0;
width: 0;
"
>
<tr>
<td role="module-content">
<p>
Confirm your email to activate your
{{app_name}} account.
</p>
</td>
</tr>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="14957e19-e5cc-41de-b9a5-4ee51daed01f"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 0px 0px 0px 0px;
line-height: 40px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<h1 style="text-align: center">
{{app_name}}
</h1>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="47ff054b-8636-4ba1-b9fc-a67b7bec8707"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
Hey there!
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
<br />
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
Thank you so much for signing up on
{{app_name}}!
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
<br />
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
Please confirm your email by
clicking
<a href="{{confirm_link}}">here</a>.
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table
border="0"
cellpadding="0"
cellspacing="0"
class="module"
data-role="module-button"
data-type="button"
role="module"
style="table-layout: fixed"
width="100%"
data-muid="be84daa7-3077-40d3-9592-f8de5d03c0cf"
>
<tbody>
<tr>
<td
align="center"
bgcolor=""
class="outer-td"
style="padding: 5px 0px 5px 0px"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
class="wrapper-mobile"
style="text-align: center"
>
<tbody>
<tr>
<td
align="center"
bgcolor="#000000"
class="inner-td"
style="
border-radius: 6px;
font-size: 16px;
text-align: center;
background-color: inherit;
"
>
<a
href="{{confirm_link}}"
style="
background-color: #000000;
border: 1px solid #333333;
border-color: #333333;
border-radius: 5px;
border-width: 1px;
color: #ffffff;
display: inline-block;
font-size: 14px;
font-weight: normal;
letter-spacing: 0px;
line-height: normal;
padding: 12px 18px 12px 18px;
text-align: center;
text-decoration: none;
border-style: solid;
"
target="_blank"
>Confirm Email</a
>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="divider"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="aca25e2b-4cbf-43ae-b606-419fa0702f66"
>
<tbody>
<tr>
<td
style="padding: 0px 0px 0px 0px"
role="module-content"
height="100%"
valign="top"
bgcolor=""
>
<table
border="0"
cellpadding="0"
cellspacing="0"
align="center"
width="100%"
height="1px"
style="
line-height: 1px;
font-size: 1px;
"
>
<tbody>
<tr>
<td
style="padding: 0px 0px 1px 0px"
bgcolor="#dbdbdb"
></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="5e37834f-be09-4696-a807-ae46f2725837"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
font-family: arial, helvetica,
sans-serif;
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
float: none;
display: inline;
color: #444444;
font-size: 11px;
"
>This email has been sent to you
because you signed up on </span
><span
style="
font-family: arial, helvetica,
sans-serif;
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
float: none;
display: inline;
font-size: 11px;
color: #000000;
"
>{{app_name}}</span
><span
style="
font-family: arial, helvetica,
sans-serif;
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
float: none;
display: inline;
color: #444444;
font-size: 11px;
"
>
a service of Streetwriters
(Private) Ltd.</span
>
</div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
box-sizing: border-box;
padding-top: 0px;
padding-right: 0px;
padding-bottom: 0px;
padding-left: 0px;
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
margin-left: 0px;
font-style: inherit;
font-variant-ligatures: inherit;
font-variant-caps: inherit;
font-variant-numeric: inherit;
font-variant-east-asian: inherit;
font-weight: inherit;
font-stretch: inherit;
line-height: 20px;
font-family: inherit;
vertical-align: baseline;
border-top-width: 0px;
border-right-width: 0px;
border-bottom-width: 0px;
border-left-width: 0px;
border-top-style: initial;
border-right-style: initial;
border-bottom-style: initial;
border-left-style: initial;
border-top-color: initial;
border-right-color: initial;
border-bottom-color: initial;
border-left-color: initial;
border-image-source: initial;
border-image-slice: initial;
border-image-width: initial;
border-image-outset: initial;
border-image-repeat: initial;
color: #444444;
letter-spacing: normal;
orphans: 2;
text-align: center;
text-indent: 0px;
text-transform: none;
white-space: normal;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
font-size: 11px;
"
>1st Floor, Valley Plaza, Mardowal
Chowk, Naushera</span
>
</div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
box-sizing: border-box;
padding-top: 0px;
padding-right: 0px;
padding-bottom: 0px;
padding-left: 0px;
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
margin-left: 0px;
font-style: inherit;
font-variant-ligatures: inherit;
font-variant-caps: inherit;
font-variant-numeric: inherit;
font-variant-east-asian: inherit;
font-weight: inherit;
font-stretch: inherit;
line-height: 20px;
font-family: inherit;
vertical-align: baseline;
border-top-width: 0px;
border-right-width: 0px;
border-bottom-width: 0px;
border-left-width: 0px;
border-top-style: initial;
border-right-style: initial;
border-bottom-style: initial;
border-left-style: initial;
border-top-color: initial;
border-right-color: initial;
border-bottom-color: initial;
border-left-color: initial;
border-image-source: initial;
border-image-slice: initial;
border-image-width: initial;
border-image-outset: initial;
border-image-repeat: initial;
color: #444444;
letter-spacing: normal;
orphans: 2;
text-align: center;
text-indent: 0px;
text-transform: none;
white-space: normal;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
font-size: 11px;
"
>Khushab,</span
><span style="font-size: 11px">
</span
><span
style="
box-sizing: border-box;
padding-top: 0px;
padding-right: 0px;
padding-bottom: 0px;
padding-left: 0px;
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
margin-left: 0px;
font-style: inherit;
font-variant-ligatures: inherit;
font-variant-caps: inherit;
font-variant-numeric: inherit;
font-variant-east-asian: inherit;
font-weight: inherit;
font-stretch: inherit;
line-height: 20px;
font-family: inherit;
vertical-align: baseline;
border-top-width: 0px;
border-right-width: 0px;
border-bottom-width: 0px;
border-left-width: 0px;
border-top-style: initial;
border-right-style: initial;
border-bottom-style: initial;
border-left-style: initial;
border-top-color: initial;
border-right-color: initial;
border-bottom-color: initial;
border-left-color: initial;
border-image-source: initial;
border-image-slice: initial;
border-image-width: initial;
border-image-outset: initial;
border-image-repeat: initial;
color: #444444;
letter-spacing: normal;
orphans: 2;
text-align: center;
text-indent: 0px;
text-transform: none;
white-space: normal;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
font-size: 11px;
"
>Punjab</span
><span style="font-size: 11px">
</span
><span
style="
color: #444444;
font-size: 11px;
"
>41100 Pakistan</span
>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</center>
</body>
</html>

View File

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

View File

@@ -0,0 +1,435 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {
width: 600px;
margin: 0 auto;
}
table {
border-collapse: collapse;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
</style>
<![endif]-->
<style type="text/css">
body,
p,
div {
font-family: arial, helvetica, sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188e6;
text-decoration: none;
}
p {
margin: 0;
padding: 0;
}
table.wrapper {
width: 100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
ul ul ul ul {
list-style-type: disc !important;
}
ol ol {
list-style-type: lower-roman !important;
}
ol ol ol {
list-style-type: lower-latin !important;
}
ol ol ol ol {
list-style-type: decimal !important;
}
@media screen and (max-width: 480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
.social-icon-column {
display: inline-block !important;
}
}
</style>
<style>
@media screen and (max-width: 480px) {
table\0 {
width: 480px !important;
}
}
</style>
<!--user entered Head Start-->
<!--End Head user entered-->
</head>
<body>
<center class="wrapper" data-link-color="#1188E6"
data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;">
<div class="webkit">
<table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#FFFFFF">
<tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0"
border="0">
<tr>
<td width="100%">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td>
<!--[if mso]>
<center>
<table><tr><td width="600">
<![endif]-->
<table width="100%" cellpadding="0" cellspacing="0" border="0"
style="width: 100%; max-width: 600px" align="center">
<tr>
<td role="modules-container" style="
padding: 0px 0px 0px 0px;
color: #000000;
text-align: left;
" bgcolor="#FFFFFF" width="100%" align="left">
<table class="module preheader preheader-hide" role="module" data-type="preheader"
border="0" cellpadding="0" cellspacing="0" width="100%" style="
display: none !important;
mso-hide: all;
visibility: hidden;
opacity: 0;
color: transparent;
height: 0;
width: 0;
">
<tr>
<td role="module-content">
<p>
Your 2FA code for {{app_name}} is
{{code}}
</p>
</td>
</tr>
</table>
<table class="module" role="module" data-type="text" border="0" cellpadding="0"
cellspacing="0" width="100%" style="table-layout: fixed"
data-muid="ee9d04d5-2dd9-446e-8f33-04b3a587d059" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="
padding: 18px 0px 18px 0px;
line-height: 40px;
text-align: inherit;
" height="100%" valign="top" bgcolor="" role="module-content">
<div>
<h1 style="text-align: center">
{{app_name}}
</h1>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table class="module" role="module" data-type="text" border="0" cellpadding="0"
cellspacing="0" width="100%" style="table-layout: fixed"
data-muid="64d4899b-5911-439e-b9c8-734d70c579c7" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
" height="100%" valign="top" bgcolor="" role="module-content">
<div>
<div style="
font-family: inherit;
text-align: inherit;
">
Please use the following 2FA code to
login to your account:
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table class="module" role="module" data-type="text" border="0" cellpadding="0"
cellspacing="0" width="100%" style="table-layout: fixed"
data-muid="e7321d14-60a8-4113-a301-b95bc8fc3001" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
" height="100%" valign="top" bgcolor="" role="module-content">
<div>
<div style="
font-family: inherit;
text-align: center;
">
<span style="
font-size: 60px;
font-family: 'courier new',
courier, monospace;
">{{code}}</span>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table class="module" role="module" data-type="text" border="0" cellpadding="0"
cellspacing="0" width="100%" style="table-layout: fixed"
data-muid="eaa21ef5-8f16-4212-adf0-f095932f2559" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
" height="100%" valign="top" bgcolor="" role="module-content">
<div>
<div style="
font-family: inherit;
text-align: inherit;
">
<span style="
color: #000000;
font-family: arial, helvetica,
sans-serif;
font-size: 14px;
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
float: none;
display: inline;
"><em>If you did not request to a 2FA
code, please report this to us
at support@streetwriters.co</em></span>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table class="module" role="module" data-type="divider" border="0" cellpadding="0"
cellspacing="0" width="100%" style="table-layout: fixed"
data-muid="ca9de43c-4050-4410-8963-d49989152c4c">
<tbody>
<tr>
<td style="padding: 0px 0px 0px 0px" role="module-content" height="100%"
valign="top" bgcolor="">
<table border="0" cellpadding="0" cellspacing="0" align="center" width="100%"
height="1px" style="
line-height: 1px;
font-size: 1px;
">
<tbody>
<tr>
<td style="padding: 0px 0px 1px 0px" bgcolor="#dbdbdb"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="module" role="module" data-type="text" border="0" cellpadding="0"
cellspacing="0" width="100%" style="table-layout: fixed"
data-muid="3d5612f4-e335-4774-9238-fbe38b2d85ed" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
" height="100%" valign="top" bgcolor="" role="module-content">
<div>
<div style="
font-family: inherit;
text-align: center;
">
<span style="
font-size: 11px;
color: #555555;
">This email has been sent to you
because you signed up on </span><span style="
font-size: 11px;
color: #000000;
"><strong>{{app_name}}</strong></span><span style="
font-size: 11px;
color: #555555;
">
- a service by Streetwriters
(Private) Ltd.</span>
</div>
<div style="
font-family: inherit;
text-align: center;
">
<span style="
font-size: 11px;
color: #555555;
">1st Floor, Valley Plaza, Mardowal
Chowk, Naushera</span>
</div>
<div style="
font-family: inherit;
text-align: center;
">
<span style="
font-size: 11px;
color: #555555;
">Khushab, Punjab 41100
Pakistan</span>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</center>
</body>
</html>

View File

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

View File

@@ -0,0 +1,807 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html
data-editor-version="2"
class="sg-campaigns"
xmlns="http://www.w3.org/1999/xhtml"
>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
/>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {
width: 600px;
margin: 0 auto;
}
table {
border-collapse: collapse;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
</style>
<![endif]-->
<style type="text/css">
body,
p,
div {
font-family: arial, helvetica, sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188e6;
text-decoration: none;
}
p {
margin: 0;
padding: 0;
}
table.wrapper {
width: 100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
ul ul ul ul {
list-style-type: disc !important;
}
ol ol {
list-style-type: lower-roman !important;
}
ol ol ol {
list-style-type: lower-latin !important;
}
ol ol ol ol {
list-style-type: decimal !important;
}
@media screen and (max-width: 480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
.social-icon-column {
display: inline-block !important;
}
}
</style>
<style>
@media screen and (max-width: 480px) {
table\0 {
width: 480px !important;
}
}
</style>
<!--user entered Head Start-->
<!--End Head user entered-->
</head>
<body>
<center
class="wrapper"
data-link-color="#1188E6"
data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;"
>
<div class="webkit">
<table
cellpadding="0"
cellspacing="0"
border="0"
width="100%"
class="wrapper"
bgcolor="#FFFFFF"
>
<tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table
width="100%"
role="content-container"
class="outer"
align="center"
cellpadding="0"
cellspacing="0"
border="0"
>
<tr>
<td width="100%">
<table
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
>
<tr>
<td>
<!--[if mso]>
<center>
<table><tr><td width="600">
<![endif]-->
<table
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
style="width: 100%; max-width: 600px"
align="center"
>
<tr>
<td
role="modules-container"
style="
padding: 0px 0px 0px 0px;
color: #000000;
text-align: left;
"
bgcolor="#FFFFFF"
width="100%"
align="left"
>
<table
class="module preheader preheader-hide"
role="module"
data-type="preheader"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="
display: none !important;
mso-hide: all;
visibility: hidden;
opacity: 0;
color: transparent;
height: 0;
width: 0;
"
>
<tr>
<td role="module-content">
<p>
Confirm your new email to change it for
your {{app_name}} account.
</p>
</td>
</tr>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="14957e19-e5cc-41de-b9a5-4ee51daed01f"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 0px 0px 0px 0px;
line-height: 40px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<h1 style="text-align: center">
{{app_name}}
</h1>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="47ff054b-8636-4ba1-b9fc-a67b7bec8707"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
Hey there!
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
<br />
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
Please confirm your new email by
clicking
<a href="{{confirm_link}}">here</a>
or the button below.
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table
border="0"
cellpadding="0"
cellspacing="0"
class="module"
data-role="module-button"
data-type="button"
role="module"
style="table-layout: fixed"
width="100%"
data-muid="be84daa7-3077-40d3-9592-f8de5d03c0cf"
>
<tbody>
<tr>
<td
align="center"
bgcolor=""
class="outer-td"
style="padding: 5px 0px 5px 0px"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
class="wrapper-mobile"
style="text-align: center"
>
<tbody>
<tr>
<td
align="center"
bgcolor="#000000"
class="inner-td"
style="
border-radius: 6px;
font-size: 16px;
text-align: center;
background-color: inherit;
"
>
<a
href="{{confirm_link}}"
style="
background-color: #000000;
border: 1px solid #333333;
border-color: #333333;
border-radius: 5px;
border-width: 1px;
color: #ffffff;
display: inline-block;
font-size: 14px;
font-weight: normal;
letter-spacing: 0px;
line-height: normal;
padding: 12px 18px 12px 18px;
text-align: center;
text-decoration: none;
border-style: solid;
"
target="_blank"
>Confirm Email</a
>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="divider"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="aca25e2b-4cbf-43ae-b606-419fa0702f66"
>
<tbody>
<tr>
<td
style="padding: 0px 0px 0px 0px"
role="module-content"
height="100%"
valign="top"
bgcolor=""
>
<table
border="0"
cellpadding="0"
cellspacing="0"
align="center"
width="100%"
height="1px"
style="
line-height: 1px;
font-size: 1px;
"
>
<tbody>
<tr>
<td
style="padding: 0px 0px 1px 0px"
bgcolor="#dbdbdb"
></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="5e37834f-be09-4696-a807-ae46f2725837"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
font-family: arial, helvetica,
sans-serif;
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
float: none;
display: inline;
color: #444444;
font-size: 11px;
"
>This email has been sent to you
because you signed up on </span
><span
style="
font-family: arial, helvetica,
sans-serif;
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
float: none;
display: inline;
font-size: 11px;
color: #000000;
"
>{{app_name}}</span
><span
style="
font-family: arial, helvetica,
sans-serif;
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
float: none;
display: inline;
color: #444444;
font-size: 11px;
"
>
a service of Streetwriters
(Private) Ltd.</span
>
</div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
box-sizing: border-box;
padding-top: 0px;
padding-right: 0px;
padding-bottom: 0px;
padding-left: 0px;
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
margin-left: 0px;
font-style: inherit;
font-variant-ligatures: inherit;
font-variant-caps: inherit;
font-variant-numeric: inherit;
font-variant-east-asian: inherit;
font-weight: inherit;
font-stretch: inherit;
line-height: 20px;
font-family: inherit;
vertical-align: baseline;
border-top-width: 0px;
border-right-width: 0px;
border-bottom-width: 0px;
border-left-width: 0px;
border-top-style: initial;
border-right-style: initial;
border-bottom-style: initial;
border-left-style: initial;
border-top-color: initial;
border-right-color: initial;
border-bottom-color: initial;
border-left-color: initial;
border-image-source: initial;
border-image-slice: initial;
border-image-width: initial;
border-image-outset: initial;
border-image-repeat: initial;
color: #444444;
letter-spacing: normal;
orphans: 2;
text-align: center;
text-indent: 0px;
text-transform: none;
white-space: normal;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
font-size: 11px;
"
>1st Floor, Valley Plaza, Mardowal
Chowk, Naushera</span
>
</div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
box-sizing: border-box;
padding-top: 0px;
padding-right: 0px;
padding-bottom: 0px;
padding-left: 0px;
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
margin-left: 0px;
font-style: inherit;
font-variant-ligatures: inherit;
font-variant-caps: inherit;
font-variant-numeric: inherit;
font-variant-east-asian: inherit;
font-weight: inherit;
font-stretch: inherit;
line-height: 20px;
font-family: inherit;
vertical-align: baseline;
border-top-width: 0px;
border-right-width: 0px;
border-bottom-width: 0px;
border-left-width: 0px;
border-top-style: initial;
border-right-style: initial;
border-bottom-style: initial;
border-left-style: initial;
border-top-color: initial;
border-right-color: initial;
border-bottom-color: initial;
border-left-color: initial;
border-image-source: initial;
border-image-slice: initial;
border-image-width: initial;
border-image-outset: initial;
border-image-repeat: initial;
color: #444444;
letter-spacing: normal;
orphans: 2;
text-align: center;
text-indent: 0px;
text-transform: none;
white-space: normal;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
font-size: 11px;
"
>Khushab,</span
><span style="font-size: 11px">
</span
><span
style="
box-sizing: border-box;
padding-top: 0px;
padding-right: 0px;
padding-bottom: 0px;
padding-left: 0px;
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
margin-left: 0px;
font-style: inherit;
font-variant-ligatures: inherit;
font-variant-caps: inherit;
font-variant-numeric: inherit;
font-variant-east-asian: inherit;
font-weight: inherit;
font-stretch: inherit;
line-height: 20px;
font-family: inherit;
vertical-align: baseline;
border-top-width: 0px;
border-right-width: 0px;
border-bottom-width: 0px;
border-left-width: 0px;
border-top-style: initial;
border-right-style: initial;
border-bottom-style: initial;
border-left-style: initial;
border-top-color: initial;
border-right-color: initial;
border-bottom-color: initial;
border-left-color: initial;
border-image-source: initial;
border-image-slice: initial;
border-image-width: initial;
border-image-outset: initial;
border-image-repeat: initial;
color: #444444;
letter-spacing: normal;
orphans: 2;
text-align: center;
text-indent: 0px;
text-transform: none;
white-space: normal;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
font-size: 11px;
"
>Punjab</span
><span style="font-size: 11px">
</span
><span
style="
color: #444444;
font-size: 11px;
"
>41100 Pakistan</span
>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</center>
</body>
</html>

View File

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

View File

@@ -0,0 +1,505 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html
data-editor-version="2"
class="sg-campaigns"
xmlns="http://www.w3.org/1999/xhtml"
>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
/>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {
width: 600px;
margin: 0 auto;
}
table {
border-collapse: collapse;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
</style>
<![endif]-->
<style type="text/css">
body,
p,
div {
font-family: arial, helvetica, sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188e6;
text-decoration: none;
}
p {
margin: 0;
padding: 0;
}
table.wrapper {
width: 100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
ul ul ul ul {
list-style-type: disc !important;
}
ol ol {
list-style-type: lower-roman !important;
}
ol ol ol {
list-style-type: lower-latin !important;
}
ol ol ol ol {
list-style-type: decimal !important;
}
@media screen and (max-width: 480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
.social-icon-column {
display: inline-block !important;
}
}
</style>
<style>
@media screen and (max-width: 480px) {
table\0 {
width: 480px !important;
}
}
</style>
<!--user entered Head Start-->
<!--End Head user entered-->
</head>
<body>
<center
class="wrapper"
data-link-color="#1188E6"
data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;"
>
<div class="webkit">
<table
cellpadding="0"
cellspacing="0"
border="0"
width="100%"
class="wrapper"
bgcolor="#FFFFFF"
>
<tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table
width="100%"
role="content-container"
class="outer"
align="center"
cellpadding="0"
cellspacing="0"
border="0"
>
<tr>
<td width="100%">
<table
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
>
<tr>
<td>
<!--[if mso]>
<center>
<table><tr><td width="600">
<![endif]-->
<table
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
style="width: 100%; max-width: 600px"
align="center"
>
<tr>
<td
role="modules-container"
style="
padding: 0px 0px 0px 0px;
color: #000000;
text-align: left;
"
bgcolor="#FFFFFF"
width="100%"
align="left"
>
<table
class="module preheader preheader-hide"
role="module"
data-type="preheader"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="
display: none !important;
mso-hide: all;
visibility: hidden;
opacity: 0;
color: transparent;
height: 0;
width: 0;
"
>
<tr>
<td role="module-content">
<p></p>
</td>
</tr>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="ee9d04d5-2dd9-446e-8f33-04b3a587d059"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 18px 0px 18px 0px;
line-height: 40px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<h1 style="text-align: center">
{{app_name}}
</h1>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="64d4899b-5911-439e-b9c8-734d70c579c7"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
We detected a failed login attempt
on your {{app_name}} account.
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
<br />
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
{{device_info}}
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
<br />
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
If this was not you
<strong
>please immediately change your
password &amp; 2FA methods</strong
>.
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="divider"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="ca9de43c-4050-4410-8963-d49989152c4c"
>
<tbody>
<tr>
<td
style="padding: 0px 0px 0px 0px"
role="module-content"
height="100%"
valign="top"
bgcolor=""
>
<table
border="0"
cellpadding="0"
cellspacing="0"
align="center"
width="100%"
height="1px"
style="
line-height: 1px;
font-size: 1px;
"
>
<tbody>
<tr>
<td
style="padding: 0px 0px 1px 0px"
bgcolor="#dbdbdb"
></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="3d5612f4-e335-4774-9238-fbe38b2d85ed"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
font-size: 11px;
color: #555555;
"
>This email has been sent to you
because you signed up on </span
><span
style="
font-size: 11px;
color: #000000;
"
><strong
>{{app_name}}</strong
></span
><span
style="
font-size: 11px;
color: #555555;
"
>
- a service by Streetwriters
(Private) Ltd.</span
>
</div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
font-size: 11px;
color: #555555;
"
>1st Floor, Valley Plaza, Mardowal
Chowk, Naushera</span
>
</div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
font-size: 11px;
color: #555555;
"
>Khushab, Punjab 41100
Pakistan</span
>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</center>
</body>
</html>

View File

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

View File

@@ -0,0 +1,658 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html
data-editor-version="2"
class="sg-campaigns"
xmlns="http://www.w3.org/1999/xhtml"
>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
/>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {
width: 600px;
margin: 0 auto;
}
table {
border-collapse: collapse;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
</style>
<![endif]-->
<style type="text/css">
body,
p,
div {
font-family: arial, helvetica, sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188e6;
text-decoration: none;
}
p {
margin: 0;
padding: 0;
}
table.wrapper {
width: 100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
ul ul ul ul {
list-style-type: disc !important;
}
ol ol {
list-style-type: lower-roman !important;
}
ol ol ol {
list-style-type: lower-latin !important;
}
ol ol ol ol {
list-style-type: decimal !important;
}
@media screen and (max-width: 480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
.social-icon-column {
display: inline-block !important;
}
}
</style>
<style>
@media screen and (max-width: 480px) {
table\0 {
width: 480px !important;
}
}
</style>
<!--user entered Head Start-->
<!--End Head user entered-->
</head>
<body>
<center
class="wrapper"
data-link-color="#1188E6"
data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;"
>
<div class="webkit">
<table
cellpadding="0"
cellspacing="0"
border="0"
width="100%"
class="wrapper"
bgcolor="#FFFFFF"
>
<tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table
width="100%"
role="content-container"
class="outer"
align="center"
cellpadding="0"
cellspacing="0"
border="0"
>
<tr>
<td width="100%">
<table
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
>
<tr>
<td>
<!--[if mso]>
<center>
<table><tr><td width="600">
<![endif]-->
<table
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
style="width: 100%; max-width: 600px"
align="center"
>
<tr>
<td
role="modules-container"
style="
padding: 0px 0px 0px 0px;
color: #000000;
text-align: left;
"
bgcolor="#FFFFFF"
width="100%"
align="left"
>
<table
class="module preheader preheader-hide"
role="module"
data-type="preheader"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="
display: none !important;
mso-hide: all;
visibility: hidden;
opacity: 0;
color: transparent;
height: 0;
width: 0;
"
>
<tr>
<td role="module-content">
<p>
Lost access to your {{app_name}}
account?
</p>
</td>
</tr>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="ee9d04d5-2dd9-446e-8f33-04b3a587d059"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 18px 0px 18px 0px;
line-height: 40px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<h1 style="text-align: center">
{{app_name}}
</h1>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="64d4899b-5911-439e-b9c8-734d70c579c7"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
Hey there!
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
<br />
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
You requested to
<strong
>reset your {{app_name}} account
password</strong
>.
</div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
<br />
</div>
<div
style="
font-family: inherit;
text-align: start;
"
>
Please
<a href="{{reset_link}}"
>click here</a
>
to reset your account password and
recover your account.
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table
border="0"
cellpadding="0"
cellspacing="0"
class="module"
data-role="module-button"
data-type="button"
role="module"
style="table-layout: fixed"
width="100%"
data-muid="1c733cb5-53af-4e3b-8ff8-677a52d3a25f"
>
<tbody>
<tr>
<td
align="center"
bgcolor=""
class="outer-td"
style="padding: 5px 0px 5px 0px"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
class="wrapper-mobile"
style="text-align: center"
>
<tbody>
<tr>
<td
align="center"
bgcolor="#000000"
class="inner-td"
style="
border-radius: 6px;
font-size: 16px;
text-align: center;
background-color: inherit;
"
>
<a
href="{{reset_link}}"
style="
background-color: #000000;
border: 1px solid #333333;
border-color: #333333;
border-radius: 6px;
border-width: 1px;
color: #ffffff;
display: inline-block;
font-size: 14px;
font-weight: normal;
letter-spacing: 0px;
line-height: normal;
padding: 12px 18px 12px 18px;
text-align: center;
text-decoration: none;
border-style: solid;
"
target="_blank"
>Reset your password</a
>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="eaa21ef5-8f16-4212-adf0-f095932f2559"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
<td
style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<div
style="
font-family: inherit;
text-align: inherit;
"
>
<span
style="
color: #000000;
font-family: arial, helvetica,
sans-serif;
font-size: 14px;
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: pre-wrap;
widows: 2;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
background-color: rgb(
255,
255,
255
);
text-decoration-thickness: initial;
text-decoration-style: initial;
text-decoration-color: initial;
float: none;
display: inline;
"
><em
>If you did not request to reset
your account password, you can
safely ignore this email.</em
></span
>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="divider"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="ca9de43c-4050-4410-8963-d49989152c4c"
>
<tbody>
<tr>
<td
style="padding: 0px 0px 0px 0px"
role="module-content"
height="100%"
valign="top"
bgcolor=""
>
<table
border="0"
cellpadding="0"
cellspacing="0"
align="center"
width="100%"
height="1px"
style="
line-height: 1px;
font-size: 1px;
"
>
<tbody>
<tr>
<td
style="padding: 0px 0px 1px 0px"
bgcolor="#dbdbdb"
></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
class="module"
role="module"
data-type="text"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="table-layout: fixed"
data-muid="3d5612f4-e335-4774-9238-fbe38b2d85ed"
data-mc-module-version="2019-10-22"
>
<tbody>
<tr>
.
<td
style="
padding: 18px 0px 18px 0px;
line-height: 22px;
text-align: inherit;
"
height="100%"
valign="top"
bgcolor=""
role="module-content"
>
<div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
font-size: 11px;
color: #555555;
"
>This email has been sent to you
because you signed up on </span
><span
style="
font-size: 11px;
color: #000000;
"
><strong
>{{app_name}}</strong
></span
><span
style="
font-size: 11px;
color: #555555;
"
>
- a service by Streetwriters
(Private) Ltd.</span
>
</div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
font-size: 11px;
color: #555555;
"
>1st Floor, Valley Plaza, Mardowal
Chowk, Naushera</span
>
</div>
<div
style="
font-family: inherit;
text-align: center;
"
>
<span
style="
font-size: 11px;
color: #555555;
"
>Khushab, Punjab 41100
Pakistan</span
>
</div>
<div></div>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</center>
</body>
</html>

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
using System.Linq;
using IdentityModel;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Http;
namespace Streetwriters.Identity.Validation
{
public class BearerTokenValidator
{
/// <summary>
/// Validates the authorization header.
/// </summary>
/// <param name="context">The context.</param>
/// <returns></returns>
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();
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> UserManager { get; set; }
private SignInManager<User> SignInManager { get; set; }
private IMFAService MFAService { get; set; }
private ITokenGenerationService TokenGenerationService { get; set; }
private IdentityServerTools Tools { get; set; }
public CustomResourceOwnerValidator(UserManager<User> userManager, SignInManager<User> 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<string, object>
{
["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}";
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> UserManager { get; set; }
private SignInManager<User> 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<User> PrincipalFactory { get; set; }
public EmailGrantValidator(UserManager<User> userManager, SignInManager<User> signInManager, IMFAService mfaService, ITokenGenerationService tokenGenerationService,
IResourceStore resourceStore, IUserClaimsPrincipalFactory<User> 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<string, object>
{
["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}";
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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.";
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> UserManager { get; set; }
private SignInManager<User> 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<User> userManager, SignInManager<User> 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}";
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<User> UserManager { get; set; }
private SignInManager<User> 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<User> userManager, SignInManager<User> 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);
}
}
}

View File

@@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"MongoDbSettings": {
"ConnectionString": "mongodb://localhost:27017/identity",
"DatabaseName": "identity"
}
}