mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 11:12:44 +00:00
open source identity server
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -262,4 +262,5 @@ __pycache__/
|
|||||||
|
|
||||||
keys/
|
keys/
|
||||||
dist/
|
dist/
|
||||||
appsettings.json
|
appsettings.json
|
||||||
|
keystore/
|
||||||
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Data", "Stree
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Messenger", "Streetwriters.Messenger\Streetwriters.Messenger.csproj", "{BDA80415-6C8D-4481-AC31-E5B4D73E9629}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Messenger", "Streetwriters.Messenger\Streetwriters.Messenger.csproj", "{BDA80415-6C8D-4481-AC31-E5B4D73E9629}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Identity", "Streetwriters.Identity\Streetwriters.Identity.csproj", "{6800DEE0-768C-4BEB-B78C-08829EC5A106}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{BDA80415-6C8D-4481-AC31-E5B4D73E9629}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -43,12 +43,18 @@ To run the `Streetwriters.Messenger` project:
|
|||||||
dotnet run --project Streetwriters.Messenger/Streetwriters.Messenger.csproj
|
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
|
## 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!**
|
**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
|
- [x] Open source the Sync server
|
||||||
- [ ] Open source the Identity server
|
- [x] Open source the Identity server
|
||||||
- [x] Open source the SSE Messaging infrastructure
|
- [x] Open source the SSE Messaging infrastructure
|
||||||
- [ ] Fully Dockerize all services
|
- [ ] Fully Dockerize all services
|
||||||
- [ ] Publish on DockerHub
|
- [ ] Publish on DockerHub
|
||||||
|
|||||||
86
Streetwriters.Identity/Config.cs
Normal file
86
Streetwriters.Identity/Config.cs
Normal 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" },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
340
Streetwriters.Identity/Controllers/AccountController.cs
Normal file
340
Streetwriters.Identity/Controllers/AccountController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
Streetwriters.Identity/Controllers/IdentityControllerBase.cs
Normal file
67
Streetwriters.Identity/Controllers/IdentityControllerBase.cs
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
Streetwriters.Identity/Controllers/MFAController.cs
Normal file
138
Streetwriters.Identity/Controllers/MFAController.cs
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
115
Streetwriters.Identity/Controllers/SignupController.cs
Normal file
115
Streetwriters.Identity/Controllers/SignupController.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Streetwriters.Identity/Enums/TokenTypes.cs
Normal file
28
Streetwriters.Identity/Enums/TokenTypes.cs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Streetwriters.Identity/Extensions/HttpContextExtensions.cs
Normal file
56
Streetwriters.Identity/Extensions/HttpContextExtensions.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Streetwriters.Identity/Extensions/IEnumerableExtensions.cs
Normal file
42
Streetwriters.Identity/Extensions/IEnumerableExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Streetwriters.Identity/Extensions/IntExtensions.cs
Normal file
34
Streetwriters.Identity/Extensions/IntExtensions.cs
Normal 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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
Streetwriters.Identity/Extensions/MongoDBTicketStore.cs
Normal file
73
Streetwriters.Identity/Extensions/MongoDBTicketStore.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
Streetwriters.Identity/Extensions/UrlExtensions.cs
Normal file
48
Streetwriters.Identity/Extensions/UrlExtensions.cs
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Streetwriters.Identity/Handlers/ClientHandlers.cs
Normal file
39
Streetwriters.Identity/Handlers/ClientHandlers.cs
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
Streetwriters.Identity/Handlers/NotesnookHandler.cs
Normal file
58
Streetwriters.Identity/Handlers/NotesnookHandler.cs
Normal 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Streetwriters.Identity/Handlers/TokenResponseHandler.cs
Normal file
43
Streetwriters.Identity/Handlers/TokenResponseHandler.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Streetwriters.Identity/Helpers/PasswordHelper.cs
Normal file
38
Streetwriters.Identity/Helpers/PasswordHelper.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Streetwriters.Identity/Interfaces/IAppHandler.cs
Normal file
31
Streetwriters.Identity/Interfaces/IAppHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Streetwriters.Identity/Interfaces/IEmailSender.cs
Normal file
34
Streetwriters.Identity/Interfaces/IEmailSender.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Streetwriters.Identity/Interfaces/IEmailTemplate.cs
Normal file
31
Streetwriters.Identity/Interfaces/IEmailTemplate.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Streetwriters.Identity/Interfaces/IMFAService.cs
Normal file
40
Streetwriters.Identity/Interfaces/IMFAService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Streetwriters.Identity/Interfaces/ISMSSender.cs
Normal file
30
Streetwriters.Identity/Interfaces/ISMSSender.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Streetwriters.Identity/Interfaces/ITokenGenerationService.cs
Normal file
33
Streetwriters.Identity/Interfaces/ITokenGenerationService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Streetwriters.Identity/MessageHandlers/CreateSubscription.cs
Normal file
56
Streetwriters.Identity/MessageHandlers/CreateSubscription.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Streetwriters.Identity/MessageHandlers/DeleteSubscription.cs
Normal file
53
Streetwriters.Identity/MessageHandlers/DeleteSubscription.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Streetwriters.Identity/Models/AuthenticatorDetails.cs
Normal file
34
Streetwriters.Identity/Models/AuthenticatorDetails.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Streetwriters.Identity/Models/ChangeEmailForm.cs
Normal file
39
Streetwriters.Identity/Models/ChangeEmailForm.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Streetwriters.Identity/Models/DeleteAccountForm.cs
Normal file
33
Streetwriters.Identity/Models/DeleteAccountForm.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Streetwriters.Identity/Models/EmailTemplate.cs
Normal file
33
Streetwriters.Identity/Models/EmailTemplate.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Streetwriters.Identity/Models/GetAccessTokenForm.cs
Normal file
49
Streetwriters.Identity/Models/GetAccessTokenForm.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Streetwriters.Identity/Models/MFAPasswordRequiredResponse.cs
Normal file
29
Streetwriters.Identity/Models/MFAPasswordRequiredResponse.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Streetwriters.Identity/Models/MFARequiredResponse.cs
Normal file
35
Streetwriters.Identity/Models/MFARequiredResponse.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Streetwriters.Identity/Models/MessageBirdOptions.cs
Normal file
30
Streetwriters.Identity/Models/MessageBirdOptions.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Streetwriters.Identity/Models/MultiFactorEnableForm.cs
Normal file
43
Streetwriters.Identity/Models/MultiFactorEnableForm.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Streetwriters.Identity/Models/MultiFactorSetupForm.cs
Normal file
37
Streetwriters.Identity/Models/MultiFactorSetupForm.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Streetwriters.Identity/Models/ResetPasswordForm.cs
Normal file
42
Streetwriters.Identity/Models/ResetPasswordForm.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
Streetwriters.Identity/Models/SignupForm.cs
Normal file
57
Streetwriters.Identity/Models/SignupForm.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Streetwriters.Identity/Models/SmtpOptions.cs
Normal file
33
Streetwriters.Identity/Models/SmtpOptions.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Streetwriters.Identity/Models/TwoFactorLoginForm.cs
Normal file
41
Streetwriters.Identity/Models/TwoFactorLoginForm.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Streetwriters.Identity/Models/UpdateUserForm.cs
Normal file
53
Streetwriters.Identity/Models/UpdateUserForm.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Streetwriters.Identity/Program.cs
Normal file
52
Streetwriters.Identity/Program.cs
Normal 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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Streetwriters.Identity/Properties/launchSettings.json
Normal file
30
Streetwriters.Identity/Properties/launchSettings.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Streetwriters.Identity/Services/CustomRefreshTokenService.cs
Normal file
43
Streetwriters.Identity/Services/CustomRefreshTokenService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
275
Streetwriters.Identity/Services/EmailSender.cs
Normal file
275
Streetwriters.Identity/Services/EmailSender.cs
Normal 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")];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
241
Streetwriters.Identity/Services/MFAService.cs
Normal file
241
Streetwriters.Identity/Services/MFAService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Streetwriters.Identity/Services/PasswordHasher.cs
Normal file
47
Streetwriters.Identity/Services/PasswordHasher.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
Streetwriters.Identity/Services/ProfileService.cs
Normal file
61
Streetwriters.Identity/Services/ProfileService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
Streetwriters.Identity/Services/SMSSender.cs
Normal file
59
Streetwriters.Identity/Services/SMSSender.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
116
Streetwriters.Identity/Services/TokenGenerationService.cs
Normal file
116
Streetwriters.Identity/Services/TokenGenerationService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
Streetwriters.Identity/Services/UserService.cs
Normal file
84
Streetwriters.Identity/Services/UserService.cs
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
219
Streetwriters.Identity/Startup.cs
Normal file
219
Streetwriters.Identity/Startup.cs
Normal 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Streetwriters.Identity/Streetwriters.Identity.csproj
Normal file
46
Streetwriters.Identity/Streetwriters.Identity.csproj
Normal 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>
|
||||||
823
Streetwriters.Identity/Templates/ConfirmEmail.html
Normal file
823
Streetwriters.Identity/Templates/ConfirmEmail.html
Normal 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>
|
||||||
17
Streetwriters.Identity/Templates/ConfirmEmail.txt
Normal file
17
Streetwriters.Identity/Templates/ConfirmEmail.txt
Normal 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
|
||||||
435
Streetwriters.Identity/Templates/Email2FACode.html
Normal file
435
Streetwriters.Identity/Templates/Email2FACode.html
Normal 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>
|
||||||
17
Streetwriters.Identity/Templates/Email2FACode.txt
Normal file
17
Streetwriters.Identity/Templates/Email2FACode.txt
Normal 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
|
||||||
807
Streetwriters.Identity/Templates/EmailChangeConfirmation.html
Normal file
807
Streetwriters.Identity/Templates/EmailChangeConfirmation.html
Normal 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>
|
||||||
15
Streetwriters.Identity/Templates/EmailChangeConfirmation.txt
Normal file
15
Streetwriters.Identity/Templates/EmailChangeConfirmation.txt
Normal 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
|
||||||
505
Streetwriters.Identity/Templates/FailedLoginAlert.html
Normal file
505
Streetwriters.Identity/Templates/FailedLoginAlert.html
Normal 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 & 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>
|
||||||
15
Streetwriters.Identity/Templates/FailedLoginAlert.txt
Normal file
15
Streetwriters.Identity/Templates/FailedLoginAlert.txt
Normal 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
|
||||||
658
Streetwriters.Identity/Templates/ResetAccountPassword.html
Normal file
658
Streetwriters.Identity/Templates/ResetAccountPassword.html
Normal 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>
|
||||||
17
Streetwriters.Identity/Templates/ResetAccountPassword.txt
Normal file
17
Streetwriters.Identity/Templates/ResetAccountPassword.txt
Normal 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
|
||||||
62
Streetwriters.Identity/Validation/BearerTokenValidator.cs
Normal file
62
Streetwriters.Identity/Validation/BearerTokenValidator.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
Streetwriters.Identity/Validation/EmailGrantValidator.cs
Normal file
106
Streetwriters.Identity/Validation/EmailGrantValidator.cs
Normal 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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
Streetwriters.Identity/Validation/MFAGrantValidator.cs
Normal file
142
Streetwriters.Identity/Validation/MFAGrantValidator.cs
Normal 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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
Streetwriters.Identity/Validation/MFAPasswordGrantValidator.cs
Normal file
106
Streetwriters.Identity/Validation/MFAPasswordGrantValidator.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Streetwriters.Identity/appsettings.Development.json
Normal file
13
Streetwriters.Identity/appsettings.Development.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MongoDbSettings": {
|
||||||
|
"ConnectionString": "mongodb://localhost:27017/identity",
|
||||||
|
"DatabaseName": "identity"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user