mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-03-21 17:43:29 +00:00
identity: simplify user sign up
This commit is contained in:
committed by
Abdullah Atta
parent
d5790d8785
commit
9ae5db378d
@@ -33,6 +33,7 @@ using Streetwriters.Common;
|
||||
using Streetwriters.Common.Accessors;
|
||||
using Streetwriters.Common.Extensions;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Notesnook.API.Controllers
|
||||
{
|
||||
@@ -43,12 +44,11 @@ namespace Notesnook.API.Controllers
|
||||
{
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Signup()
|
||||
public async Task<IActionResult> Signup([FromForm] SignupForm form)
|
||||
{
|
||||
try
|
||||
{
|
||||
await UserService.CreateUserAsync();
|
||||
return Ok();
|
||||
return Ok(await UserService.CreateUserAsync(form));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -20,12 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
using System.Threading.Tasks;
|
||||
using Notesnook.API.Models;
|
||||
using Notesnook.API.Models.Responses;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Notesnook.API.Interfaces
|
||||
{
|
||||
public interface IUserService
|
||||
{
|
||||
Task CreateUserAsync();
|
||||
Task<SignupResponse> CreateUserAsync(SignupForm form);
|
||||
Task DeleteUserAsync(string userId);
|
||||
Task DeleteUserAsync(string userId, string? jti, string password);
|
||||
Task<bool> ResetUserAsync(string userId, bool removeAttachments);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Notesnook.API.Models.Responses
|
||||
{
|
||||
public class SignupResponse : Response
|
||||
{
|
||||
[JsonPropertyName("userId")]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("errors")]
|
||||
public string[]? Errors { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -51,15 +51,16 @@ namespace Notesnook.API.Services
|
||||
private IS3Service S3Service { get; set; } = s3Service;
|
||||
private readonly IUnitOfWork unit = unitOfWork;
|
||||
|
||||
public async Task CreateUserAsync()
|
||||
public async Task<SignupResponse> CreateUserAsync(SignupForm form)
|
||||
{
|
||||
SignupResponse response = await httpClient.ForwardAsync<SignupResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer}/signup", HttpMethod.Post);
|
||||
if (!response.Success || (response.Errors != null && response.Errors.Length > 0) || response.UserId == null)
|
||||
SignupResponse response = await serviceAccessor.UserAccountService.CreateUserAsync(form.ClientId, form.Email, form.Password, HttpContextAccessor.HttpContext?.Request.Headers["User-Agent"].ToString());
|
||||
|
||||
if ((response.Errors != null && response.Errors.Length > 0) || response.UserId == null)
|
||||
{
|
||||
logger.LogError("Failed to sign up user: {Response}", JsonSerializer.Serialize(response));
|
||||
if (response.Errors != null && response.Errors.Length > 0)
|
||||
throw new Exception(string.Join(" ", response.Errors));
|
||||
else throw new Exception("Could not create a new account. Error code: " + response.StatusCode);
|
||||
else throw new Exception("Could not create a new account.");
|
||||
}
|
||||
|
||||
await Repositories.UsersSettings.InsertAsync(new UserSettings
|
||||
@@ -84,7 +85,7 @@ namespace Notesnook.API.Services
|
||||
});
|
||||
}
|
||||
|
||||
logger.LogInformation("New user created: {Response}", JsonSerializer.Serialize(response));
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<UserResponse> GetUserAsync(string userId)
|
||||
|
||||
@@ -16,5 +16,7 @@ namespace Streetwriters.Common.Interfaces
|
||||
Task<bool> ResetPasswordAsync(string userId, string newPassword);
|
||||
[WampProcedure("co.streetwriters.identity.users.clear_sessions")]
|
||||
Task<bool> ClearSessionsAsync(string userId, string clientId, bool all, string jti, string? refreshToken);
|
||||
[WampProcedure("co.streetwriters.identity.users.create_user")]
|
||||
Task<SignupResponse> CreateUserAsync(string clientId, string email, string password, string? userAgent = null);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Streetwriters.Identity.Models
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
public class SignupForm
|
||||
{
|
||||
30
Streetwriters.Common/Models/SignupResponse.cs
Normal file
30
Streetwriters.Common/Models/SignupResponse.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
public class SignupResponse
|
||||
{
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
[JsonPropertyName("expires_in")]
|
||||
public int AccessTokenLifetime { get; set; }
|
||||
[JsonPropertyName("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
[JsonPropertyName("scope")]
|
||||
public string Scope { get; set; }
|
||||
[JsonPropertyName("user_id")]
|
||||
public string UserId { get; set; }
|
||||
public string[]? Errors { get; set; }
|
||||
|
||||
public static SignupResponse Error(IEnumerable<string> errors)
|
||||
{
|
||||
return new SignupResponse
|
||||
{
|
||||
Errors = [.. errors]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Identity.Enums;
|
||||
using Streetwriters.Identity.Extensions;
|
||||
using Streetwriters.Identity.Interfaces;
|
||||
using Streetwriters.Identity.Models;
|
||||
using Streetwriters.Identity.Services;
|
||||
@@ -125,7 +126,7 @@ namespace Streetwriters.Identity.Controllers
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user.Email);
|
||||
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL);
|
||||
var callbackUrl = UrlExtensions.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL);
|
||||
await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
|
||||
}
|
||||
else
|
||||
@@ -158,7 +159,7 @@ namespace Streetwriters.Identity.Controllers
|
||||
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);
|
||||
var callbackUrl = UrlExtensions.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD);
|
||||
#if (DEBUG || STAGING)
|
||||
return Ok(callbackUrl);
|
||||
#else
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
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 <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 Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
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
|
||||
{
|
||||
private readonly ILogger<SignupController> logger;
|
||||
private readonly EmailAddressValidator emailValidator;
|
||||
|
||||
public SignupController(UserManager<User> _userManager, ITemplatedEmailSender _emailSender,
|
||||
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IMFAService _mfaService,
|
||||
ILogger<SignupController> logger, EmailAddressValidator emailValidator) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.emailValidator = emailValidator;
|
||||
}
|
||||
|
||||
private async Task AddClientRoleAsync(string clientId)
|
||||
{
|
||||
if (await RoleManager.FindByNameAsync(clientId) == null)
|
||||
await RoleManager.CreateAsync(new MongoRole(clientId));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[EnableRateLimiting("strict")]
|
||||
public async Task<IActionResult> Signup([FromForm] SignupForm form)
|
||||
{
|
||||
if (Constants.DISABLE_SIGNUPS)
|
||||
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 emailValidator.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 (user == null) return BadRequest(new string[] { "User not found." });
|
||||
|
||||
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);
|
||||
if (user == null) return BadRequest(new string[] { "User not found after creation." });
|
||||
|
||||
await UserManager.AddToRoleAsync(user, client.Id);
|
||||
if (Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
await UserManager.AddClaimAsync(user, new Claim(UserService.GetClaimKey(client.Id), "believer"));
|
||||
}
|
||||
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);
|
||||
if (!string.IsNullOrEmpty(user.Email) && callbackUrl != null)
|
||||
{
|
||||
await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
|
||||
}
|
||||
}
|
||||
return Ok(new
|
||||
{
|
||||
userId = user.Id.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create user account for email: {Email}", form.Email);
|
||||
return BadRequest("Failed to create an account.");
|
||||
}
|
||||
}
|
||||
|
||||
static string PlatformFromUserAgent(string? userAgent)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userAgent)) return "unknown";
|
||||
return userAgent.Contains("okhttp/") ? "android" : userAgent.Contains("Darwin/") || userAgent.Contains("CFNetwork/") ? "ios" : "web";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,25 +25,24 @@ using Streetwriters.Common;
|
||||
using Streetwriters.Identity.Controllers;
|
||||
using Streetwriters.Identity.Enums;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
namespace Streetwriters.Identity.Extensions
|
||||
{
|
||||
public static class UrlHelperExtensions
|
||||
public static class UrlExtensions
|
||||
{
|
||||
public static string? TokenLink(this IUrlHelper urlHelper, string userId, string code, string clientId, TokenType type)
|
||||
public static string? TokenLink(string userId, string code, string clientId, TokenType type)
|
||||
{
|
||||
|
||||
return urlHelper.ActionLink(
|
||||
var url = new UriBuilder();
|
||||
#if (DEBUG || STAGING)
|
||||
host: $"{Servers.IdentityServer.Hostname}:{Servers.IdentityServer.Port}",
|
||||
protocol: "http",
|
||||
url.Host = $"{Servers.IdentityServer.Hostname}";
|
||||
url.Port = Servers.IdentityServer.Port;
|
||||
url.Scheme = "http";
|
||||
#else
|
||||
host: Servers.IdentityServer.PublicURL.Host,
|
||||
protocol: Servers.IdentityServer.PublicURL.Scheme,
|
||||
url.Host = Servers.IdentityServer.PublicURL.Host;
|
||||
url.Scheme = Servers.IdentityServer.PublicURL.Scheme;
|
||||
#endif
|
||||
action: nameof(AccountController.ConfirmToken),
|
||||
controller: "Account",
|
||||
values: new { userId, code, clientId, type });
|
||||
|
||||
url.Path = "account/confirm";
|
||||
url.Query = $"userId={Uri.EscapeDataString(userId)}&code={Uri.EscapeDataString(code)}&clientId={Uri.EscapeDataString(clientId)}&type={Uri.EscapeDataString(type.ToString())}";
|
||||
return url.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4.ResponseHandling;
|
||||
using IdentityServer4.Validation;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
@@ -26,8 +27,9 @@ 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);
|
||||
Task<string> CreateAccessTokenAsync(User user, string clientId, int lifetime = 1800);
|
||||
Task<string> CreateAccessTokenFromValidatedRequestAsync(ValidatedTokenRequest validatedRequest, User user, string[] scopes, int lifetime = 1200);
|
||||
Task<ClaimsPrincipal> TransformTokenRequestAsync(ValidatedTokenRequest request, User user, string grantType, string[] scopes, int lifetime = 1200);
|
||||
Task<TokenResponse?> CreateUserTokensAsync(User user, string clientId, int lifetime = 1800);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ using IdentityModel;
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Configuration;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.ResponseHandling;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using IdentityServer4.Validation;
|
||||
@@ -41,12 +42,14 @@ namespace Streetwriters.Identity.Helpers
|
||||
private IdentityServerOptions ISOptions { get; set; }
|
||||
private IdentityServerTools Tools { get; set; }
|
||||
private IResourceStore ResourceStore { get; set; }
|
||||
private readonly IRefreshTokenService refreshTokenService;
|
||||
public TokenGenerationService(ITokenService tokenService,
|
||||
IUserClaimsPrincipalFactory<User> principalFactory,
|
||||
IdentityServerOptions identityServerOptions,
|
||||
IPersistedGrantStore persistedGrantStore,
|
||||
IdentityServerTools tools,
|
||||
IResourceStore resourceStore)
|
||||
IResourceStore resourceStore,
|
||||
IRefreshTokenService _refreshTokenService)
|
||||
{
|
||||
TokenService = tokenService;
|
||||
PrincipalFactory = principalFactory;
|
||||
@@ -54,16 +57,25 @@ namespace Streetwriters.Identity.Helpers
|
||||
PersistedGrantStore = persistedGrantStore;
|
||||
Tools = tools;
|
||||
ResourceStore = resourceStore;
|
||||
refreshTokenService = _refreshTokenService;
|
||||
}
|
||||
|
||||
public async Task<string> CreateAccessTokenAsync(User user, string clientId)
|
||||
public async Task<string> CreateAccessTokenAsync(User user, string clientId, int lifetime = 1800)
|
||||
{
|
||||
var client = Config.Clients.FirstOrDefault((c) => c.ClientId == clientId);
|
||||
if (client == null)
|
||||
{
|
||||
throw new System.ArgumentException($"Client with ID '{clientId}' not found", nameof(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 IdentityUser = new IdentityServerUser(user.Id.ToString())
|
||||
{
|
||||
AdditionalClaims = [.. IdentityPricipal.Claims],
|
||||
DisplayName = user.UserName,
|
||||
AuthenticationTime = System.DateTime.UtcNow,
|
||||
IdentityProvider = IdentityServerConstants.LocalIdentityProvider
|
||||
};
|
||||
var Request = new TokenCreationRequest
|
||||
{
|
||||
Subject = IdentityUser.CreatePrincipal(),
|
||||
@@ -71,16 +83,61 @@ namespace Streetwriters.Identity.Helpers
|
||||
ValidatedRequest = new ValidatedRequest()
|
||||
};
|
||||
Request.ValidatedRequest.Subject = Request.Subject;
|
||||
Request.ValidatedRequest.SetClient(Config.Clients.FirstOrDefault((c) => c.ClientId == clientId));
|
||||
Request.ValidatedRequest.SetClient(client);
|
||||
Request.ValidatedRequest.AccessTokenType = AccessTokenType.Reference;
|
||||
Request.ValidatedRequest.AccessTokenLifetime = 18000;
|
||||
Request.ValidatedResources = new ResourceValidationResult(new Resources(Config.IdentityResources, Config.ApiResources, Config.ApiScopes));
|
||||
Request.ValidatedRequest.AccessTokenLifetime = lifetime;
|
||||
var requestedScopes = client.AllowedScopes.Select(s => new ParsedScopeValue(s));
|
||||
Request.ValidatedResources = await ResourceStore.CreateResourceValidationResult(new ParsedScopesResult
|
||||
{
|
||||
ParsedScopes = [.. requestedScopes]
|
||||
});
|
||||
Request.ValidatedRequest.Options = ISOptions;
|
||||
Request.ValidatedRequest.ClientClaims = IdentityUser.AdditionalClaims;
|
||||
var accessToken = await TokenService.CreateAccessTokenAsync(Request);
|
||||
return await TokenService.CreateSecurityTokenAsync(accessToken);
|
||||
}
|
||||
|
||||
public async Task<TokenResponse?> CreateUserTokensAsync(User user, string clientId, int lifetime = 1800)
|
||||
{
|
||||
var client = Config.Clients.FirstOrDefault((c) => c.ClientId == clientId);
|
||||
var principal = await PrincipalFactory.CreateAsync(user);
|
||||
if (client == null || principal == null) return null;
|
||||
var IdentityUser = new IdentityServerUser(user.Id.ToString())
|
||||
{
|
||||
AdditionalClaims = [.. principal.Claims],
|
||||
DisplayName = user.UserName,
|
||||
AuthenticationTime = System.DateTime.UtcNow,
|
||||
IdentityProvider = IdentityServerConstants.LocalIdentityProvider
|
||||
};
|
||||
var Request = new TokenCreationRequest
|
||||
{
|
||||
Subject = IdentityUser.CreatePrincipal(),
|
||||
IncludeAllIdentityClaims = true,
|
||||
ValidatedRequest = new ValidatedRequest()
|
||||
};
|
||||
Request.ValidatedRequest.Subject = Request.Subject;
|
||||
Request.ValidatedRequest.SetClient(client);
|
||||
Request.ValidatedRequest.AccessTokenType = AccessTokenType.Reference;
|
||||
Request.ValidatedRequest.AccessTokenLifetime = lifetime;
|
||||
var requestedScopes = client.AllowedScopes.Select(s => new ParsedScopeValue(s));
|
||||
Request.ValidatedResources = await ResourceStore.CreateResourceValidationResult(new ParsedScopesResult
|
||||
{
|
||||
ParsedScopes = [.. requestedScopes]
|
||||
});
|
||||
Request.ValidatedRequest.Options = ISOptions;
|
||||
Request.ValidatedRequest.ClientClaims = IdentityUser.AdditionalClaims;
|
||||
var accessToken = await TokenService.CreateAccessTokenAsync(Request);
|
||||
var refreshToken = await refreshTokenService.CreateRefreshTokenAsync(principal, accessToken, client);
|
||||
|
||||
return new TokenResponse
|
||||
{
|
||||
AccessToken = await TokenService.CreateSecurityTokenAsync(accessToken),
|
||||
AccessTokenLifetime = lifetime,
|
||||
RefreshToken = refreshToken,
|
||||
Scope = string.Join(" ", accessToken.Scopes)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ClaimsPrincipal> TransformTokenRequestAsync(ValidatedTokenRequest request, User user, string grantType, string[] scopes, int lifetime = 20 * 60)
|
||||
{
|
||||
var principal = await PrincipalFactory.CreateAsync(user);
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using AspNetCore.Identity.Mongo.Model;
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Common.Services;
|
||||
using Streetwriters.Identity.Enums;
|
||||
using Streetwriters.Identity.Extensions;
|
||||
using Streetwriters.Identity.Interfaces;
|
||||
using Streetwriters.Identity.Models;
|
||||
|
||||
namespace Streetwriters.Identity.Services
|
||||
{
|
||||
public class UserAccountService(UserManager<User> userManager, IMFAService mfaService, IPersistedGrantStore persistedGrantStore) : IUserAccountService
|
||||
public class UserAccountService(UserManager<User> userManager, IMFAService mfaService, IPersistedGrantStore persistedGrantStore, RoleManager<MongoRole> roleManager, EmailAddressValidator emailValidator, ITemplatedEmailSender emailSender, ITokenGenerationService tokenGenerationService, ILogger<UserAccountService> logger) : IUserAccountService
|
||||
{
|
||||
public async Task<UserModel?> GetUserAsync(string clientId, string userId)
|
||||
{
|
||||
@@ -109,6 +116,89 @@ namespace Streetwriters.Identity.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<SignupResponse> CreateUserAsync(string clientId, string email, string password, string? userAgent = null)
|
||||
{
|
||||
if (Constants.DISABLE_SIGNUPS)
|
||||
return new SignupResponse
|
||||
{
|
||||
Errors = ["Creating new accounts is not allowed."]
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var client = Clients.FindClientById(clientId);
|
||||
if (client == null) return new SignupResponse
|
||||
{
|
||||
Errors = ["Invalid client id."]
|
||||
};
|
||||
|
||||
if (await roleManager.FindByNameAsync(clientId) == null)
|
||||
await roleManager.CreateAsync(new MongoRole(clientId));
|
||||
|
||||
// email addresses must be case-insensitive
|
||||
email = email.ToLowerInvariant();
|
||||
|
||||
if (!await emailValidator.IsEmailAddressValidAsync(email))
|
||||
return new SignupResponse
|
||||
{
|
||||
Errors = ["Invalid email address."]
|
||||
};
|
||||
|
||||
var result = await userManager.CreateAsync(new User
|
||||
{
|
||||
Email = email,
|
||||
EmailConfirmed = Constants.IS_SELF_HOSTED,
|
||||
UserName = email,
|
||||
}, password);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user == null) return SignupResponse.Error(["User not found after creation."]);
|
||||
|
||||
await userManager.AddToRoleAsync(user, client.Id);
|
||||
if (Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
await userManager.AddClaimAsync(user, new Claim(UserService.GetClaimKey(client.Id), "believer"));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (userAgent != null) await userManager.AddClaimAsync(user, new Claim("platform", PlatformFromUserAgent(userAgent)));
|
||||
var code = await userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = UrlExtensions.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL);
|
||||
if (!string.IsNullOrEmpty(user.Email) && callbackUrl != null)
|
||||
{
|
||||
await emailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
|
||||
}
|
||||
}
|
||||
|
||||
var response = await tokenGenerationService.CreateUserTokensAsync(user, client.Id, 3600);
|
||||
if (response == null) return SignupResponse.Error(["Failed to generate access token."]);
|
||||
|
||||
return new SignupResponse
|
||||
{
|
||||
AccessToken = response.AccessToken,
|
||||
AccessTokenLifetime = response.AccessTokenLifetime,
|
||||
RefreshToken = response.RefreshToken,
|
||||
Scope = response.Scope,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
return SignupResponse.Error(result.Errors.ToErrors());
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create user account for email: {Email}", email);
|
||||
return SignupResponse.Error(["Failed to create an account."]);
|
||||
}
|
||||
}
|
||||
|
||||
private static string PlatformFromUserAgent(string? userAgent)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userAgent)) return "unknown";
|
||||
return userAgent.Contains("okhttp/") ? "android" : userAgent.Contains("Darwin/") || userAgent.Contains("CFNetwork/") ? "ios" : "web";
|
||||
}
|
||||
private static string GetHashedKey(string value, string grantType)
|
||||
{
|
||||
return (value + ":" + grantType).Sha256();
|
||||
|
||||
Reference in New Issue
Block a user