diff --git a/Streetwriters.Identity/Controllers/AccountController.cs b/Streetwriters.Identity/Controllers/AccountController.cs index 3e20de9..e6f7ff9 100644 --- a/Streetwriters.Identity/Controllers/AccountController.cs +++ b/Streetwriters.Identity/Controllers/AccountController.cs @@ -1,344 +1,344 @@ -/* -This file is part of the Notesnook Sync Server project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the Affero GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -Affero GNU General Public License for more details. - -You should have received a copy of the Affero GNU General Public License -along with this program. If not, see . -*/ - -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Security.Claims; -using System.Text.Json; -using System.Threading.Tasks; -using AspNetCore.Identity.Mongo.Model; -using IdentityServer4; -using IdentityServer4.Configuration; -using IdentityServer4.Models; -using IdentityServer4.Stores; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Streetwriters.Common; -using Streetwriters.Common.Enums; -using Streetwriters.Common.Interfaces; -using Streetwriters.Common.Messages; -using Streetwriters.Common.Models; -using Streetwriters.Identity.Enums; -using Streetwriters.Identity.Interfaces; -using Streetwriters.Identity.Models; -using Streetwriters.Identity.Services; -using static IdentityServer4.IdentityServerConstants; - -namespace Streetwriters.Identity.Controllers -{ - [ApiController] - [DisplayName("Account")] - [Route("account")] - [Authorize(LocalApi.PolicyName)] - public class AccountController : IdentityControllerBase - { - private IPersistedGrantStore PersistedGrantStore { get; set; } - private ITokenGenerationService TokenGenerationService { get; set; } - private IUserClaimsPrincipalFactory PrincipalFactory { get; set; } - private IdentityServerOptions ISOptions { get; set; } - private IUserAccountService UserAccountService { get; set; } - public AccountController(UserManager _userManager, IEmailSender _emailSender, - SignInManager _signInManager, RoleManager _roleManager, IPersistedGrantStore store, - ITokenGenerationService tokenGenerationService, IMFAService _mfaService, IUserAccountService userAccountService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) - { - PersistedGrantStore = store; - TokenGenerationService = tokenGenerationService; - UserAccountService = userAccountService; - } - - [HttpGet("confirm")] - [AllowAnonymous] - [ResponseCache(NoStore = true)] - public async Task ConfirmToken(string userId, string code, string clientId, TokenType type) - { - var client = Clients.FindClientById(clientId); - if (client == null) return BadRequest("Invalid client_id."); - - var user = await UserManager.FindByIdAsync(userId); - if (!await UserService.IsUserValidAsync(UserManager, 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()); - - if (await UserManager.IsInRoleAsync(user, client.Id)) - { - await client.OnEmailConfirmed(userId); - } - - if (!await UserManager.GetTwoFactorEnabledAsync(user)) - { - await MFAService.EnableMFAAsync(user, MFAMethods.Email); - user = await UserManager.GetUserAsync(User); - } - - var redirectUrl = $"{client.EmailConfirmedRedirectURL}?userId={userId}"; - return RedirectPermanent(redirectUrl); - } - 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 = $"{client.AccountRecoveryRedirectURL}?userId={userId}&code={authorizationCode}"; - return RedirectPermanent(redirectUrl); - } - default: - return BadRequest("Invalid type."); - } - - } - - [HttpPost("verify")] - public async Task SendVerificationEmail([FromForm] string newEmail) - { - var client = Clients.FindClientById(User.FindFirstValue("client_id")); - if (client == null) return BadRequest("Invalid client_id."); - - var user = await UserManager.GetUserAsync(User); - if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); - - if (string.IsNullOrEmpty(newEmail)) - { - 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); - } - else - { - var code = await UserManager.GenerateChangeEmailTokenAsync(user, newEmail); - await EmailSender.SendChangeEmailConfirmationAsync(newEmail, code, client); - } - return Ok(); - } - - [HttpGet] - public async Task GetUserAccount() - { - var client = Clients.FindClientById(User.FindFirstValue("client_id")); - if (client == null) return BadRequest("Invalid client_id."); - var user = await UserManager.GetUserAsync(User); - return Ok(UserAccountService.GetUserAsync(client.Id, user.Id.ToString())); - } - - [HttpPost("recover")] - [AllowAnonymous] - public async Task ResetUserPassword([FromForm] ResetPasswordForm form) - { - var client = Clients.FindClientById(form.ClientId); - if (client == null) return BadRequest("Invalid client_id."); - - var user = await UserManager.FindByEmailAsync(form.Email); - if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok(); - - var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword"); - var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD, Request.Scheme); -#if DEBUG - return Ok(callbackUrl); -#else - await Slogger.Info("ResetUserPassword", user.Email, callbackUrl); - await EmailSender.SendPasswordResetEmailAsync(user.Email, callbackUrl, client); - return Ok(); -#endif - } - - [HttpPost("logout")] - public async Task Logout() - { - var client = Clients.FindClientById(User.FindFirstValue("client_id")); - if (client == null) return BadRequest("Invalid client_id."); - - var user = await UserManager.GetUserAsync(User); - if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); - - var subjectId = User.FindFirstValue("sub"); - var jti = User.FindFirstValue("jti"); - - var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter - { - ClientId = client.Id, - SubjectId = subjectId - }); - grants = grants.Where((grant) => grant.Data.Contains(jti)); - if (grants.Any()) - { - foreach (var grant in grants) - { - await PersistedGrantStore.RemoveAsync(grant.Key); - } - } - return Ok(); - } - - [HttpPost("token")] - [AllowAnonymous] - public async Task GetAccessTokenFromCode([FromForm] GetAccessTokenForm form) - { - if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId."); - var user = await UserManager.FindByIdAsync(form.UserId); - if (!await UserService.IsUserValidAsync(UserManager, 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, - scope = string.Join(' ', Config.ApiScopes.Select(s => s.Name)), - expires_in = 18000 - }); - } - - [HttpPatch] - public async Task UpdateAccount([FromForm] UpdateUserForm form) - { - var client = Clients.FindClientById(User.FindFirstValue("client_id")); - if (client == null) return BadRequest("Invalid client_id."); - - var user = await UserManager.GetUserAsync(User); - if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) - return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); - - switch (form.Type) - { - case "change_email": - { - var result = await UserManager.ChangeEmailAsync(user, form.NewEmail, form.VerificationCode); - if (result.Succeeded) - { - result = await UserManager.RemovePasswordAsync(user); - if (result.Succeeded) - { - result = await UserManager.AddPasswordAsync(user, form.Password); - if (result.Succeeded) - { - await UserManager.SetUserNameAsync(user, form.NewEmail); - await SendLogoutMessageAsync(user.Id.ToString(), "Email changed."); - return Ok(); - } - } - } - return BadRequest(result.Errors.ToErrors()); - } - case "change_password": - { - var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword); - if (result.Succeeded) - { - await SendLogoutMessageAsync(user.Id.ToString(), "Password changed."); - return Ok(); - } - return BadRequest(result.Errors.ToErrors()); - } - case "reset_password": - { - var result = await UserManager.RemovePasswordAsync(user); - if (result.Succeeded) - { - await MFAService.ResetMFAAsync(user); - result = await UserManager.AddPasswordAsync(user, form.NewPassword); - if (result.Succeeded) - { - await SendLogoutMessageAsync(user.Id.ToString(), "Password reset."); - return Ok(); - } - } - return BadRequest(result.Errors.ToErrors()); - } - case "change_marketing_consent": - { - var claimType = $"{client.Id}:marketing_consent"; - var claims = await UserManager.GetClaimsAsync(user); - var marketingConsentClaim = claims.FirstOrDefault((claim) => claim.Type == claimType); - if (marketingConsentClaim != null) await UserManager.RemoveClaimAsync(user, marketingConsentClaim); - if (!form.Enabled) - await UserManager.AddClaimAsync(user, new Claim(claimType, "false")); - return Ok(); - } - - } - return BadRequest("Invalid type."); - } - - [HttpPost("sessions/clear")] - public async Task ClearUserSessions([FromQuery] bool all, [FromForm] string refresh_token) - { - var client = Clients.FindClientById(User.FindFirstValue("client_id")); - if (client == null) return BadRequest("Invalid client_id."); - - var user = await UserManager.GetUserAsync(User); - if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id}'."); - - var jti = User.FindFirstValue("jti"); - - var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter - { - ClientId = client.Id, - SubjectId = user.Id.ToString() - }); - var refreshTokenKey = GetHashedKey(refresh_token, PersistedGrantTypes.RefreshToken); - var removedKeys = new List(); - foreach (var grant in grants) - { - if (!all && (grant.Data.Contains(jti) || grant.Key == refreshTokenKey)) continue; - await PersistedGrantStore.RemoveAsync(grant.Key); - removedKeys.Add(grant.Key); - } - - await WampServers.NotesnookServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys)); - await WampServers.MessengerServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys)); - await WampServers.SubscriptionServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys)); - await SendLogoutMessageAsync(user.Id.ToString(), "Session revoked."); - return Ok(); - } - - private static string GetHashedKey(string value, string grantType) - { - return (value + ":" + grantType).Sha256(); - } - - private async Task SendLogoutMessageAsync(string userId, string reason) - { - await SendMessageAsync(userId, new Message - { - Type = "logout", - Data = JsonSerializer.Serialize(new { reason }) - }); - } - - private async Task SendMessageAsync(string userId, Message message) - { - await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage - { - UserId = userId, - OriginTokenId = User.FindFirstValue("jti"), - Message = message - }); - } - } +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; +using AspNetCore.Identity.Mongo.Model; +using IdentityServer4; +using IdentityServer4.Configuration; +using IdentityServer4.Models; +using IdentityServer4.Stores; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Streetwriters.Common; +using Streetwriters.Common.Enums; +using Streetwriters.Common.Interfaces; +using Streetwriters.Common.Messages; +using Streetwriters.Common.Models; +using Streetwriters.Identity.Enums; +using Streetwriters.Identity.Interfaces; +using Streetwriters.Identity.Models; +using Streetwriters.Identity.Services; +using static IdentityServer4.IdentityServerConstants; + +namespace Streetwriters.Identity.Controllers +{ + [ApiController] + [DisplayName("Account")] + [Route("account")] + [Authorize(LocalApi.PolicyName)] + public class AccountController : IdentityControllerBase + { + private IPersistedGrantStore PersistedGrantStore { get; set; } + private ITokenGenerationService TokenGenerationService { get; set; } + private IUserClaimsPrincipalFactory PrincipalFactory { get; set; } + private IdentityServerOptions ISOptions { get; set; } + private IUserAccountService UserAccountService { get; set; } + public AccountController(UserManager _userManager, IEmailSender _emailSender, + SignInManager _signInManager, RoleManager _roleManager, IPersistedGrantStore store, + ITokenGenerationService tokenGenerationService, IMFAService _mfaService, IUserAccountService userAccountService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) + { + PersistedGrantStore = store; + TokenGenerationService = tokenGenerationService; + UserAccountService = userAccountService; + } + + [HttpGet("confirm")] + [AllowAnonymous] + [ResponseCache(NoStore = true)] + public async Task ConfirmToken(string userId, string code, string clientId, TokenType type) + { + var client = Clients.FindClientById(clientId); + if (client == null) return BadRequest("Invalid client_id."); + + var user = await UserManager.FindByIdAsync(userId); + if (!await UserService.IsUserValidAsync(UserManager, 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()); + + if (await UserManager.IsInRoleAsync(user, client.Id)) + { + await client.OnEmailConfirmed(userId); + } + + if (!await UserManager.GetTwoFactorEnabledAsync(user)) + { + await MFAService.EnableMFAAsync(user, MFAMethods.Email); + user = await UserManager.GetUserAsync(User); + } + + var redirectUrl = $"{client.EmailConfirmedRedirectURL}?userId={userId}"; + return RedirectPermanent(redirectUrl); + } + 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 = $"{client.AccountRecoveryRedirectURL}?userId={userId}&code={authorizationCode}"; + return RedirectPermanent(redirectUrl); + } + default: + return BadRequest("Invalid type."); + } + + } + + [HttpPost("verify")] + public async Task SendVerificationEmail([FromForm] string newEmail) + { + var client = Clients.FindClientById(User.FindFirstValue("client_id")); + if (client == null) return BadRequest("Invalid client_id."); + + var user = await UserManager.GetUserAsync(User); + if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); + + if (string.IsNullOrEmpty(newEmail)) + { + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL); + await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client); + } + else + { + var code = await UserManager.GenerateChangeEmailTokenAsync(user, newEmail); + await EmailSender.SendChangeEmailConfirmationAsync(newEmail, code, client); + } + return Ok(); + } + + [HttpGet] + public async Task GetUserAccount() + { + var client = Clients.FindClientById(User.FindFirstValue("client_id")); + if (client == null) return BadRequest("Invalid client_id."); + var user = await UserManager.GetUserAsync(User); + return Ok(UserAccountService.GetUserAsync(client.Id, user.Id.ToString())); + } + + [HttpPost("recover")] + [AllowAnonymous] + public async Task ResetUserPassword([FromForm] ResetPasswordForm form) + { + var client = Clients.FindClientById(form.ClientId); + if (client == null) return BadRequest("Invalid client_id."); + + var user = await UserManager.FindByEmailAsync(form.Email); + if (!await UserService.IsUserValidAsync(UserManager, 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); +#if DEBUG + return Ok(callbackUrl); +#else + await Slogger.Info("ResetUserPassword", user.Email, callbackUrl); + await EmailSender.SendPasswordResetEmailAsync(user.Email, callbackUrl, client); + return Ok(); +#endif + } + + [HttpPost("logout")] + public async Task Logout() + { + var client = Clients.FindClientById(User.FindFirstValue("client_id")); + if (client == null) return BadRequest("Invalid client_id."); + + var user = await UserManager.GetUserAsync(User); + if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); + + var subjectId = User.FindFirstValue("sub"); + var jti = User.FindFirstValue("jti"); + + var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter + { + ClientId = client.Id, + SubjectId = subjectId + }); + grants = grants.Where((grant) => grant.Data.Contains(jti)); + if (grants.Any()) + { + foreach (var grant in grants) + { + await PersistedGrantStore.RemoveAsync(grant.Key); + } + } + return Ok(); + } + + [HttpPost("token")] + [AllowAnonymous] + public async Task GetAccessTokenFromCode([FromForm] GetAccessTokenForm form) + { + if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId."); + var user = await UserManager.FindByIdAsync(form.UserId); + if (!await UserService.IsUserValidAsync(UserManager, 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, + scope = string.Join(' ', Config.ApiScopes.Select(s => s.Name)), + expires_in = 18000 + }); + } + + [HttpPatch] + public async Task UpdateAccount([FromForm] UpdateUserForm form) + { + var client = Clients.FindClientById(User.FindFirstValue("client_id")); + if (client == null) return BadRequest("Invalid client_id."); + + var user = await UserManager.GetUserAsync(User); + if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) + return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'."); + + switch (form.Type) + { + case "change_email": + { + var result = await UserManager.ChangeEmailAsync(user, form.NewEmail, form.VerificationCode); + if (result.Succeeded) + { + result = await UserManager.RemovePasswordAsync(user); + if (result.Succeeded) + { + result = await UserManager.AddPasswordAsync(user, form.Password); + if (result.Succeeded) + { + await UserManager.SetUserNameAsync(user, form.NewEmail); + await SendLogoutMessageAsync(user.Id.ToString(), "Email changed."); + return Ok(); + } + } + } + return BadRequest(result.Errors.ToErrors()); + } + case "change_password": + { + var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword); + if (result.Succeeded) + { + await SendLogoutMessageAsync(user.Id.ToString(), "Password changed."); + return Ok(); + } + return BadRequest(result.Errors.ToErrors()); + } + case "reset_password": + { + var result = await UserManager.RemovePasswordAsync(user); + if (result.Succeeded) + { + await MFAService.ResetMFAAsync(user); + result = await UserManager.AddPasswordAsync(user, form.NewPassword); + if (result.Succeeded) + { + await SendLogoutMessageAsync(user.Id.ToString(), "Password reset."); + return Ok(); + } + } + return BadRequest(result.Errors.ToErrors()); + } + case "change_marketing_consent": + { + var claimType = $"{client.Id}:marketing_consent"; + var claims = await UserManager.GetClaimsAsync(user); + var marketingConsentClaim = claims.FirstOrDefault((claim) => claim.Type == claimType); + if (marketingConsentClaim != null) await UserManager.RemoveClaimAsync(user, marketingConsentClaim); + if (!form.Enabled) + await UserManager.AddClaimAsync(user, new Claim(claimType, "false")); + return Ok(); + } + + } + return BadRequest("Invalid type."); + } + + [HttpPost("sessions/clear")] + public async Task ClearUserSessions([FromQuery] bool all, [FromForm] string refresh_token) + { + var client = Clients.FindClientById(User.FindFirstValue("client_id")); + if (client == null) return BadRequest("Invalid client_id."); + + var user = await UserManager.GetUserAsync(User); + if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id}'."); + + var jti = User.FindFirstValue("jti"); + + var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter + { + ClientId = client.Id, + SubjectId = user.Id.ToString() + }); + var refreshTokenKey = GetHashedKey(refresh_token, PersistedGrantTypes.RefreshToken); + var removedKeys = new List(); + foreach (var grant in grants) + { + if (!all && (grant.Data.Contains(jti) || grant.Key == refreshTokenKey)) continue; + await PersistedGrantStore.RemoveAsync(grant.Key); + removedKeys.Add(grant.Key); + } + + await WampServers.NotesnookServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys)); + await WampServers.MessengerServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys)); + await WampServers.SubscriptionServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys)); + await SendLogoutMessageAsync(user.Id.ToString(), "Session revoked."); + return Ok(); + } + + private static string GetHashedKey(string value, string grantType) + { + return (value + ":" + grantType).Sha256(); + } + + private async Task SendLogoutMessageAsync(string userId, string reason) + { + await SendMessageAsync(userId, new Message + { + Type = "logout", + Data = JsonSerializer.Serialize(new { reason }) + }); + } + + private async Task SendMessageAsync(string userId, Message message) + { + await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage + { + UserId = userId, + OriginTokenId = User.FindFirstValue("jti"), + Message = message + }); + } + } } \ No newline at end of file diff --git a/Streetwriters.Identity/Controllers/SignupController.cs b/Streetwriters.Identity/Controllers/SignupController.cs index 2ea1997..c28cb4e 100644 --- a/Streetwriters.Identity/Controllers/SignupController.cs +++ b/Streetwriters.Identity/Controllers/SignupController.cs @@ -1,139 +1,139 @@ -/* -This file is part of the Notesnook Sync Server project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the Affero GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -Affero GNU General Public License for more details. - -You should have received a copy of the Affero GNU General Public License -along with this program. If not, see . -*/ - -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using AspNetCore.Identity.Mongo.Model; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Streetwriters.Common; -using Streetwriters.Common.Enums; -using Streetwriters.Common.Models; -using Streetwriters.Identity.Enums; -using Streetwriters.Identity.Interfaces; -using Streetwriters.Identity.Models; -using Streetwriters.Identity.Services; - -namespace Streetwriters.Identity.Controllers -{ - [ApiController] - [Route("signup")] - public class SignupController : IdentityControllerBase - { - public SignupController(UserManager _userManager, IEmailSender _emailSender, - SignInManager _signInManager, RoleManager _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) - { } - - private async Task AddClientRoleAsync(string clientId) - { - if (await RoleManager.FindByNameAsync(clientId) == null) - await RoleManager.CreateAsync(new MongoRole(clientId)); - } - - [HttpPost] - [AllowAnonymous] - public async Task Signup([FromForm] SignupForm form) - { - if (Constants.DISABLE_ACCOUNT_CREATION) - return BadRequest(new string[] { "Creating new accounts is not allowed." }); - try - { - var client = Clients.FindClientById(form.ClientId); - if (client == null) return BadRequest(new string[] { "Invalid client id." }); - - await AddClientRoleAsync(client.Id); - - // email addresses must be case-insensitive - form.Email = form.Email.ToLowerInvariant(); - form.Username = form.Username?.ToLowerInvariant(); - - if (!await EmailAddressValidator.IsEmailAddressValidAsync(form.Email)) return BadRequest(new string[] { "Invalid email address." }); - - var result = await UserManager.CreateAsync(new User - { - Email = form.Email, - EmailConfirmed = Constants.IS_SELF_HOSTED, - 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 MFAService.DisableMFAAsync(user); - await UserManager.AddToRoleAsync(user, client.Id); - } - else - { - return BadRequest(new string[] { "Invalid email address.." }); - } - - return Ok(new - { - userId = user.Id.ToString() - }); - } - - if (result.Succeeded) - { - var user = await UserManager.FindByEmailAsync(form.Email); - await UserManager.AddToRoleAsync(user, client.Id); - if (Constants.IS_SELF_HOSTED) - { - await UserManager.AddClaimAsync(user, UserService.SubscriptionTypeToClaim(client.Id, Common.Enums.SubscriptionType.PREMIUM)); - } - else - { - await UserManager.AddClaimAsync(user, new Claim("platform", PlatformFromUserAgent(base.HttpContext.Request.Headers.UserAgent))); - 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()); - } - catch (System.Exception ex) - { - await Slogger.Error("Signup", ex.ToString()); - return BadRequest("Failed to create an account."); - } - } - - string PlatformFromUserAgent(string userAgent) - { - return userAgent.Contains("okhttp/") ? "android" : userAgent.Contains("Darwin/") || userAgent.Contains("CFNetwork/") ? "ios" : "web"; - } - } -} +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using AspNetCore.Identity.Mongo.Model; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Streetwriters.Common; +using Streetwriters.Common.Enums; +using Streetwriters.Common.Models; +using Streetwriters.Identity.Enums; +using Streetwriters.Identity.Interfaces; +using Streetwriters.Identity.Models; +using Streetwriters.Identity.Services; + +namespace Streetwriters.Identity.Controllers +{ + [ApiController] + [Route("signup")] + public class SignupController : IdentityControllerBase + { + public SignupController(UserManager _userManager, IEmailSender _emailSender, + SignInManager _signInManager, RoleManager _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) + { } + + private async Task AddClientRoleAsync(string clientId) + { + if (await RoleManager.FindByNameAsync(clientId) == null) + await RoleManager.CreateAsync(new MongoRole(clientId)); + } + + [HttpPost] + [AllowAnonymous] + public async Task Signup([FromForm] SignupForm form) + { + if (Constants.DISABLE_ACCOUNT_CREATION) + return BadRequest(new string[] { "Creating new accounts is not allowed." }); + try + { + var client = Clients.FindClientById(form.ClientId); + if (client == null) return BadRequest(new string[] { "Invalid client id." }); + + await AddClientRoleAsync(client.Id); + + // email addresses must be case-insensitive + form.Email = form.Email.ToLowerInvariant(); + form.Username = form.Username?.ToLowerInvariant(); + + if (!await EmailAddressValidator.IsEmailAddressValidAsync(form.Email)) return BadRequest(new string[] { "Invalid email address." }); + + var result = await UserManager.CreateAsync(new User + { + Email = form.Email, + EmailConfirmed = Constants.IS_SELF_HOSTED, + 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 MFAService.DisableMFAAsync(user); + await UserManager.AddToRoleAsync(user, client.Id); + } + else + { + return BadRequest(new string[] { "Invalid email address.." }); + } + + return Ok(new + { + userId = user.Id.ToString() + }); + } + + if (result.Succeeded) + { + var user = await UserManager.FindByEmailAsync(form.Email); + await UserManager.AddToRoleAsync(user, client.Id); + if (Constants.IS_SELF_HOSTED) + { + await UserManager.AddClaimAsync(user, UserService.SubscriptionTypeToClaim(client.Id, Common.Enums.SubscriptionType.PREMIUM)); + } + else + { + await UserManager.AddClaimAsync(user, new Claim("platform", PlatformFromUserAgent(base.HttpContext.Request.Headers.UserAgent))); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL); + await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client); + } + return Ok(new + { + userId = user.Id.ToString() + }); + } + + return BadRequest(result.Errors.ToErrors()); + } + catch (System.Exception ex) + { + await Slogger.Error("Signup", ex.ToString()); + return BadRequest("Failed to create an account."); + } + } + + string PlatformFromUserAgent(string userAgent) + { + return userAgent.Contains("okhttp/") ? "android" : userAgent.Contains("Darwin/") || userAgent.Contains("CFNetwork/") ? "ios" : "web"; + } + } +} diff --git a/Streetwriters.Identity/Extensions/UrlExtensions.cs b/Streetwriters.Identity/Extensions/UrlExtensions.cs index 9f51ec2..3fb085c 100644 --- a/Streetwriters.Identity/Extensions/UrlExtensions.cs +++ b/Streetwriters.Identity/Extensions/UrlExtensions.cs @@ -1,48 +1,49 @@ -/* -This file is part of the Notesnook Sync Server project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the Affero GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -Affero GNU General Public License for more details. - -You should have received a copy of the Affero GNU General Public License -along with this program. If not, see . -*/ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Streetwriters.Common; -using Streetwriters.Identity.Controllers; -using Streetwriters.Identity.Enums; - -namespace Microsoft.AspNetCore.Mvc -{ - public static class UrlHelperExtensions - { - public static string TokenLink(this IUrlHelper urlHelper, string userId, string code, string clientId, TokenType type, string scheme) - { - - return urlHelper.ActionLink( -#if DEBUG - host: $"{Servers.IdentityServer.Hostname}:{Servers.IdentityServer.Port}", -#else - host: Servers.IdentityServer.Domain, -#endif - action: nameof(AccountController.ConfirmToken), - controller: "Account", - values: new { userId, code, clientId, type }, - protocol: scheme); - - } - } +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Streetwriters.Common; +using Streetwriters.Identity.Controllers; +using Streetwriters.Identity.Enums; + +namespace Microsoft.AspNetCore.Mvc +{ + public static class UrlHelperExtensions + { + public static string TokenLink(this IUrlHelper urlHelper, string userId, string code, string clientId, TokenType type) + { + + return urlHelper.ActionLink( +#if DEBUG + host: $"{Servers.IdentityServer.Hostname}:{Servers.IdentityServer.Port}", + protocol: "http", +#else + host: Servers.IdentityServer.PublicURL.Host, + protocol: Servers.IdentityServer.PublicURL.Scheme, +#endif + action: nameof(AccountController.ConfirmToken), + controller: "Account", + values: new { userId, code, clientId, type }); + + } + } } \ No newline at end of file