From 4e681687d6f777b5f6e871b88f39030e97325dc0 Mon Sep 17 00:00:00 2001 From: Lewis Zou Date: Fri, 9 Aug 2024 16:10:26 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=20API=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SecurityTokenService/ConsentOptions.cs | 17 +- .../Controllers/AccountController.cs | 901 +++++++++--------- .../Controllers/ApiResult.cs | 17 +- .../Controllers/ConsentController.cs | 333 ++++--- .../Controllers/DiagnosticsController.cs | 23 +- .../Controllers/Inputs.cs | 359 ++++--- .../Controllers/Outputs.cs | 119 ++- .../Controllers/SecurityHeaders.cs | 75 +- .../Controllers/SessionController.cs | 35 +- .../20220513053014_PersistedGrantInit.cs | 79 +- ...20220513052942_SecurityTokenServiceInit.cs | 137 ++- .../MySqlPersistedGrantDbContextFactory.cs | 18 +- .../MySqlSecurityTokenServiceDbContext.cs | 8 +- ...SqlSecurityTokenServiceDbContextFactory.cs | 18 +- .../20220513053148_PersistedGrantInit.cs | 141 ++- ...20210923145138_SecurityTokenServiceInit.cs | 427 +++++---- .../PostgreSqlPersistedGrantDbContext.cs | 27 +- ...ostgreSqlPersistedGrantDbContextFactory.cs | 19 +- ...PostgreSqlSecurityTokenServiceDbContext.cs | 44 +- ...SqlSecurityTokenServiceDbContextFactory.cs | 19 +- src/SecurityTokenService/Data/SeedData.cs | 37 +- src/SecurityTokenService/Errors.cs | 43 +- .../AuthorizationRequestExtensions.cs | 21 +- .../Extensions/ClientStoreExtensions.cs | 31 +- .../Extensions/ModelBuilderExtensions.cs | 103 +- .../Identity/DefaultIdentityErrorDescriber.cs | 205 ++-- .../Identity/IdentityExtensionOptions.cs | 25 +- .../Identity/IdentitySeedData.cs | 99 +- .../Identity/TestUsers.cs | 85 +- .../IdentityServerExtensionOptions.cs | 21 +- .../IdentityServerExtensions.cs | 301 +++--- src/SecurityTokenService/Program.cs | 253 ++--- .../Properties/launchSettings.json | 3 +- .../SecurityTokenServiceOptions.cs | 19 +- src/SecurityTokenService/Startup.cs | 450 +++------ src/SecurityTokenService/Util.cs | 2 +- .../WebApplicationBuilderExtensions.cs | 252 +++++ 37 files changed, 2444 insertions(+), 2322 deletions(-) create mode 100644 src/SecurityTokenService/WebApplicationBuilderExtensions.cs diff --git a/src/SecurityTokenService/ConsentOptions.cs b/src/SecurityTokenService/ConsentOptions.cs index 4a542da..afc5799 100644 --- a/src/SecurityTokenService/ConsentOptions.cs +++ b/src/SecurityTokenService/ConsentOptions.cs @@ -1,12 +1,11 @@ -namespace SecurityTokenService +namespace SecurityTokenService; + +public class ConsentOptions { - public class ConsentOptions - { - public static bool EnableOfflineAccess = true; - public static string OfflineAccessDisplayName = "Offline Access"; - public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; + public static bool EnableOfflineAccess = true; + public static string OfflineAccessDisplayName = "Offline Access"; + public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; - public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; - public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; - } + public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; + public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; } \ No newline at end of file diff --git a/src/SecurityTokenService/Controllers/AccountController.cs b/src/SecurityTokenService/Controllers/AccountController.cs index 0804ba9..b2ba329 100644 --- a/src/SecurityTokenService/Controllers/AccountController.cs +++ b/src/SecurityTokenService/Controllers/AccountController.cs @@ -22,588 +22,587 @@ using SecurityTokenService.Sms; using SecurityTokenService.Stores; -namespace SecurityTokenService.Controllers +namespace SecurityTokenService.Controllers; + +[SecurityHeaders] +[AllowAnonymous] +[Route("[controller]")] +public class AccountController( + IIdentityServerInteractionService interaction, + IEventService events, + SignInManager signInManager, + IOptionsMonitor options, + IOptionsMonitor identityExtensionOptions, + UserManager userManager, + ILogger logger, + IHostEnvironment hostEnvironment, + IPhoneCodeStore phoneCodeStore, + IPasswordValidator passwordValidator, + ISmsSender smsSender) + : ControllerBase { - [SecurityHeaders] - [AllowAnonymous] - [Route("[controller]")] - public class AccountController( - IIdentityServerInteractionService interaction, - IEventService events, - SignInManager signInManager, - IOptionsMonitor options, - IOptionsMonitor identityExtensionOptions, - UserManager userManager, - ILogger logger, - IHostEnvironment hostEnvironment, - IPhoneCodeStore phoneCodeStore, - IPasswordValidator passwordValidator, - ISmsSender smsSender) - : ControllerBase + private readonly SecurityTokenServiceOptions _options = options.CurrentValue; + private readonly IdentityExtensionOptions _identityExtensionOptions = identityExtensionOptions.CurrentValue; + + /// + /// 通过旧密码修改密码 + /// 要提供用户名 + /// + /// + [HttpPost("ResetPassword2")] + public async Task ResetPasswordByOldPasswordAsync( + Inputs.V1.ResetPasswordByOldPasswordInput input) { - private readonly SecurityTokenServiceOptions _options = options.CurrentValue; - private readonly IdentityExtensionOptions _identityExtensionOptions = identityExtensionOptions.CurrentValue; - - /// - /// 通过旧密码修改密码 - /// 要提供用户名 - /// - /// - [HttpPost("ResetPassword2")] - public async Task ResetPasswordByOldPasswordAsync( - Inputs.V1.ResetPasswordByOldPasswordInput input) - { - var modelErrorResult = BuildModelValidResult(); - if (modelErrorResult != null) - { - return modelErrorResult; - } + var modelErrorResult = BuildModelValidResult(); + if (modelErrorResult != null) + { + return modelErrorResult; + } - var user = await userManager.FindAsync(input.UserName, - _identityExtensionOptions.SoftDeleteColumn); + var user = await userManager.FindAsync(input.UserName, + _identityExtensionOptions.SoftDeleteColumn); - if (user == null) + if (user == null) + { + return new ObjectResult(new ApiResult { - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityUserIsNotExist, Success = false, Message = "用户不存在" - }); - } + Code = Errors.IdentityUserIsNotExist, Success = false, Message = "用户不存在" + }); + } - if (await userManager.IsLockedOutAsync(user)) + if (await userManager.IsLockedOutAsync(user)) + { + return new ObjectResult(new ApiResult { - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityUserIsLockedOut, Success = false, Message = "用户被锁定" - }); - } + Code = Errors.IdentityUserIsLockedOut, Success = false, Message = "用户被锁定" + }); + } - var passwordValidateResult = - await passwordValidator.ValidateAsync(userManager, user, input.NewPassword); - if (!passwordValidateResult.Succeeded) + var passwordValidateResult = + await passwordValidator.ValidateAsync(userManager, user, input.NewPassword); + if (!passwordValidateResult.Succeeded) + { + var msg = string.Join(Environment.NewLine, passwordValidateResult.Errors.Select(x => x.Description)); + return new ObjectResult(new ApiResult { - var msg = string.Join(Environment.NewLine, passwordValidateResult.Errors.Select(x => x.Description)); - return new ObjectResult(new ApiResult - { - Code = Errors.PasswordValidateFailed, Success = false, Message = msg - }); - } + Code = Errors.PasswordValidateFailed, Success = false, Message = msg + }); + } - var checkPasswordResult = await userManager.CheckPasswordAsync(user, input.OldPassword); - if (checkPasswordResult) + var checkPasswordResult = await userManager.CheckPasswordAsync(user, input.OldPassword); + if (checkPasswordResult) + { + var token = await userManager.GeneratePasswordResetTokenAsync(user); + var result = await userManager.ResetPasswordAsync(user, token, input.ConfirmNewPassword); + if (result.Succeeded) { - var token = await userManager.GeneratePasswordResetTokenAsync(user); - var result = await userManager.ResetPasswordAsync(user, token, input.ConfirmNewPassword); - if (result.Succeeded) - { - return new ObjectResult(new ApiResult { Message = "修改成功" }); - } - - var msg = string.Join(Environment.NewLine, result.Errors.Select(x => x.Description)); - return new ObjectResult(new ApiResult - { - Code = Errors.ChangePasswordFailed, Success = false, Message = msg - }); + return new ObjectResult(new ApiResult { Message = "修改成功" }); } + var msg = string.Join(Environment.NewLine, result.Errors.Select(x => x.Description)); return new ObjectResult(new ApiResult { - Code = Errors.IdentityInvalidCredentials, Success = false, Message = "用户名或密码不正确" + Code = Errors.ChangePasswordFailed, Success = false, Message = msg }); } - /// - /// 通过手机号修改密码 - /// - /// - /// - [HttpPost("ResetPassword")] - public async Task ResetPasswordAsync( - [FromBody] Inputs.V1.ResetPasswordByPhoneNumberInput input) + return new ObjectResult(new ApiResult { - var modelErrorResult = BuildModelValidResult(); - if (modelErrorResult != null) - { - return modelErrorResult; - } + Code = Errors.IdentityInvalidCredentials, Success = false, Message = "用户名或密码不正确" + }); + } + + /// + /// 通过手机号修改密码 + /// + /// + /// + [HttpPost("ResetPassword")] + public async Task ResetPasswordAsync( + [FromBody] Inputs.V1.ResetPasswordByPhoneNumberInput input) + { + var modelErrorResult = BuildModelValidResult(); + if (modelErrorResult != null) + { + return modelErrorResult; + } - var user = await userManager.FindAsync(input.PhoneNumber, - _identityExtensionOptions.SoftDeleteColumn); + var user = await userManager.FindAsync(input.PhoneNumber, + _identityExtensionOptions.SoftDeleteColumn); - if (user == null) + if (user == null) + { + return new ObjectResult(new ApiResult { - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityUserIsNotExist, Success = false, Message = "用户不存在" - }); - } + Code = Errors.IdentityUserIsNotExist, Success = false, Message = "用户不存在" + }); + } - if (await userManager.IsLockedOutAsync(user)) + if (await userManager.IsLockedOutAsync(user)) + { + return new ObjectResult(new ApiResult { - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityUserIsLockedOut, Success = false, Message = "用户被锁定" - }); - } + Code = Errors.IdentityUserIsLockedOut, Success = false, Message = "用户被锁定" + }); + } - var passwordValidateResult = - await passwordValidator.ValidateAsync(userManager, user, input.NewPassword); - if (!passwordValidateResult.Succeeded) + var passwordValidateResult = + await passwordValidator.ValidateAsync(userManager, user, input.NewPassword); + if (!passwordValidateResult.Succeeded) + { + var msg = string.Join(Environment.NewLine, passwordValidateResult.Errors.Select(x => x.Description)); + return new ObjectResult(new ApiResult { - var msg = string.Join(Environment.NewLine, passwordValidateResult.Errors.Select(x => x.Description)); - return new ObjectResult(new ApiResult - { - Code = Errors.PasswordValidateFailed, Success = false, Message = msg - }); - } + Code = Errors.PasswordValidateFailed, Success = false, Message = msg + }); + } - var code = await phoneCodeStore.GetAsync(input.PhoneNumber); - //获取手机号对应的缓存验证码 - if (string.IsNullOrEmpty(code) || input.VerifyCode != code) + var code = await phoneCodeStore.GetAsync(input.PhoneNumber); + //获取手机号对应的缓存验证码 + if (string.IsNullOrEmpty(code) || input.VerifyCode != code) + { + return new ObjectResult(new ApiResult { - return new ObjectResult(new ApiResult - { - Code = Errors.VerifyCodeIsInCorrect, Success = false, Message = "验证码不正确" - }); - } + Code = Errors.VerifyCodeIsInCorrect, Success = false, Message = "验证码不正确" + }); + } - var token = await userManager.GeneratePasswordResetTokenAsync(user); - var result = await userManager.ResetPasswordAsync(user, token, input.ConfirmNewPassword); + var token = await userManager.GeneratePasswordResetTokenAsync(user); + var result = await userManager.ResetPasswordAsync(user, token, input.ConfirmNewPassword); - if (!result.Succeeded) + if (!result.Succeeded) + { + var msg = string.Join(Environment.NewLine, result.Errors.Select(x => x.Description)); + return new ObjectResult(new ApiResult { - var msg = string.Join(Environment.NewLine, result.Errors.Select(x => x.Description)); - return new ObjectResult(new ApiResult - { - Code = Errors.ChangePasswordFailed, Success = false, Message = msg - }); - } + Code = Errors.ChangePasswordFailed, Success = false, Message = msg + }); + } + + await phoneCodeStore.UpdateAsync(input.PhoneNumber, ""); + return new ObjectResult(new ApiResult { Message = "修改成功" }); + } - await phoneCodeStore.UpdateAsync(input.PhoneNumber, ""); - return new ObjectResult(new ApiResult { Message = "修改成功" }); + [HttpPost("Login")] + public async Task LoginAsync(Inputs.V1.LoginInput model) + { + var modelErrorResult = BuildModelValidResult(); + if (modelErrorResult != null) + { + return modelErrorResult; } - [HttpPost("Login")] - public async Task LoginAsync(Inputs.V1.LoginInput model) + var passwordValidateResult = await passwordValidator.ValidateAsync(userManager, null, model.Password); + if (!passwordValidateResult.Succeeded) { - var modelErrorResult = BuildModelValidResult(); - if (modelErrorResult != null) + var message = string.Join(Environment.NewLine, + passwordValidateResult.Errors.Select(x => x.Description)); + return new ObjectResult(new ApiResult { - return modelErrorResult; - } + Code = Errors.PasswordValidateFailed, Success = false, Message = $"{message}\n请先修改密码后再登录" + }); + } - var passwordValidateResult = await passwordValidator.ValidateAsync(userManager, null, model.Password); - if (!passwordValidateResult.Succeeded) + var context = await interaction.GetAuthorizationContextAsync(model.ReturnUrl); + // the user clicked the "cancel" button + if (model.Button != "login") + { + if (context != null) { - var message = string.Join(Environment.NewLine, - passwordValidateResult.Errors.Select(x => x.Description)); - return new ObjectResult(new ApiResult - { - Code = Errors.PasswordValidateFailed, Success = false, Message = $"{message}\n请先修改密码后再登录" - }); - } + // if the user cancels, send a result back into IdentityServer as if they + // denied the consent (even if this client does not require consent). + // this will send back an access denied OIDC error response to the client. + await interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied); - var context = await interaction.GetAuthorizationContextAsync(model.ReturnUrl); - // the user clicked the "cancel" button - if (model.Button != "login") - { - if (context != null) + // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null + if (context.IsNativeClient()) { - // if the user cancels, send a result back into IdentityServer as if they - // denied the consent (even if this client does not require consent). - // this will send back an access denied OIDC error response to the client. - await interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied); + // The client is native, so this change in how to + // return the response is for better UX for the end user. - // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null - if (context.IsNativeClient()) + return new ObjectResult(new ApiResult { - // The client is native, so this change in how to - // return the response is for better UX for the end user. - - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityNativeClientIsNotSupported, - Success = false, - Message = "不支持 NativeClient" - }); - } - - return new ObjectResult(new RedirectResult(model.ReturnUrl)); + Code = Errors.IdentityNativeClientIsNotSupported, + Success = false, + Message = "不支持 NativeClient" + }); } - // since we don't have a valid context, then we just go back to the home page - return new ObjectResult(new RedirectResult("/")); + return new ObjectResult(new RedirectResult(model.ReturnUrl)); } - var user = await userManager.FindAsync(model.Username, _identityExtensionOptions.SoftDeleteColumn); + // since we don't have a valid context, then we just go back to the home page + return new ObjectResult(new RedirectResult("/")); + } - if (user == null) - { - await events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", - clientId: context?.Client.ClientId)); - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityInvalidCredentials, Success = false, Message = "用户不存在" - }); - } + var user = await userManager.FindAsync(model.Username, _identityExtensionOptions.SoftDeleteColumn); - var result = await signInManager.PasswordSignInAsync(user, model.Password, - model.RememberLogin, false); - if (result.Succeeded) + if (user == null) + { + await events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", + clientId: context?.Client.ClientId)); + return new ObjectResult(new ApiResult { - await events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, - clientId: context?.Client.ClientId)); + Code = Errors.IdentityInvalidCredentials, Success = false, Message = "用户不存在" + }); + } - if (context != null) - { - // if (await _clientStore.IsPkceClientAsync(context.Client.ClientId)) - // { - // // if the client is PKCE then we assume it's native, so this change in how to - // // return the response is for better UX for the end user. - // // TODO: 意义是说若是 PKCE 客户端,应该返回页面内容,而不是让终端用户自己跳转 - // // 但这样, 不好定义纯 HTML 的内容 - // return new ObjectResult(new RedirectResult(model.ReturnUrl)); - // // return new ObjectResult(new - // // { - // // Code = 302, - // // Location = - // // $"/redirect.html?redirectUrl={HttpUtility.UrlEncode(model.ReturnUrl)}&_t={DateTimeOffset.Now.ToUnixTimeSeconds()}" - // // }); - // } - // else - // { - // // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null - // return new ObjectResult(new RedirectResult(model.ReturnUrl)); - // } - return new ObjectResult(new RedirectResult(model.ReturnUrl)); - } + var result = await signInManager.PasswordSignInAsync(user, model.Password, + model.RememberLogin, false); + if (result.Succeeded) + { + await events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, + clientId: context?.Client.ClientId)); + if (context != null) + { + // if (await _clientStore.IsPkceClientAsync(context.Client.ClientId)) + // { + // // if the client is PKCE then we assume it's native, so this change in how to + // // return the response is for better UX for the end user. + // // TODO: 意义是说若是 PKCE 客户端,应该返回页面内容,而不是让终端用户自己跳转 + // // 但这样, 不好定义纯 HTML 的内容 + // return new ObjectResult(new RedirectResult(model.ReturnUrl)); + // // return new ObjectResult(new + // // { + // // Code = 302, + // // Location = + // // $"/redirect.html?redirectUrl={HttpUtility.UrlEncode(model.ReturnUrl)}&_t={DateTimeOffset.Now.ToUnixTimeSeconds()}" + // // }); + // } + // else + // { + // // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null + // return new ObjectResult(new RedirectResult(model.ReturnUrl)); + // } return new ObjectResult(new RedirectResult(model.ReturnUrl)); } - // TODO: 2 次认证需要支持 - if (result.RequiresTwoFactor) + return new ObjectResult(new RedirectResult(model.ReturnUrl)); + } + + // TODO: 2 次认证需要支持 + if (result.RequiresTwoFactor) + { + return new ObjectResult(new ApiResult { - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityTwoFactorIsNotSupported, Success = false, Message = "" - }); - } + Code = Errors.IdentityTwoFactorIsNotSupported, Success = false, Message = "" + }); + } - if (result.IsNotAllowed) + if (result.IsNotAllowed) + { + return new ObjectResult(new ApiResult { - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityUserIsNotAllowed, Success = false, Message = "禁止登录" - }); - } + Code = Errors.IdentityUserIsNotAllowed, Success = false, Message = "禁止登录" + }); + } - if (result.IsLockedOut) + if (result.IsLockedOut) + { + return new ObjectResult(new ApiResult { - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityUserIsLockedOut, Success = false, Message = "帐号被锁定" - }); - } + Code = Errors.IdentityUserIsLockedOut, Success = false, Message = "帐号被锁定" + }); + } - await events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", - clientId: context?.Client.ClientId)); - return new ObjectResult( - new ApiResult { Code = Errors.IdentityInvalidCredentials, Success = false, Message = "登录失败" }); + await events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", + clientId: context?.Client.ClientId)); + return new ObjectResult( + new ApiResult { Code = Errors.IdentityInvalidCredentials, Success = false, Message = "登录失败" }); - // something went wrong, show form with error - // var vm = await BuildLoginViewModelAsync(model); - // return View(vm); - } + // something went wrong, show form with error + // var vm = await BuildLoginViewModelAsync(model); + // return View(vm); + } - /// - /// 通过短信登录 - /// - /// - /// - [HttpPost("LoginBySms")] - public async Task LoginBySmsAsync(Inputs.V1.LoginBySmsInput model) + /// + /// 通过短信登录 + /// + /// + /// + [HttpPost("LoginBySms")] + public async Task LoginBySmsAsync(Inputs.V1.LoginBySmsInput model) + { + if (!ModelState.IsValid) { - if (!ModelState.IsValid) + var messageBuilder = new StringBuilder(); + foreach (var stateEntry in ModelState) { - var messageBuilder = new StringBuilder(); - foreach (var stateEntry in ModelState) + if (stateEntry.Value.ValidationState != ModelValidationState.Invalid) { - if (stateEntry.Value.ValidationState != ModelValidationState.Invalid) - { - continue; - } - - foreach (var error in stateEntry.Value.Errors) - { - messageBuilder.AppendLine(error.ErrorMessage); - } + continue; } - return new ObjectResult(new ApiResult + foreach (var error in stateEntry.Value.Errors) { - Code = 400, Success = false, Message = messageBuilder.ToString() - }); + messageBuilder.AppendLine(error.ErrorMessage); + } } - var context = await interaction.GetAuthorizationContextAsync(model.ReturnUrl); - // the user clicked the "cancel" button - if (model.Button != "login") + return new ObjectResult(new ApiResult + { + Code = 400, Success = false, Message = messageBuilder.ToString() + }); + } + + var context = await interaction.GetAuthorizationContextAsync(model.ReturnUrl); + // the user clicked the "cancel" button + if (model.Button != "login") + { + if (context != null) { - if (context != null) + // if the user cancels, send a result back into IdentityServer as if they + // denied the consent (even if this client does not require consent). + // this will send back an access denied OIDC error response to the client. + await interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied); + + // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null + if (context.IsNativeClient()) { - // if the user cancels, send a result back into IdentityServer as if they - // denied the consent (even if this client does not require consent). - // this will send back an access denied OIDC error response to the client. - await interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied); + // The client is native, so this change in how to + // return the response is for better UX for the end user. - // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null - if (context.IsNativeClient()) + return new ObjectResult(new ApiResult { - // The client is native, so this change in how to - // return the response is for better UX for the end user. - - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityNativeClientIsNotSupported, - Success = false, - Message = "不支持 NativeClient" - }); - } - - return new ObjectResult(new RedirectResult(model.ReturnUrl)); + Code = Errors.IdentityNativeClientIsNotSupported, + Success = false, + Message = "不支持 NativeClient" + }); } - // since we don't have a valid context, then we just go back to the home page - return new ObjectResult(new RedirectResult("/")); - } - - var user = await userManager.FindAsync(model.PhoneNumber, _identityExtensionOptions.SoftDeleteColumn); - - if (user == null) - { - await events.RaiseAsync(new UserLoginFailureEvent(model.PhoneNumber, "invalid credentials", - clientId: context?.Client.ClientId)); - return new ObjectResult(new ApiResult - { - Code = Errors.IdentityInvalidCredentials, Success = false, Message = "用户不存在" - }); + return new ObjectResult(new RedirectResult(model.ReturnUrl)); } - var code = await phoneCodeStore.GetAsync(model.PhoneNumber); - //获取手机号对应的缓存验证码 - if (string.IsNullOrEmpty(code) || model.VerifyCode != code) - { - return new ObjectResult(new ApiResult - { - Code = Errors.VerifyCodeIsInCorrect, Success = false, Message = "验证码不正确" - }); - } + // since we don't have a valid context, then we just go back to the home page + return new ObjectResult(new RedirectResult("/")); + } - await signInManager.SignInAsync(user, true); + var user = await userManager.FindAsync(model.PhoneNumber, _identityExtensionOptions.SoftDeleteColumn); - await events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, + if (user == null) + { + await events.RaiseAsync(new UserLoginFailureEvent(model.PhoneNumber, "invalid credentials", clientId: context?.Client.ClientId)); - - if (context == null) + return new ObjectResult(new ApiResult { - return new ObjectResult( - new RedirectResult(string.IsNullOrWhiteSpace(model.ReturnUrl) ? "/" : model.ReturnUrl)); - } - - return new ObjectResult(new RedirectResult(model.ReturnUrl)); + Code = Errors.IdentityInvalidCredentials, Success = false, Message = "用户不存在" + }); } - /// - /// 发送短信验证码 - /// - /// - /// - [HttpPost("SendSmsCode")] - [HttpPost("sms")] - public async Task SendSmsCodeAsync([FromBody] Inputs.V1.SendSmsCode input) + var code = await phoneCodeStore.GetAsync(model.PhoneNumber); + //获取手机号对应的缓存验证码 + if (string.IsNullOrEmpty(code) || model.VerifyCode != code) { - var modelErrorResult = BuildModelValidApiResult(); - if (modelErrorResult != null) + return new ObjectResult(new ApiResult { - return modelErrorResult; - } + Code = Errors.VerifyCodeIsInCorrect, Success = false, Message = "验证码不正确" + }); + } - // 不存在也应该发短信, 因为可以是通过短信注册的 - // var user = await _userManager.FindAsync(input.PhoneNumber, _identityExtensionOptions.SoftDeleteColumn); - // if (user != null) - // { - // if (await _userManager.IsLockedOutAsync(user)) - // { - // return new ApiResult { Code = Errors.IdentityUserIsLockedOut, Success = false, Message = "帐号被锁定" }; - // } - // } + await signInManager.SignInAsync(user, true); - var code = RandomNumberGenerator.GetInt32(1111, 9999).ToString(); + await events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, + clientId: context?.Client.ClientId)); - var countryCode = string.IsNullOrWhiteSpace(input.CountryCode) ? "+86" : input.CountryCode; - var phoneNumber = $"{countryCode} {input.PhoneNumber}"; - await phoneCodeStore.UpdateAsync(input.PhoneNumber, code); + if (context == null) + { + return new ObjectResult( + new RedirectResult(string.IsNullOrWhiteSpace(model.ReturnUrl) ? "/" : model.ReturnUrl)); + } - if (hostEnvironment.IsDevelopment()) - { - logger.LogInformation($"Send sms code to {input.PhoneNumber}: {code}"); - return new ApiResult { Success = true }; - } + return new ObjectResult(new RedirectResult(model.ReturnUrl)); + } - try - { - await smsSender.SendAsync(phoneNumber, code); - return new ApiResult { Success = true, Message = "发送成功" }; - } - catch (FriendlyException fe) - { - return new ApiResult { Message = fe.Message, Success = false, Code = Errors.SendSmsFailed }; - } + /// + /// 发送短信验证码 + /// + /// + /// + [HttpPost("SendSmsCode")] + [HttpPost("sms")] + public async Task SendSmsCodeAsync([FromBody] Inputs.V1.SendSmsCode input) + { + var modelErrorResult = BuildModelValidApiResult(); + if (modelErrorResult != null) + { + return modelErrorResult; } - [HttpGet("Logout")] - public async Task Logout(string logoutId) - { - var vm = await BuildLogoutOutputAsync(logoutId); + // 不存在也应该发短信, 因为可以是通过短信注册的 + // var user = await _userManager.FindAsync(input.PhoneNumber, _identityExtensionOptions.SoftDeleteColumn); + // if (user != null) + // { + // if (await _userManager.IsLockedOutAsync(user)) + // { + // return new ApiResult { Code = Errors.IdentityUserIsLockedOut, Success = false, Message = "帐号被锁定" }; + // } + // } - if (vm.ShowLogoutPrompt == false) - { - // if the request for logout was properly authenticated from IdentityServer, then - // we don't need to show the prompt and can just log the user out directly. - return await Logout(new Inputs.V1.LogoutInput { LogoutId = logoutId }); - } + var code = RandomNumberGenerator.GetInt32(1111, 9999).ToString(); - return Redirect($"~/logout.html?logoutId={vm.LogoutId}"); + var countryCode = string.IsNullOrWhiteSpace(input.CountryCode) ? "+86" : input.CountryCode; + var phoneNumber = $"{countryCode} {input.PhoneNumber}"; + await phoneCodeStore.UpdateAsync(input.PhoneNumber, code); + + if (hostEnvironment.IsDevelopment()) + { + logger.LogInformation($"Send sms code to {input.PhoneNumber}: {code}"); + return new ApiResult { Success = true }; } - [HttpPost("Logout")] - public async Task Logout([FromForm] Inputs.V1.LogoutInput model) + try { - // build a model so the logged out page knows what to display - var vm = await BuildLoggedOutOutputAsync(model.LogoutId); + await smsSender.SendAsync(phoneNumber, code); + return new ApiResult { Success = true, Message = "发送成功" }; + } + catch (FriendlyException fe) + { + return new ApiResult { Message = fe.Message, Success = false, Code = Errors.SendSmsFailed }; + } + } - if (User.Identity?.IsAuthenticated == true) - { - // delete local authentication cookie - await signInManager.SignOutAsync(); + [HttpGet("Logout")] + public async Task Logout(string logoutId) + { + var vm = await BuildLogoutOutputAsync(logoutId); - // raise the logout event - await events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); - } + if (vm.ShowLogoutPrompt == false) + { + // if the request for logout was properly authenticated from IdentityServer, then + // we don't need to show the prompt and can just log the user out directly. + return await Logout(new Inputs.V1.LogoutInput { LogoutId = logoutId }); + } - // check if we need to trigger sign-out at an upstream identity provider - if (vm.TriggerExternalSignout) - { - // build a return URL so the upstream provider will redirect back - // to us after the user has logged out. this allows us to then - // complete our single sign-out processing. - var url = Url.Action("Logout", new { logoutId = vm.LogoutId }); + return Redirect($"~/logout.html?logoutId={vm.LogoutId}"); + } - // this triggers a redirect to the external provider for sign-out - return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme); - } + [HttpPost("Logout")] + public async Task Logout([FromForm] Inputs.V1.LogoutInput model) + { + // build a model so the logged out page knows what to display + var vm = await BuildLoggedOutOutputAsync(model.LogoutId); - var postLogoutRedirect = HttpUtility.UrlEncode(vm.PostLogoutRedirectUri); - var signOutIframe = HttpUtility.UrlEncode(vm.SignOutIframeUrl); - var query = - $"postLogoutRedirectUri={postLogoutRedirect}&clientName={HttpUtility.UrlEncode(vm.ClientName)}&signOutIframeUrl={signOutIframe}&automaticRedirectAfterSignOut={(vm.AutomaticRedirectAfterSignOut ? "true" : "false")}"; + if (User.Identity?.IsAuthenticated == true) + { + // delete local authentication cookie + await signInManager.SignOutAsync(); - return Redirect( - $"~/loggedout.html?" + query); + // raise the logout event + await events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); } - private async Task BuildLoggedOutOutputAsync(string logoutId) + // check if we need to trigger sign-out at an upstream identity provider + if (vm.TriggerExternalSignout) { - var logout = await interaction.GetLogoutContextAsync(logoutId); + // build a return URL so the upstream provider will redirect back + // to us after the user has logged out. this allows us to then + // complete our single sign-out processing. + var url = Url.Action("Logout", new { logoutId = vm.LogoutId }); - var vm = new Outputs.V1.LoggedOutOutput - { - AutomaticRedirectAfterSignOut = _options.AutomaticRedirectAfterSignOut, - PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, - ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout.ClientName, - SignOutIframeUrl = logout?.SignOutIFrameUrl, - LogoutId = logoutId - }; - - if (User.Identity?.IsAuthenticated != true) - { - return vm; - } + // this triggers a redirect to the external provider for sign-out + return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme); + } - var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; - if (idp is null or IdentityServer4.IdentityServerConstants.LocalIdentityProvider) - { - return vm; - } + var postLogoutRedirect = HttpUtility.UrlEncode(vm.PostLogoutRedirectUri); + var signOutIframe = HttpUtility.UrlEncode(vm.SignOutIframeUrl); + var query = + $"postLogoutRedirectUri={postLogoutRedirect}&clientName={HttpUtility.UrlEncode(vm.ClientName)}&signOutIframeUrl={signOutIframe}&automaticRedirectAfterSignOut={(vm.AutomaticRedirectAfterSignOut ? "true" : "false")}"; - var providerSupportsSignOut = await HttpContext.GetSchemeSupportsSignOutAsync(idp); - if (!providerSupportsSignOut) - { - return vm; - } + return Redirect( + $"~/loggedout.html?" + query); + } - vm.LogoutId ??= await interaction.CreateLogoutContextAsync(); - vm.ExternalAuthenticationScheme = idp; + private async Task BuildLoggedOutOutputAsync(string logoutId) + { + var logout = await interaction.GetLogoutContextAsync(logoutId); + var vm = new Outputs.V1.LoggedOutOutput + { + AutomaticRedirectAfterSignOut = _options.AutomaticRedirectAfterSignOut, + PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, + ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout.ClientName, + SignOutIframeUrl = logout?.SignOutIFrameUrl, + LogoutId = logoutId + }; + + if (User.Identity?.IsAuthenticated != true) + { return vm; } - private async Task BuildLogoutOutputAsync(string logoutId) + var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; + if (idp is null or IdentityServer4.IdentityServerConstants.LocalIdentityProvider) { - var vm = new Outputs.V1.LogoutOutput { LogoutId = logoutId, ShowLogoutPrompt = _options.ShowLogoutPrompt }; + return vm; + } - if (User.Identity?.IsAuthenticated != true) - { - // if the user is not authenticated, then just show logged out page - vm.ShowLogoutPrompt = false; - return vm; - } + var providerSupportsSignOut = await HttpContext.GetSchemeSupportsSignOutAsync(idp); + if (!providerSupportsSignOut) + { + return vm; + } - var context = await interaction.GetLogoutContextAsync(logoutId); - if (context?.ShowSignoutPrompt != false) - { - return vm; - } + vm.LogoutId ??= await interaction.CreateLogoutContextAsync(); + vm.ExternalAuthenticationScheme = idp; + + return vm; + } - // it's safe to automatically sign-out + private async Task BuildLogoutOutputAsync(string logoutId) + { + var vm = new Outputs.V1.LogoutOutput { LogoutId = logoutId, ShowLogoutPrompt = _options.ShowLogoutPrompt }; + + if (User.Identity?.IsAuthenticated != true) + { + // if the user is not authenticated, then just show logged out page vm.ShowLogoutPrompt = false; return vm; + } - // show the logout prompt. this prevents attacks where the user - // is automatically signed out by another malicious web page. + var context = await interaction.GetLogoutContextAsync(logoutId); + if (context?.ShowSignoutPrompt != false) + { + return vm; } - private ApiResult BuildModelValidApiResult() + // it's safe to automatically sign-out + vm.ShowLogoutPrompt = false; + return vm; + + // show the logout prompt. this prevents attacks where the user + // is automatically signed out by another malicious web page. + } + + private ApiResult BuildModelValidApiResult() + { + if (!ModelState.IsValid) { - if (!ModelState.IsValid) + var messageBuilder = new StringBuilder(); + foreach (var stateEntry in ModelState) { - var messageBuilder = new StringBuilder(); - foreach (var stateEntry in ModelState) + if (stateEntry.Value.ValidationState != ModelValidationState.Invalid) { - if (stateEntry.Value.ValidationState != ModelValidationState.Invalid) - { - continue; - } - - foreach (var error in stateEntry.Value.Errors) - { - messageBuilder.AppendLine(error.ErrorMessage); - } + continue; } - return new ApiResult { Code = 400, Success = false, Message = messageBuilder.ToString() }; - } - else - { - return null; + foreach (var error in stateEntry.Value.Errors) + { + messageBuilder.AppendLine(error.ErrorMessage); + } } - } - private ObjectResult BuildModelValidResult() + return new ApiResult { Code = 400, Success = false, Message = messageBuilder.ToString() }; + } + else { - var apiErrorResult = BuildModelValidApiResult(); - return apiErrorResult == null ? null : new ObjectResult(apiErrorResult); + return null; } } -} + + private ObjectResult BuildModelValidResult() + { + var apiErrorResult = BuildModelValidApiResult(); + return apiErrorResult == null ? null : new ObjectResult(apiErrorResult); + } +} \ No newline at end of file diff --git a/src/SecurityTokenService/Controllers/ApiResult.cs b/src/SecurityTokenService/Controllers/ApiResult.cs index b41849d..c9be814 100644 --- a/src/SecurityTokenService/Controllers/ApiResult.cs +++ b/src/SecurityTokenService/Controllers/ApiResult.cs @@ -1,12 +1,11 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global -namespace SecurityTokenService.Controllers +namespace SecurityTokenService.Controllers; + +public class ApiResult { - public class ApiResult - { - public int Code { get; set; } = 200; - public string Message { get; set; } = string.Empty; - public object Data { get; set; } - public bool Success { get; set; } = true; - } -} + public int Code { get; set; } = 200; + public string Message { get; set; } = string.Empty; + public object Data { get; set; } + public bool Success { get; set; } = true; +} \ No newline at end of file diff --git a/src/SecurityTokenService/Controllers/ConsentController.cs b/src/SecurityTokenService/Controllers/ConsentController.cs index 8e002ce..6da21fe 100644 --- a/src/SecurityTokenService/Controllers/ConsentController.cs +++ b/src/SecurityTokenService/Controllers/ConsentController.cs @@ -11,227 +11,226 @@ using Microsoft.Extensions.Logging; using SecurityTokenService.Extensions; -namespace SecurityTokenService.Controllers +namespace SecurityTokenService.Controllers; + +[SecurityHeaders] +[Route("[controller]")] +[Authorize] +public class ConsentController : ControllerBase { - [SecurityHeaders] - [Route("[controller]")] - [Authorize] - public class ConsentController : ControllerBase + private readonly IIdentityServerInteractionService _interaction; + private readonly IClientStore _clientStore; + private readonly IResourceStore _resourceStore; + private readonly IEventService _events; + private readonly ILogger _logger; + + public ConsentController(IClientStore clientStore, IIdentityServerInteractionService interaction, + IResourceStore resourceStore, IEventService events, ILogger logger) { - private readonly IIdentityServerInteractionService _interaction; - private readonly IClientStore _clientStore; - private readonly IResourceStore _resourceStore; - private readonly IEventService _events; - private readonly ILogger _logger; - - public ConsentController(IClientStore clientStore, IIdentityServerInteractionService interaction, - IResourceStore resourceStore, IEventService events, ILogger logger) - { - _clientStore = clientStore; - _interaction = interaction; - _resourceStore = resourceStore; - _events = events; - _logger = logger; - } + _clientStore = clientStore; + _interaction = interaction; + _resourceStore = resourceStore; + _events = events; + _logger = logger; + } - /// - /// Shows the consent screen - /// - /// - /// - [HttpGet] - public async Task Index(string returnUrl) + /// + /// Shows the consent screen + /// + /// + /// + [HttpGet] + public async Task Index(string returnUrl) + { + var request = await _interaction.GetAuthorizationContextAsync(returnUrl); + string error; + if (request != null) { - var request = await _interaction.GetAuthorizationContextAsync(returnUrl); - string error; - if (request != null) + var client = await _clientStore.FindEnabledClientByIdAsync(request.Client.ClientId); + if (client != null) { - var client = await _clientStore.FindEnabledClientByIdAsync(request.Client.ClientId); - if (client != null) + var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.Client.AllowedScopes); + if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any())) { - var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.Client.AllowedScopes); - if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any())) - { - var output = await CreateConsentOutputAsync(returnUrl, client, resources); - return new ObjectResult(new ApiResult - { - Data = output - }); - } - else + var output = await CreateConsentOutputAsync(returnUrl, client, resources); + return new ObjectResult(new ApiResult { - error = $"No scopes matching: {request.Client.AllowedScopes.Aggregate((x, y) => x + ", " + y)}"; - _logger.LogError(error); - return new ObjectResult(new ApiResult - { - Message = error, - Code = Errors.ConsentNoScopesMatching - }); - } + Data = output + }); } else { - error = $"Invalid client id: {request.Client.ClientId}"; + error = $"No scopes matching: {request.Client.AllowedScopes.Aggregate((x, y) => x + ", " + y)}"; _logger.LogError(error); return new ObjectResult(new ApiResult { Message = error, - Code = Errors.InvalidClientId + Code = Errors.ConsentNoScopesMatching }); } } else { - error = $"No consent request matching request: {returnUrl}"; + error = $"Invalid client id: {request.Client.ClientId}"; _logger.LogError(error); return new ObjectResult(new ApiResult { Message = error, - Code = Errors.NoConsentRequestMatchingRequest + Code = Errors.InvalidClientId }); } } - - [HttpPost] - public async Task Index(Inputs.V1.ConsentInput model) + else { - // validate return url is still valid - var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); + error = $"No consent request matching request: {returnUrl}"; + _logger.LogError(error); + return new ObjectResult(new ApiResult + { + Message = error, + Code = Errors.NoConsentRequestMatchingRequest + }); + } + } - ConsentResponse grantedConsent; + [HttpPost] + public async Task Index(Inputs.V1.ConsentInput model) + { + // validate return url is still valid + var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); - // user clicked 'no' - send back the standard 'access_denied' response - if (model.Button == "no") - { - grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; + ConsentResponse grantedConsent; - // emit event - await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, - request.ValidatedResources.RawScopeValues)); - } - // user clicked 'yes' - validate the data - else if (model.Button == "yes") + // user clicked 'no' - send back the standard 'access_denied' response + if (model.Button == "no") + { + grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; + + // emit event + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, + request.ValidatedResources.RawScopeValues)); + } + // user clicked 'yes' - validate the data + else if (model.Button == "yes") + { + // if the user consented to some scope, build the response model + if (model.ScopesConsented != null && model.ScopesConsented.Any()) { - // if the user consented to some scope, build the response model - if (model.ScopesConsented != null && model.ScopesConsented.Any()) + var scopes = model.ScopesConsented; + if (ConsentOptions.EnableOfflineAccess == false) { - var scopes = model.ScopesConsented; - if (ConsentOptions.EnableOfflineAccess == false) - { - scopes = scopes.Where(x => - x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess); - } - - grantedConsent = new ConsentResponse - { - RememberConsent = model.RememberConsent == "on", - ScopesValuesConsented = scopes.ToArray(), - Description = model.Description - }; - - // emit event - await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, - request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, - grantedConsent.RememberConsent)); + scopes = scopes.Where(x => + x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess); } - else + + grantedConsent = new ConsentResponse { - // result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; - return Redirect("error.html?errorId="); - } + RememberConsent = model.RememberConsent == "on", + ScopesValuesConsented = scopes.ToArray(), + Description = model.Description + }; + + // emit event + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, + request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, + grantedConsent.RememberConsent)); } else { - return Redirect("~/error.html?errorId=" + Errors.ConsentInvalidSelection); + // result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; + return Redirect("error.html?errorId="); } + } + else + { + return Redirect("~/error.html?errorId=" + Errors.ConsentInvalidSelection); + } - // communicate outcome of consent back to identityserver - await _interaction.GrantConsentAsync(request, grantedConsent); + // communicate outcome of consent back to identityserver + await _interaction.GrantConsentAsync(request, grantedConsent); - // indicate that's it ok to redirect back to authorization endpoint - var redirectUrl = model.ReturnUrl; + // indicate that's it ok to redirect back to authorization endpoint + var redirectUrl = model.ReturnUrl; - if (!string.IsNullOrWhiteSpace(redirectUrl)) + if (!string.IsNullOrWhiteSpace(redirectUrl)) + { + if (await _clientStore.IsPkceClientAsync(request.Client.ClientId)) { - if (await _clientStore.IsPkceClientAsync(request.Client.ClientId)) - { - // if the client is PKCE then we assume it's native, so this change in how to - // return the response is for better UX for the end user. - return Redirect("~/redirect.html?redirectUrl=" + HttpUtility.UrlEncode(redirectUrl)); - } - - return Redirect(redirectUrl); + // if the client is PKCE then we assume it's native, so this change in how to + // return the response is for better UX for the end user. + return Redirect("~/redirect.html?redirectUrl=" + HttpUtility.UrlEncode(redirectUrl)); } - // 重新询问 - return Redirect(HttpContext.Request.Headers["Referer"]); + return Redirect(redirectUrl); } - private async Task CreateConsentOutputAsync(string returnUrl, - Client client, Resources resources) + // 重新询问 + return Redirect(HttpContext.Request.Headers["Referer"]); + } + + private async Task CreateConsentOutputAsync(string returnUrl, + Client client, Resources resources) + { + var vm = new Outputs.V1.ConsentOutput { - var vm = new Outputs.V1.ConsentOutput - { - ReturnUrl = returnUrl, - ClientName = client.ClientName ?? client.ClientId, - ClientUrl = client.ClientUri, - ClientLogoUrl = client.LogoUri, - AllowRememberConsent = client.AllowRememberConsent, - IdentityScopes = resources.IdentityResources - .Select(x => CreateIdentityResourceScopeOutput(x, true)).ToArray() - }; - - var scopeNames = resources.ApiResources.SelectMany(x => x.Scopes).ToHashSet(); - var apiScopes = await _resourceStore.FindApiScopesByNameAsync(scopeNames); - vm.ResourceScopes = apiScopes.Select(x => - CreateApiScopeOutput(x, true)).ToArray(); - if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess) + ReturnUrl = returnUrl, + ClientName = client.ClientName ?? client.ClientId, + ClientUrl = client.ClientUri, + ClientLogoUrl = client.LogoUri, + AllowRememberConsent = client.AllowRememberConsent, + IdentityScopes = resources.IdentityResources + .Select(x => CreateIdentityResourceScopeOutput(x, true)).ToArray() + }; + + var scopeNames = resources.ApiResources.SelectMany(x => x.Scopes).ToHashSet(); + var apiScopes = await _resourceStore.FindApiScopesByNameAsync(scopeNames); + vm.ResourceScopes = apiScopes.Select(x => + CreateApiScopeOutput(x, true)).ToArray(); + if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess) + { + vm.ResourceScopes = vm.ResourceScopes.Union(new[] { - vm.ResourceScopes = vm.ResourceScopes.Union(new[] - { - CreateScopeOutput(true) - }); - } - - return vm; + CreateScopeOutput(true) + }); } - private Outputs.V1.ScopeOutput CreateIdentityResourceScopeOutput(IdentityResource identity, bool check) + return vm; + } + + private Outputs.V1.ScopeOutput CreateIdentityResourceScopeOutput(IdentityResource identity, bool check) + { + return new Outputs.V1.ScopeOutput { - return new Outputs.V1.ScopeOutput - { - Name = identity.Name, - DisplayName = identity.DisplayName, - Description = identity.Description, - Emphasize = identity.Emphasize, - Required = identity.Required, - Checked = check || identity.Required - }; - } + Name = identity.Name, + DisplayName = identity.DisplayName, + Description = identity.Description, + Emphasize = identity.Emphasize, + Required = identity.Required, + Checked = check || identity.Required + }; + } - public Outputs.V1.ScopeOutput CreateApiScopeOutput(ApiScope scope, bool check) + public Outputs.V1.ScopeOutput CreateApiScopeOutput(ApiScope scope, bool check) + { + return new Outputs.V1.ScopeOutput { - return new Outputs.V1.ScopeOutput - { - Name = scope.Name, - DisplayName = scope.DisplayName, - Description = scope.Description, - Emphasize = scope.Emphasize, - Required = scope.Required, - Checked = check || scope.Required - }; - } + Name = scope.Name, + DisplayName = scope.DisplayName, + Description = scope.Description, + Emphasize = scope.Emphasize, + Required = scope.Required, + Checked = check || scope.Required + }; + } - private Outputs.V1.ScopeOutput CreateScopeOutput(bool check) + private Outputs.V1.ScopeOutput CreateScopeOutput(bool check) + { + return new Outputs.V1.ScopeOutput { - return new Outputs.V1.ScopeOutput - { - Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, - DisplayName = ConsentOptions.OfflineAccessDisplayName, - Description = ConsentOptions.OfflineAccessDescription, - Emphasize = true, - Checked = check - }; - } + Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, + DisplayName = ConsentOptions.OfflineAccessDisplayName, + Description = ConsentOptions.OfflineAccessDescription, + Emphasize = true, + Checked = check + }; } } \ No newline at end of file diff --git a/src/SecurityTokenService/Controllers/DiagnosticsController.cs b/src/SecurityTokenService/Controllers/DiagnosticsController.cs index ab2b1dd..9868d3d 100644 --- a/src/SecurityTokenService/Controllers/DiagnosticsController.cs +++ b/src/SecurityTokenService/Controllers/DiagnosticsController.cs @@ -3,18 +3,17 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace SecurityTokenService.Controllers +namespace SecurityTokenService.Controllers; + +[SecurityHeaders] +[Route("[controller]")] +[Authorize] +public class DiagnosticsController : ControllerBase { - [SecurityHeaders] - [Route("[controller]")] - [Authorize] - public class DiagnosticsController : ControllerBase + [HttpGet] + public async Task Index() { - [HttpGet] - public async Task Index() - { - var model = new Outputs.V1.DiagnosticsOutput(await HttpContext.AuthenticateAsync()); - return new ObjectResult(model); - } + var model = new Outputs.V1.DiagnosticsOutput(await HttpContext.AuthenticateAsync()); + return new ObjectResult(model); } -} +} \ No newline at end of file diff --git a/src/SecurityTokenService/Controllers/Inputs.cs b/src/SecurityTokenService/Controllers/Inputs.cs index 774b559..fe58678 100644 --- a/src/SecurityTokenService/Controllers/Inputs.cs +++ b/src/SecurityTokenService/Controllers/Inputs.cs @@ -3,188 +3,187 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global -namespace SecurityTokenService.Controllers +namespace SecurityTokenService.Controllers; + +public static class Inputs { - public static class Inputs + public static class V1 { - public static class V1 + public class ResetPasswordByOldPasswordInput + { + /// + /// 用户名或邮箱或手机号 + /// + [Required, StringLength(50)] + public string UserName { get; set; } + + /// + /// 新密码 + /// + [Required, StringLength(32)] + public string NewPassword { get; set; } + + /// + /// 确认新密码 + /// + [Required, StringLength(32)] + [Compare("NewPassword", ErrorMessage = "两次密码不一致")] + public string ConfirmNewPassword { get; set; } + + /// + /// 旧密码 + /// + [Required, StringLength(50)] + public string OldPassword { get; set; } + } + + public class ResetPasswordByPhoneNumberInput + { + /// + /// 新密码 + /// + [StringLength(36, MinimumLength = 4, ErrorMessage = "密码长度不规范")] + public string NewPassword { get; set; } + + /// + /// 确认新密码 + /// + [StringLength(36, MinimumLength = 4, ErrorMessage = "密码长度不规范")] + [Compare("NewPassword", ErrorMessage = "两次密码不一致")] + public string ConfirmNewPassword { get; set; } + + /// + /// 手机号 + /// + [Required(ErrorMessage = "手机号能不能空"), StringLength(20, ErrorMessage = "手机号长度超长")] + public string PhoneNumber { get; set; } + + /// + /// 验证码 + /// + [Required(ErrorMessage = "请填写验证码"), StringLength(6, ErrorMessage = "验证码长度不正确")] + public string VerifyCode { get; set; } + } + + public class SendSmsCode + { + /// + /// + /// + [Required(ErrorMessage = "手机号能不能空"), StringLength(20, ErrorMessage = "手机号长度超长")] + public string PhoneNumber { get; set; } + + /// + /// + /// + [StringLength(10, ErrorMessage = "国家地区码长度不正确")] + public string CountryCode { get; set; } + } + + public class ConsentInput + { + /// + /// + /// + [StringLength(10)] + public string Button { get; set; } + + public IEnumerable ScopesConsented { get; set; } + + /// + /// + /// + [StringLength(10)] + public string RememberConsent { get; set; } + + /// + /// + /// + [StringLength(1000)] + public string ReturnUrl { get; set; } + + /// + /// + /// + [StringLength(2000)] + public string Description { get; set; } + } + + public class LoginBySmsInput + { + /// + /// + /// + [Required(ErrorMessage = "手机号能不能空"), StringLength(20, ErrorMessage = "手机号长度超长")] + public string PhoneNumber { get; set; } + + /// + /// 验证码 + /// + [Required(ErrorMessage = "请填写验证码"), StringLength(6, ErrorMessage = "验证码长度不正确")] + public string VerifyCode { get; set; } + + /// + /// + /// + [Display(Name = "记住我")] + public bool RememberLogin { get; set; } + + /// + /// + /// + [StringLength(1000)] + public string ReturnUrl { get; set; } + + /// + /// + /// + [StringLength(5)] + public string Button { get; set; } + } + + public class LoginInput + { + /// + /// 用户名 + /// + [Required(ErrorMessage = "用户名不能为空"), + StringLength(24, ErrorMessage = "用户名不符合规范")] + public string Username { get; set; } + + /// + /// 密码 + /// + [Required(ErrorMessage = "密码不能为空"), + StringLength(24, ErrorMessage = "密码不符合规范")] + public string Password { get; set; } + + /// + /// + /// + [Display(Name = "记住我")] + public bool RememberLogin { get; set; } + + /// + /// + /// + [StringLength(1000)] + public string ReturnUrl { get; set; } + + /// + /// + /// + [StringLength(5)] + public string Button { get; set; } + } + + public class LogoutInput { - public class ResetPasswordByOldPasswordInput - { - /// - /// 用户名或邮箱或手机号 - /// - [Required, StringLength(50)] - public string UserName { get; set; } - - /// - /// 新密码 - /// - [Required, StringLength(32)] - public string NewPassword { get; set; } - - /// - /// 确认新密码 - /// - [Required, StringLength(32)] - [Compare("NewPassword", ErrorMessage = "两次密码不一致")] - public string ConfirmNewPassword { get; set; } - - /// - /// 旧密码 - /// - [Required, StringLength(50)] - public string OldPassword { get; set; } - } - - public class ResetPasswordByPhoneNumberInput - { - /// - /// 新密码 - /// - [StringLength(36, MinimumLength = 4, ErrorMessage = "密码长度不规范")] - public string NewPassword { get; set; } - - /// - /// 确认新密码 - /// - [StringLength(36, MinimumLength = 4, ErrorMessage = "密码长度不规范")] - [Compare("NewPassword", ErrorMessage = "两次密码不一致")] - public string ConfirmNewPassword { get; set; } - - /// - /// 手机号 - /// - [Required(ErrorMessage = "手机号能不能空"), StringLength(20, ErrorMessage = "手机号长度超长")] - public string PhoneNumber { get; set; } - - /// - /// 验证码 - /// - [Required(ErrorMessage = "请填写验证码"), StringLength(6, ErrorMessage = "验证码长度不正确")] - public string VerifyCode { get; set; } - } - - public class SendSmsCode - { - /// - /// - /// - [Required(ErrorMessage = "手机号能不能空"), StringLength(20, ErrorMessage = "手机号长度超长")] - public string PhoneNumber { get; set; } - - /// - /// - /// - [StringLength(10, ErrorMessage = "国家地区码长度不正确")] - public string CountryCode { get; set; } - } - - public class ConsentInput - { - /// - /// - /// - [StringLength(10)] - public string Button { get; set; } - - public IEnumerable ScopesConsented { get; set; } - - /// - /// - /// - [StringLength(10)] - public string RememberConsent { get; set; } - - /// - /// - /// - [StringLength(1000)] - public string ReturnUrl { get; set; } - - /// - /// - /// - [StringLength(2000)] - public string Description { get; set; } - } - - public class LoginBySmsInput - { - /// - /// - /// - [Required(ErrorMessage = "手机号能不能空"), StringLength(20, ErrorMessage = "手机号长度超长")] - public string PhoneNumber { get; set; } - - /// - /// 验证码 - /// - [Required(ErrorMessage = "请填写验证码"), StringLength(6, ErrorMessage = "验证码长度不正确")] - public string VerifyCode { get; set; } - - /// - /// - /// - [Display(Name = "记住我")] - public bool RememberLogin { get; set; } - - /// - /// - /// - [StringLength(1000)] - public string ReturnUrl { get; set; } - - /// - /// - /// - [StringLength(5)] - public string Button { get; set; } - } - - public class LoginInput - { - /// - /// 用户名 - /// - [Required(ErrorMessage = "用户名不能为空"), - StringLength(24, ErrorMessage = "用户名不符合规范")] - public string Username { get; set; } - - /// - /// 密码 - /// - [Required(ErrorMessage = "密码不能为空"), - StringLength(24, ErrorMessage = "密码不符合规范")] - public string Password { get; set; } - - /// - /// - /// - [Display(Name = "记住我")] - public bool RememberLogin { get; set; } - - /// - /// - /// - [StringLength(1000)] - public string ReturnUrl { get; set; } - - /// - /// - /// - [StringLength(5)] - public string Button { get; set; } - } - - public class LogoutInput - { - /// - /// - /// - [StringLength(36)] - public string LogoutId { get; set; } - } + /// + /// + /// + [StringLength(36)] + public string LogoutId { get; set; } } } -} +} \ No newline at end of file diff --git a/src/SecurityTokenService/Controllers/Outputs.cs b/src/SecurityTokenService/Controllers/Outputs.cs index 7625833..6a378a6 100644 --- a/src/SecurityTokenService/Controllers/Outputs.cs +++ b/src/SecurityTokenService/Controllers/Outputs.cs @@ -3,81 +3,80 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global -namespace SecurityTokenService.Controllers +namespace SecurityTokenService.Controllers; + +public static class Outputs { - public static class Outputs + public static class V1 { - public static class V1 + public class ConsentOutput { - public class ConsentOutput - { - public string ReturnUrl { get; set; } - public string ClientName { get; set; } - public string ClientUrl { get; set; } - public string ClientLogoUrl { get; set; } - public bool AllowRememberConsent { get; set; } - public IEnumerable IdentityScopes { get; set; } - public IEnumerable ResourceScopes { get; set; } - } + public string ReturnUrl { get; set; } + public string ClientName { get; set; } + public string ClientUrl { get; set; } + public string ClientLogoUrl { get; set; } + public bool AllowRememberConsent { get; set; } + public IEnumerable IdentityScopes { get; set; } + public IEnumerable ResourceScopes { get; set; } + } - public class ScopeOutput - { - public string Name { get; set; } - public string DisplayName { get; set; } - public string Description { get; set; } - public bool Emphasize { get; set; } - public bool Required { get; set; } - public bool Checked { get; set; } - } + public class ScopeOutput + { + public string Name { get; set; } + public string DisplayName { get; set; } + public string Description { get; set; } + public bool Emphasize { get; set; } + public bool Required { get; set; } + public bool Checked { get; set; } + } - public class DiagnosticsOutput + public class DiagnosticsOutput + { + public DiagnosticsOutput(AuthenticateResult authenticateResult) { - public DiagnosticsOutput(AuthenticateResult authenticateResult) + var claims = new List(); + var principal = authenticateResult.Principal; + if (principal != null) { - var claims = new List(); - var principal = authenticateResult.Principal; - if (principal != null) + foreach (var claim in principal.Claims) { - foreach (var claim in principal.Claims) - { - claims.Add(new { claim.Type, claim.Value }); - } - - Claims = claims; + claims.Add(new { claim.Type, claim.Value }); } - if (authenticateResult.Properties != null) - { - var properties = new List(); - foreach (var prop in authenticateResult.Properties.Items) - { - properties.Add(new { prop.Key, prop.Value }); - } + Claims = claims; + } - Properties = properties; + if (authenticateResult.Properties != null) + { + var properties = new List(); + foreach (var prop in authenticateResult.Properties.Items) + { + properties.Add(new { prop.Key, prop.Value }); } - } - public object Claims { get; } - public object Properties { get; set; } + Properties = properties; + } } - public class LogoutOutput - { - public string LogoutId { get; set; } - public bool ShowLogoutPrompt { get; set; } = true; - } + public object Claims { get; } + public object Properties { get; set; } + } - public class LoggedOutOutput - { - public string PostLogoutRedirectUri { get; set; } - public string ClientName { get; set; } - public string SignOutIframeUrl { get; set; } - public bool AutomaticRedirectAfterSignOut { get; set; } - public string LogoutId { get; set; } - public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; - public string ExternalAuthenticationScheme { get; set; } - } + public class LogoutOutput + { + public string LogoutId { get; set; } + public bool ShowLogoutPrompt { get; set; } = true; + } + + public class LoggedOutOutput + { + public string PostLogoutRedirectUri { get; set; } + public string ClientName { get; set; } + public string SignOutIframeUrl { get; set; } + public bool AutomaticRedirectAfterSignOut { get; set; } + public string LogoutId { get; set; } + public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; + public string ExternalAuthenticationScheme { get; set; } } } -} +} \ No newline at end of file diff --git a/src/SecurityTokenService/Controllers/SecurityHeaders.cs b/src/SecurityTokenService/Controllers/SecurityHeaders.cs index 1e5c225..a4c27e4 100644 --- a/src/SecurityTokenService/Controllers/SecurityHeaders.cs +++ b/src/SecurityTokenService/Controllers/SecurityHeaders.cs @@ -1,53 +1,52 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -namespace SecurityTokenService.Controllers +namespace SecurityTokenService.Controllers; + +public class SecurityHeadersAttribute : ActionFilterAttribute { - public class SecurityHeadersAttribute : ActionFilterAttribute + public override void OnResultExecuting(ResultExecutingContext context) { - public override void OnResultExecuting(ResultExecutingContext context) + var result = context.Result; + if (result is ViewResult) { - var result = context.Result; - if (result is ViewResult) + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options")) { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options - if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options")) - { - context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff"); - } + context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff"); + } - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options - if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options")) - { - context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); - } + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options")) + { + context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); + } - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy - var csp = - "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';"; - // also consider adding upgrade-insecure-requests once you have HTTPS in place for production - //csp += "upgrade-insecure-requests;"; - // also an example if you need client images to be displayed from twitter - // csp += "img-src 'self' https://pbs.twimg.com;"; + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + var csp = + "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';"; + // also consider adding upgrade-insecure-requests once you have HTTPS in place for production + //csp += "upgrade-insecure-requests;"; + // also an example if you need client images to be displayed from twitter + // csp += "img-src 'self' https://pbs.twimg.com;"; - // once for standards compliant browsers - if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy")) - { - context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp); - } + // once for standards compliant browsers + if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy")) + { + context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp); + } - // and once again for IE - if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy")) - { - context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp); - } + // and once again for IE + if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy")) + { + context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp); + } - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy - var referrer_policy = "no-referrer"; - if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy")) - { - context.HttpContext.Response.Headers.Add("Referrer-Policy", referrer_policy); - } + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy + var referrer_policy = "no-referrer"; + if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy")) + { + context.HttpContext.Response.Headers.Add("Referrer-Policy", referrer_policy); } } } diff --git a/src/SecurityTokenService/Controllers/SessionController.cs b/src/SecurityTokenService/Controllers/SessionController.cs index 8d89787..fd71a67 100644 --- a/src/SecurityTokenService/Controllers/SessionController.cs +++ b/src/SecurityTokenService/Controllers/SessionController.cs @@ -3,26 +3,25 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace SecurityTokenService.Controllers +namespace SecurityTokenService.Controllers; + +[Route("[controller]")] +[SecurityHeaders] +[Authorize] +public class SessionController : ControllerBase { - [Route("[controller]")] - [SecurityHeaders] - [Authorize] - public class SessionController : ControllerBase + [HttpGet] + public ValueTask IndexAsync() { - [HttpGet] - public ValueTask IndexAsync() + IActionResult a = new ObjectResult(new ApiResult { - IActionResult a = new ObjectResult(new ApiResult - { - Code = 200, - Data = HttpContext.User.Claims.Where(x => x.Type != "AspNet.Identity.SecurityStamp") - .Select(x => new - { - x.Type, x.Value - }) - }); - return ValueTask.FromResult(a); - } + Code = 200, + Data = HttpContext.User.Claims.Where(x => x.Type != "AspNet.Identity.SecurityStamp") + .Select(x => new + { + x.Type, x.Value + }) + }); + return ValueTask.FromResult(a); } } \ No newline at end of file diff --git a/src/SecurityTokenService/Data/MySql/Migrations/PersistedGrant/20220513053014_PersistedGrantInit.cs b/src/SecurityTokenService/Data/MySql/Migrations/PersistedGrant/20220513053014_PersistedGrantInit.cs index e45ad62..9074c8f 100644 --- a/src/SecurityTokenService/Data/MySql/Migrations/PersistedGrant/20220513053014_PersistedGrantInit.cs +++ b/src/SecurityTokenService/Data/MySql/Migrations/PersistedGrant/20220513053014_PersistedGrantInit.cs @@ -3,16 +3,16 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace SecurityTokenService.Data.MySql.Migrations.PersistedGrant +namespace SecurityTokenService.Data.MySql.Migrations.PersistedGrant; + +public partial class PersistedGrantInit : Migration { - public partial class PersistedGrantInit : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase() - .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( + migrationBuilder.CreateTable( name: "identity_server_device_codes", columns: table => new { @@ -37,9 +37,9 @@ protected override void Up(MigrationBuilder migrationBuilder) { table.PrimaryKey("PK_identity_server_device_codes", x => x.user_code); }) - .Annotation("MySql:CharSet", "utf8mb4"); + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( + migrationBuilder.CreateTable( name: "identity_server_persisted_grants", columns: table => new { @@ -65,42 +65,41 @@ protected override void Up(MigrationBuilder migrationBuilder) { table.PrimaryKey("PK_identity_server_persisted_grants", x => x.key); }) - .Annotation("MySql:CharSet", "utf8mb4"); + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateIndex( - name: "IX_identity_server_device_codes_device_code", - table: "identity_server_device_codes", - column: "device_code", - unique: true); + migrationBuilder.CreateIndex( + name: "IX_identity_server_device_codes_device_code", + table: "identity_server_device_codes", + column: "device_code", + unique: true); - migrationBuilder.CreateIndex( - name: "IX_identity_server_device_codes_expiration", - table: "identity_server_device_codes", - column: "expiration"); + migrationBuilder.CreateIndex( + name: "IX_identity_server_device_codes_expiration", + table: "identity_server_device_codes", + column: "expiration"); - migrationBuilder.CreateIndex( - name: "IX_identity_server_persisted_grants_expiration", - table: "identity_server_persisted_grants", - column: "expiration"); + migrationBuilder.CreateIndex( + name: "IX_identity_server_persisted_grants_expiration", + table: "identity_server_persisted_grants", + column: "expiration"); - migrationBuilder.CreateIndex( - name: "IX_identity_server_persisted_grants_subject_id_client_id_type", - table: "identity_server_persisted_grants", - columns: new[] { "subject_id", "client_id", "type" }); + migrationBuilder.CreateIndex( + name: "IX_identity_server_persisted_grants_subject_id_client_id_type", + table: "identity_server_persisted_grants", + columns: new[] { "subject_id", "client_id", "type" }); - migrationBuilder.CreateIndex( - name: "IX_identity_server_persisted_grants_subject_id_session_id_type", - table: "identity_server_persisted_grants", - columns: new[] { "subject_id", "session_id", "type" }); - } + migrationBuilder.CreateIndex( + name: "IX_identity_server_persisted_grants_subject_id_session_id_type", + table: "identity_server_persisted_grants", + columns: new[] { "subject_id", "session_id", "type" }); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "identity_server_device_codes"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "identity_server_device_codes"); - migrationBuilder.DropTable( - name: "identity_server_persisted_grants"); - } + migrationBuilder.DropTable( + name: "identity_server_persisted_grants"); } -} +} \ No newline at end of file diff --git a/src/SecurityTokenService/Data/MySql/Migrations/SecurityTokenService/20220513052942_SecurityTokenServiceInit.cs b/src/SecurityTokenService/Data/MySql/Migrations/SecurityTokenService/20220513052942_SecurityTokenServiceInit.cs index 25f2a09..6b389f9 100644 --- a/src/SecurityTokenService/Data/MySql/Migrations/SecurityTokenService/20220513052942_SecurityTokenServiceInit.cs +++ b/src/SecurityTokenService/Data/MySql/Migrations/SecurityTokenService/20220513052942_SecurityTokenServiceInit.cs @@ -4,16 +4,16 @@ #nullable disable -namespace SecurityTokenService.Data.MySql.Migrations.SecurityTokenService +namespace SecurityTokenService.Data.MySql.Migrations.SecurityTokenService; + +public partial class SecurityTokenServiceInit : Migration { - public partial class SecurityTokenServiceInit : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase() - .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( + migrationBuilder.CreateTable( name: "sts_role", columns: table => new { @@ -30,9 +30,9 @@ protected override void Up(MigrationBuilder migrationBuilder) { table.PrimaryKey("PK_sts_role", x => x.id); }) - .Annotation("MySql:CharSet", "utf8mb4"); + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( + migrationBuilder.CreateTable( name: "sts_user", columns: table => new { @@ -70,9 +70,9 @@ protected override void Up(MigrationBuilder migrationBuilder) { table.PrimaryKey("PK_sts_user", x => x.id); }) - .Annotation("MySql:CharSet", "utf8mb4"); + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( + migrationBuilder.CreateTable( name: "sts_role_claim", columns: table => new { @@ -95,9 +95,9 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "id", onDelete: ReferentialAction.Cascade); }) - .Annotation("MySql:CharSet", "utf8mb4"); + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( + migrationBuilder.CreateTable( name: "sts_user_claim", columns: table => new { @@ -120,9 +120,9 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "id", onDelete: ReferentialAction.Cascade); }) - .Annotation("MySql:CharSet", "utf8mb4"); + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( + migrationBuilder.CreateTable( name: "sts_user_login", columns: table => new { @@ -145,9 +145,9 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "id", onDelete: ReferentialAction.Cascade); }) - .Annotation("MySql:CharSet", "utf8mb4"); + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( + migrationBuilder.CreateTable( name: "sts_user_role", columns: table => new { @@ -172,9 +172,9 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "id", onDelete: ReferentialAction.Cascade); }) - .Annotation("MySql:CharSet", "utf8mb4"); + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( + migrationBuilder.CreateTable( name: "sts_user_token", columns: table => new { @@ -197,68 +197,67 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "id", onDelete: ReferentialAction.Cascade); }) - .Annotation("MySql:CharSet", "utf8mb4"); + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - table: "sts_role", - column: "normalized_name", - unique: true); + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "sts_role", + column: "normalized_name", + unique: true); - migrationBuilder.CreateIndex( - name: "IX_sts_role_claim_role_id", - table: "sts_role_claim", - column: "role_id"); + migrationBuilder.CreateIndex( + name: "IX_sts_role_claim_role_id", + table: "sts_role_claim", + column: "role_id"); - migrationBuilder.CreateIndex( - name: "EmailIndex", - table: "sts_user", - column: "normalized_email"); + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "sts_user", + column: "normalized_email"); - migrationBuilder.CreateIndex( - name: "UserNameIndex", - table: "sts_user", - column: "normalized_user_name", - unique: true); + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "sts_user", + column: "normalized_user_name", + unique: true); - migrationBuilder.CreateIndex( - name: "IX_sts_user_claim_user_id", - table: "sts_user_claim", - column: "user_id"); + migrationBuilder.CreateIndex( + name: "IX_sts_user_claim_user_id", + table: "sts_user_claim", + column: "user_id"); - migrationBuilder.CreateIndex( - name: "IX_sts_user_login_user_id", - table: "sts_user_login", - column: "user_id"); + migrationBuilder.CreateIndex( + name: "IX_sts_user_login_user_id", + table: "sts_user_login", + column: "user_id"); - migrationBuilder.CreateIndex( - name: "IX_sts_user_role_role_id", - table: "sts_user_role", - column: "role_id"); - } + migrationBuilder.CreateIndex( + name: "IX_sts_user_role_role_id", + table: "sts_user_role", + column: "role_id"); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "sts_role_claim"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "sts_role_claim"); - migrationBuilder.DropTable( - name: "sts_user_claim"); + migrationBuilder.DropTable( + name: "sts_user_claim"); - migrationBuilder.DropTable( - name: "sts_user_login"); + migrationBuilder.DropTable( + name: "sts_user_login"); - migrationBuilder.DropTable( - name: "sts_user_role"); + migrationBuilder.DropTable( + name: "sts_user_role"); - migrationBuilder.DropTable( - name: "sts_user_token"); + migrationBuilder.DropTable( + name: "sts_user_token"); - migrationBuilder.DropTable( - name: "sts_role"); + migrationBuilder.DropTable( + name: "sts_role"); - migrationBuilder.DropTable( - name: "sts_user"); - } + migrationBuilder.DropTable( + name: "sts_user"); } -} +} \ No newline at end of file diff --git a/src/SecurityTokenService/Data/MySql/MySqlPersistedGrantDbContextFactory.cs b/src/SecurityTokenService/Data/MySql/MySqlPersistedGrantDbContextFactory.cs index 2a205f5..5540cee 100644 --- a/src/SecurityTokenService/Data/MySql/MySqlPersistedGrantDbContextFactory.cs +++ b/src/SecurityTokenService/Data/MySql/MySqlPersistedGrantDbContextFactory.cs @@ -1,16 +1,14 @@ -using System; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.DependencyInjection; -namespace SecurityTokenService.Data.MySql +namespace SecurityTokenService.Data.MySql; + +public class MySqlPersistedGrantDbContextFactory : IDesignTimeDbContextFactory { - public class MySqlPersistedGrantDbContextFactory : IDesignTimeDbContextFactory + public MySqlPersistedGrantDbContext CreateDbContext(string[] args) { - public MySqlPersistedGrantDbContext CreateDbContext(string[] args) - { - var service = Program.CreateHostBuilder(Array.Empty()).Build().Services; - return (MySqlPersistedGrantDbContext)service.CreateScope() - .ServiceProvider.GetRequiredService(typeof(MySqlPersistedGrantDbContext)); - } + var service = Program.CreateApp([]).Services; + return (MySqlPersistedGrantDbContext)service.CreateScope() + .ServiceProvider.GetRequiredService(typeof(MySqlPersistedGrantDbContext)); } -} \ No newline at end of file +} diff --git a/src/SecurityTokenService/Data/MySql/MySqlSecurityTokenServiceDbContext.cs b/src/SecurityTokenService/Data/MySql/MySqlSecurityTokenServiceDbContext.cs index 69c87c5..616ff90 100644 --- a/src/SecurityTokenService/Data/MySql/MySqlSecurityTokenServiceDbContext.cs +++ b/src/SecurityTokenService/Data/MySql/MySqlSecurityTokenServiceDbContext.cs @@ -22,9 +22,13 @@ protected override void OnModelCreating(ModelBuilder builder) entityTypeBuilder.ToTable("system_data_protection_key"); entityTypeBuilder.Property(x => x.Id).HasColumnName("id"); entityTypeBuilder.Property(x => x.FriendlyName).HasMaxLength(64).HasColumnName("friendly_name"); - entityTypeBuilder.Property(x => x.Xml).HasMaxLength(1200).HasColumnName("xml") - .HasConversion(v => Util.Encrypt(Util.DataProtectionKeyAes, v), + + var xmlPropertyBuilder = entityTypeBuilder.Property(x => x.Xml).HasMaxLength(1200).HasColumnName("xml"); + if (Util.DataProtectionKeyAes != null) + { + xmlPropertyBuilder.HasConversion(v => Util.Encrypt(Util.DataProtectionKeyAes, v), v => Util.Decrypt(Util.DataProtectionKeyAes, v)); + } entityTypeBuilder.HasKey(x => x.Id); } diff --git a/src/SecurityTokenService/Data/MySql/MySqlSecurityTokenServiceDbContextFactory.cs b/src/SecurityTokenService/Data/MySql/MySqlSecurityTokenServiceDbContextFactory.cs index 0ee8f5f..d4c2e40 100644 --- a/src/SecurityTokenService/Data/MySql/MySqlSecurityTokenServiceDbContextFactory.cs +++ b/src/SecurityTokenService/Data/MySql/MySqlSecurityTokenServiceDbContextFactory.cs @@ -1,17 +1,15 @@ -using System; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.DependencyInjection; -namespace SecurityTokenService.Data.MySql +namespace SecurityTokenService.Data.MySql; + +public class MySqlSecurityTokenServiceDbContextFactory + : IDesignTimeDbContextFactory { - public class - MySqlSecurityTokenServiceDbContextFactory : IDesignTimeDbContextFactory + public MySqlSecurityTokenServiceDbContext CreateDbContext(string[] args) { - public MySqlSecurityTokenServiceDbContext CreateDbContext(string[] args) - { - var service = Program.CreateHostBuilder(Array.Empty()).Build().Services; - return (MySqlSecurityTokenServiceDbContext)service.CreateScope() - .ServiceProvider.GetRequiredService(typeof(MySqlSecurityTokenServiceDbContext)); - } + var service = Program.CreateApp([]).Services; + return (MySqlSecurityTokenServiceDbContext)service.CreateScope() + .ServiceProvider.GetRequiredService(typeof(MySqlSecurityTokenServiceDbContext)); } } diff --git a/src/SecurityTokenService/Data/PostgreSql/Migrations/PersistedGrant/20220513053148_PersistedGrantInit.cs b/src/SecurityTokenService/Data/PostgreSql/Migrations/PersistedGrant/20220513053148_PersistedGrantInit.cs index aa7e8fe..c83473a 100644 --- a/src/SecurityTokenService/Data/PostgreSql/Migrations/PersistedGrant/20220513053148_PersistedGrantInit.cs +++ b/src/SecurityTokenService/Data/PostgreSql/Migrations/PersistedGrant/20220513053148_PersistedGrantInit.cs @@ -3,85 +3,84 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace SecurityTokenService.Data.PostgreSql.Migrations.PersistedGrant +namespace SecurityTokenService.Data.PostgreSql.Migrations.PersistedGrant; + +public partial class PersistedGrantInit : Migration { - public partial class PersistedGrantInit : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "identity_server_device_codes", - columns: table => new - { - user_code = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - device_code = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - subject_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - session_id = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - client_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - description = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - creation_time = table.Column(type: "timestamp with time zone", nullable: false), - expiration = table.Column(type: "timestamp with time zone", nullable: false), - data = table.Column(type: "character varying(50000)", maxLength: 50000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_identity_server_device_codes", x => x.user_code); - }); + migrationBuilder.CreateTable( + name: "identity_server_device_codes", + columns: table => new + { + user_code = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + device_code = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + subject_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + session_id = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + client_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + description = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + creation_time = table.Column(type: "timestamp with time zone", nullable: false), + expiration = table.Column(type: "timestamp with time zone", nullable: false), + data = table.Column(type: "character varying(50000)", maxLength: 50000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_identity_server_device_codes", x => x.user_code); + }); - migrationBuilder.CreateTable( - name: "identity_server_persisted_grants", - columns: table => new - { - key = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - subject_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - session_id = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - client_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - description = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - creation_time = table.Column(type: "timestamp with time zone", nullable: false), - expiration = table.Column(type: "timestamp with time zone", nullable: true), - consumed_time = table.Column(type: "timestamp with time zone", nullable: true), - data = table.Column(type: "character varying(50000)", maxLength: 50000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_identity_server_persisted_grants", x => x.key); - }); + migrationBuilder.CreateTable( + name: "identity_server_persisted_grants", + columns: table => new + { + key = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + subject_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + session_id = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + client_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + description = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + creation_time = table.Column(type: "timestamp with time zone", nullable: false), + expiration = table.Column(type: "timestamp with time zone", nullable: true), + consumed_time = table.Column(type: "timestamp with time zone", nullable: true), + data = table.Column(type: "character varying(50000)", maxLength: 50000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_identity_server_persisted_grants", x => x.key); + }); - migrationBuilder.CreateIndex( - name: "IX_identity_server_device_codes_device_code", - table: "identity_server_device_codes", - column: "device_code", - unique: true); + migrationBuilder.CreateIndex( + name: "IX_identity_server_device_codes_device_code", + table: "identity_server_device_codes", + column: "device_code", + unique: true); - migrationBuilder.CreateIndex( - name: "IX_identity_server_device_codes_expiration", - table: "identity_server_device_codes", - column: "expiration"); + migrationBuilder.CreateIndex( + name: "IX_identity_server_device_codes_expiration", + table: "identity_server_device_codes", + column: "expiration"); - migrationBuilder.CreateIndex( - name: "IX_identity_server_persisted_grants_expiration", - table: "identity_server_persisted_grants", - column: "expiration"); + migrationBuilder.CreateIndex( + name: "IX_identity_server_persisted_grants_expiration", + table: "identity_server_persisted_grants", + column: "expiration"); - migrationBuilder.CreateIndex( - name: "IX_identity_server_persisted_grants_subject_id_client_id_type", - table: "identity_server_persisted_grants", - columns: new[] { "subject_id", "client_id", "type" }); + migrationBuilder.CreateIndex( + name: "IX_identity_server_persisted_grants_subject_id_client_id_type", + table: "identity_server_persisted_grants", + columns: new[] { "subject_id", "client_id", "type" }); - migrationBuilder.CreateIndex( - name: "IX_identity_server_persisted_grants_subject_id_session_id_type", - table: "identity_server_persisted_grants", - columns: new[] { "subject_id", "session_id", "type" }); - } + migrationBuilder.CreateIndex( + name: "IX_identity_server_persisted_grants_subject_id_session_id_type", + table: "identity_server_persisted_grants", + columns: new[] { "subject_id", "session_id", "type" }); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "identity_server_device_codes"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "identity_server_device_codes"); - migrationBuilder.DropTable( - name: "identity_server_persisted_grants"); - } + migrationBuilder.DropTable( + name: "identity_server_persisted_grants"); } -} +} \ No newline at end of file diff --git a/src/SecurityTokenService/Data/PostgreSql/Migrations/SecurityTokenService/20210923145138_SecurityTokenServiceInit.cs b/src/SecurityTokenService/Data/PostgreSql/Migrations/SecurityTokenService/20210923145138_SecurityTokenServiceInit.cs index 94edecc..95d14e5 100644 --- a/src/SecurityTokenService/Data/PostgreSql/Migrations/SecurityTokenService/20210923145138_SecurityTokenServiceInit.cs +++ b/src/SecurityTokenService/Data/PostgreSql/Migrations/SecurityTokenService/20210923145138_SecurityTokenServiceInit.cs @@ -4,220 +4,219 @@ #nullable disable -namespace SecurityTokenService.Data.PostgreSql.Migrations.SecurityTokenService +namespace SecurityTokenService.Data.PostgreSql.Migrations.SecurityTokenService; + +public partial class SecurityTokenServiceInit : Migration { - public partial class SecurityTokenServiceInit : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "cerberus_role", - columns: table => new - { - id = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - normalized_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - concurrency_stamp = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_cerberus_role", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "cerberus_user", - columns: table => new - { - id = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), - family_name = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - given_name = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - is_deleted = table.Column(type: "boolean", nullable: false), - user_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - normalized_user_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - normalized_email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - email_confirmed = table.Column(type: "boolean", nullable: false), - password_hash = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - security_stamp = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - concurrency_stamp = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - phone_number = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - phone_number_confirmed = table.Column(type: "boolean", nullable: false), - two_factor_enabled = table.Column(type: "boolean", nullable: false), - lockout_end = table.Column(type: "timestamp with time zone", nullable: true), - lockout_enabled = table.Column(type: "boolean", nullable: false), - access_failed_count = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_cerberus_user", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "cerberus_role_claim", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - role_id = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - claim_type = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - claim_value = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_cerberus_role_claim", x => x.id); - table.ForeignKey( - name: "FK_cerberus_role_claim_cerberus_role_role_id", - column: x => x.role_id, - principalTable: "cerberus_role", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "cerberus_user_claim", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - user_id = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - claim_type = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - claim_value = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_cerberus_user_claim", x => x.id); - table.ForeignKey( - name: "FK_cerberus_user_claim_cerberus_user_user_id", - column: x => x.user_id, - principalTable: "cerberus_user", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "cerberus_user_login", - columns: table => new - { - login_provider = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), - provider_key = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), - provider_display_name = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - user_id = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_cerberus_user_login", x => new { x.login_provider, x.provider_key }); - table.ForeignKey( - name: "FK_cerberus_user_login_cerberus_user_user_id", - column: x => x.user_id, - principalTable: "cerberus_user", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "cerberus_user_role", - columns: table => new - { - user_id = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), - role_id = table.Column(type: "character varying(36)", maxLength: 36, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_cerberus_user_role", x => new { x.user_id, x.role_id }); - table.ForeignKey( - name: "FK_cerberus_user_role_cerberus_role_role_id", - column: x => x.role_id, - principalTable: "cerberus_role", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_cerberus_user_role_cerberus_user_user_id", - column: x => x.user_id, - principalTable: "cerberus_user", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "cerberus_user_token", - columns: table => new - { - user_id = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), - login_provider = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), - name = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), - value = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_cerberus_user_token", x => new { x.user_id, x.login_provider, x.name }); - table.ForeignKey( - name: "FK_cerberus_user_token_cerberus_user_user_id", - column: x => x.user_id, - principalTable: "cerberus_user", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - table: "cerberus_role", - column: "normalized_name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_cerberus_role_claim_role_id", - table: "cerberus_role_claim", - column: "role_id"); - - migrationBuilder.CreateIndex( - name: "EmailIndex", - table: "cerberus_user", - column: "normalized_email"); - - migrationBuilder.CreateIndex( - name: "UserNameIndex", - table: "cerberus_user", - column: "normalized_user_name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_cerberus_user_claim_user_id", - table: "cerberus_user_claim", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "IX_cerberus_user_login_user_id", - table: "cerberus_user_login", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "IX_cerberus_user_role_role_id", - table: "cerberus_user_role", - column: "role_id"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "cerberus_role_claim"); - - migrationBuilder.DropTable( - name: "cerberus_user_claim"); - - migrationBuilder.DropTable( - name: "cerberus_user_login"); - - migrationBuilder.DropTable( - name: "cerberus_user_role"); - - migrationBuilder.DropTable( - name: "cerberus_user_token"); - - migrationBuilder.DropTable( - name: "cerberus_role"); - - migrationBuilder.DropTable( - name: "cerberus_user"); - } + migrationBuilder.CreateTable( + name: "cerberus_role", + columns: table => new + { + id = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + concurrency_stamp = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_cerberus_role", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "cerberus_user", + columns: table => new + { + id = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), + family_name = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + given_name = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + is_deleted = table.Column(type: "boolean", nullable: false), + user_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_user_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + email_confirmed = table.Column(type: "boolean", nullable: false), + password_hash = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + security_stamp = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + concurrency_stamp = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + phone_number = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + phone_number_confirmed = table.Column(type: "boolean", nullable: false), + two_factor_enabled = table.Column(type: "boolean", nullable: false), + lockout_end = table.Column(type: "timestamp with time zone", nullable: true), + lockout_enabled = table.Column(type: "boolean", nullable: false), + access_failed_count = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cerberus_user", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "cerberus_role_claim", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + role_id = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + claim_type = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + claim_value = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_cerberus_role_claim", x => x.id); + table.ForeignKey( + name: "FK_cerberus_role_claim_cerberus_role_role_id", + column: x => x.role_id, + principalTable: "cerberus_role", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "cerberus_user_claim", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + user_id = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + claim_type = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + claim_value = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_cerberus_user_claim", x => x.id); + table.ForeignKey( + name: "FK_cerberus_user_claim_cerberus_user_user_id", + column: x => x.user_id, + principalTable: "cerberus_user", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "cerberus_user_login", + columns: table => new + { + login_provider = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), + provider_key = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), + provider_display_name = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + user_id = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cerberus_user_login", x => new { x.login_provider, x.provider_key }); + table.ForeignKey( + name: "FK_cerberus_user_login_cerberus_user_user_id", + column: x => x.user_id, + principalTable: "cerberus_user", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "cerberus_user_role", + columns: table => new + { + user_id = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), + role_id = table.Column(type: "character varying(36)", maxLength: 36, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cerberus_user_role", x => new { x.user_id, x.role_id }); + table.ForeignKey( + name: "FK_cerberus_user_role_cerberus_role_role_id", + column: x => x.role_id, + principalTable: "cerberus_role", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_cerberus_user_role_cerberus_user_user_id", + column: x => x.user_id, + principalTable: "cerberus_user", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "cerberus_user_token", + columns: table => new + { + user_id = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), + login_provider = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), + name = table.Column(type: "character varying(36)", maxLength: 36, nullable: false), + value = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_cerberus_user_token", x => new { x.user_id, x.login_provider, x.name }); + table.ForeignKey( + name: "FK_cerberus_user_token_cerberus_user_user_id", + column: x => x.user_id, + principalTable: "cerberus_user", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "cerberus_role", + column: "normalized_name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_cerberus_role_claim_role_id", + table: "cerberus_role_claim", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "cerberus_user", + column: "normalized_email"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "cerberus_user", + column: "normalized_user_name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_cerberus_user_claim_user_id", + table: "cerberus_user_claim", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_cerberus_user_login_user_id", + table: "cerberus_user_login", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_cerberus_user_role_role_id", + table: "cerberus_user_role", + column: "role_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "cerberus_role_claim"); + + migrationBuilder.DropTable( + name: "cerberus_user_claim"); + + migrationBuilder.DropTable( + name: "cerberus_user_login"); + + migrationBuilder.DropTable( + name: "cerberus_user_role"); + + migrationBuilder.DropTable( + name: "cerberus_user_token"); + + migrationBuilder.DropTable( + name: "cerberus_role"); + + migrationBuilder.DropTable( + name: "cerberus_user"); } -} +} \ No newline at end of file diff --git a/src/SecurityTokenService/Data/PostgreSql/PostgreSqlPersistedGrantDbContext.cs b/src/SecurityTokenService/Data/PostgreSql/PostgreSqlPersistedGrantDbContext.cs index 10f855b..6c2c731 100644 --- a/src/SecurityTokenService/Data/PostgreSql/PostgreSqlPersistedGrantDbContext.cs +++ b/src/SecurityTokenService/Data/PostgreSql/PostgreSqlPersistedGrantDbContext.cs @@ -4,21 +4,18 @@ using Microsoft.Extensions.Options; using SecurityTokenService.IdentityServer; -namespace SecurityTokenService.Data.PostgreSql +namespace SecurityTokenService.Data.PostgreSql; + +public class PostgreSqlPersistedGrantDbContext( + DbContextOptions options, + OperationalStoreOptions storeOptions) + : IdentityServer4.EntityFramework.DbContexts.PersistedGrantDbContext< + PostgreSqlPersistedGrantDbContext>(options, storeOptions) { - public class PostgreSqlPersistedGrantDbContext : IdentityServer4.EntityFramework.DbContexts.PersistedGrantDbContext< - PostgreSqlPersistedGrantDbContext> + protected override void OnModelCreating(ModelBuilder modelBuilder) { - public PostgreSqlPersistedGrantDbContext(DbContextOptions options, - OperationalStoreOptions storeOptions) : base(options, storeOptions) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - var extensionOptions = this.GetService>().CurrentValue; - modelBuilder.ConfigureDefault(extensionOptions.TablePrefix); - } + base.OnModelCreating(modelBuilder); + var extensionOptions = this.GetService>().CurrentValue; + modelBuilder.ConfigureDefault(extensionOptions.TablePrefix); } -} \ No newline at end of file +} diff --git a/src/SecurityTokenService/Data/PostgreSql/PostgreSqlPersistedGrantDbContextFactory.cs b/src/SecurityTokenService/Data/PostgreSql/PostgreSqlPersistedGrantDbContextFactory.cs index 5d09a08..73504a9 100644 --- a/src/SecurityTokenService/Data/PostgreSql/PostgreSqlPersistedGrantDbContextFactory.cs +++ b/src/SecurityTokenService/Data/PostgreSql/PostgreSqlPersistedGrantDbContextFactory.cs @@ -1,16 +1,15 @@ -using System; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.DependencyInjection; -namespace SecurityTokenService.Data.PostgreSql +namespace SecurityTokenService.Data.PostgreSql; + +public class PostgreSqlPersistedGrantDbContextFactory + : IDesignTimeDbContextFactory { - public class PostgreSqlPersistedGrantDbContextFactory : IDesignTimeDbContextFactory + public PostgreSqlPersistedGrantDbContext CreateDbContext(string[] args) { - public PostgreSqlPersistedGrantDbContext CreateDbContext(string[] args) - { - var service = Program.CreateHostBuilder(Array.Empty()).Build().Services; - return (PostgreSqlPersistedGrantDbContext)service.CreateScope() - .ServiceProvider.GetRequiredService(typeof(PostgreSqlPersistedGrantDbContext)); - } + var service = Program.CreateApp([]).Services; + return (PostgreSqlPersistedGrantDbContext)service.CreateScope() + .ServiceProvider.GetRequiredService(typeof(PostgreSqlPersistedGrantDbContext)); } -} \ No newline at end of file +} diff --git a/src/SecurityTokenService/Data/PostgreSql/PostgreSqlSecurityTokenServiceDbContext.cs b/src/SecurityTokenService/Data/PostgreSql/PostgreSqlSecurityTokenServiceDbContext.cs index 7b1b31a..fcb9440 100644 --- a/src/SecurityTokenService/Data/PostgreSql/PostgreSqlSecurityTokenServiceDbContext.cs +++ b/src/SecurityTokenService/Data/PostgreSql/PostgreSqlSecurityTokenServiceDbContext.cs @@ -5,32 +5,34 @@ using Microsoft.Extensions.Options; using SecurityTokenService.Identity; -namespace SecurityTokenService.Data.PostgreSql +namespace SecurityTokenService.Data.PostgreSql; + +public class PostgreSqlSecurityTokenServiceDbContext( + DbContextOptions options) + : IdentityDbContext(options), IDataProtectionKeyContext { - public class PostgreSqlSecurityTokenServiceDbContext( - DbContextOptions options) - : IdentityDbContext(options), IDataProtectionKeyContext + protected override void OnModelCreating(ModelBuilder builder) { - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - var identityExtensionOptions = this.GetService>().CurrentValue; - builder.ConfigureIdentity(identityExtensionOptions); - builder.ConfigureDefault(identityExtensionOptions.TablePrefix); + base.OnModelCreating(builder); - var entityTypeBuilder = builder.Entity(); - entityTypeBuilder.ToTable("system_data_protection_keys"); - entityTypeBuilder.Property(x => x.Id).HasColumnName("id"); - entityTypeBuilder.Property(x => x.FriendlyName).HasMaxLength(64).HasColumnName("friendly_name"); - entityTypeBuilder.Property(x => x.Xml).HasMaxLength(1200).HasColumnName("xml") - .HasConversion(v => Util.Encrypt(Util.DataProtectionKeyAes, v), - v => Util.Decrypt(Util.DataProtectionKeyAes, v)); + var identityExtensionOptions = this.GetService>().CurrentValue; + builder.ConfigureIdentity(identityExtensionOptions); + builder.ConfigureDefault(identityExtensionOptions.TablePrefix); - entityTypeBuilder.HasKey(x => x.Id); + var entityTypeBuilder = builder.Entity(); + entityTypeBuilder.ToTable("system_data_protection_keys"); + entityTypeBuilder.Property(x => x.Id).HasColumnName("id"); + entityTypeBuilder.Property(x => x.FriendlyName).HasMaxLength(64).HasColumnName("friendly_name"); + var xmlPropertyBuilder = entityTypeBuilder.Property(x => x.Xml).HasMaxLength(1200).HasColumnName("xml"); + if (Util.DataProtectionKeyAes != null) + { + xmlPropertyBuilder.HasConversion(v => Util.Encrypt(Util.DataProtectionKeyAes, v), + v => Util.Decrypt(Util.DataProtectionKeyAes, v)); } - // ReSharper disable once UnassignedGetOnlyAutoProperty - public DbSet DataProtectionKeys { get; set; } + entityTypeBuilder.HasKey(x => x.Id); } + + // ReSharper disable once UnassignedGetOnlyAutoProperty + public DbSet DataProtectionKeys { get; set; } } diff --git a/src/SecurityTokenService/Data/PostgreSql/PostgreSqlSecurityTokenServiceDbContextFactory.cs b/src/SecurityTokenService/Data/PostgreSql/PostgreSqlSecurityTokenServiceDbContextFactory.cs index ecd4f82..474a83b 100644 --- a/src/SecurityTokenService/Data/PostgreSql/PostgreSqlSecurityTokenServiceDbContextFactory.cs +++ b/src/SecurityTokenService/Data/PostgreSql/PostgreSqlSecurityTokenServiceDbContextFactory.cs @@ -1,16 +1,15 @@ -using System; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.DependencyInjection; -namespace SecurityTokenService.Data.PostgreSql +namespace SecurityTokenService.Data.PostgreSql; + +public class PostgreSqlSecurityTokenServiceDbContextFactory + : IDesignTimeDbContextFactory { - public class PostgreSqlSecurityTokenServiceDbContextFactory : IDesignTimeDbContextFactory + public PostgreSqlSecurityTokenServiceDbContext CreateDbContext(string[] args) { - public PostgreSqlSecurityTokenServiceDbContext CreateDbContext(string[] args) - { - var service = Program.CreateHostBuilder(Array.Empty()).Build().Services; - return (PostgreSqlSecurityTokenServiceDbContext)service.CreateScope() - .ServiceProvider.GetRequiredService(typeof(PostgreSqlSecurityTokenServiceDbContext)); - } + var service = Program.CreateApp([]).Services; + return (PostgreSqlSecurityTokenServiceDbContext)service.CreateScope() + .ServiceProvider.GetRequiredService(typeof(PostgreSqlSecurityTokenServiceDbContext)); } -} \ No newline at end of file +} diff --git a/src/SecurityTokenService/Data/SeedData.cs b/src/SecurityTokenService/Data/SeedData.cs index 53e8c67..391985c 100644 --- a/src/SecurityTokenService/Data/SeedData.cs +++ b/src/SecurityTokenService/Data/SeedData.cs @@ -4,31 +4,30 @@ using SecurityTokenService.Identity; using Serilog; -namespace SecurityTokenService.Data +namespace SecurityTokenService.Data; + +public class SeedData { - public class SeedData - { - private readonly UserManager _userManager; + private readonly UserManager _userManager; - public SeedData(UserManager userManager) - { - _userManager = userManager; - } + public SeedData(UserManager userManager) + { + _userManager = userManager; + } - public void Load() + public void Load() + { + if (!_userManager.Users.Any()) { - if (!_userManager.Users.Any()) + foreach (var user in TestUsers.Users) { - foreach (var user in TestUsers.Users) + var result = _userManager + .CreateAsync(new User(user.Username), user.Password) + .Result; + if (!result.Succeeded) { - var result = _userManager - .CreateAsync(new User(user.Username), user.Password) - .Result; - if (!result.Succeeded) - { - Log.Logger.Error( - $"Create user: {user.Username} failed: {JsonConvert.SerializeObject(result.Errors)}"); - } + Log.Logger.Error( + $"Create user: {user.Username} failed: {JsonConvert.SerializeObject(result.Errors)}"); } } } diff --git a/src/SecurityTokenService/Errors.cs b/src/SecurityTokenService/Errors.cs index 1e25c9f..6e1f68b 100644 --- a/src/SecurityTokenService/Errors.cs +++ b/src/SecurityTokenService/Errors.cs @@ -1,23 +1,22 @@ -namespace SecurityTokenService +namespace SecurityTokenService; + +public static class Errors { - public static class Errors - { - public const int IdentityTwoFactorIsNotSupported = 4001; - public const int IdentityUserIsNotAllowed = 4002; - public const int IdentityUserIsLockedOut = 4003; - public const int IdentityInvalidCredentials = 4004; - public const int IdentityNativeClientIsNotSupported = 4005; - public const int InvalidReturnUrl = 4006; - public const int ConsentInvalidSelection = 4007; - public const int ConsentNoScopesMatching = 4008; - public const int InvalidClientId = 4009; - public const int NoConsentRequestMatchingRequest = 4010; - public const int IdentityLoginFailed = 4011; - public const int IdentityUserIsNotExist = 4012; - public const int VerifyCodeIsExpired = 4013; - public const int VerifyCodeIsInCorrect = 4014; - public const int PasswordValidateFailed = 4015; - public const int ChangePasswordFailed = 4016; - public const int SendSmsFailed = 4017; - } -} + public const int IdentityTwoFactorIsNotSupported = 4001; + public const int IdentityUserIsNotAllowed = 4002; + public const int IdentityUserIsLockedOut = 4003; + public const int IdentityInvalidCredentials = 4004; + public const int IdentityNativeClientIsNotSupported = 4005; + public const int InvalidReturnUrl = 4006; + public const int ConsentInvalidSelection = 4007; + public const int ConsentNoScopesMatching = 4008; + public const int InvalidClientId = 4009; + public const int NoConsentRequestMatchingRequest = 4010; + public const int IdentityLoginFailed = 4011; + public const int IdentityUserIsNotExist = 4012; + public const int VerifyCodeIsExpired = 4013; + public const int VerifyCodeIsInCorrect = 4014; + public const int PasswordValidateFailed = 4015; + public const int ChangePasswordFailed = 4016; + public const int SendSmsFailed = 4017; +} \ No newline at end of file diff --git a/src/SecurityTokenService/Extensions/AuthorizationRequestExtensions.cs b/src/SecurityTokenService/Extensions/AuthorizationRequestExtensions.cs index 44fd4b6..443593f 100644 --- a/src/SecurityTokenService/Extensions/AuthorizationRequestExtensions.cs +++ b/src/SecurityTokenService/Extensions/AuthorizationRequestExtensions.cs @@ -1,18 +1,17 @@ using System; using IdentityServer4.Models; -namespace SecurityTokenService.Extensions +namespace SecurityTokenService.Extensions; + +public static class AuthorizationRequestExtensions { - public static class AuthorizationRequestExtensions + /// + /// Checks if the redirect URI is for a native client. + /// + /// + public static bool IsNativeClient(this AuthorizationRequest context) { - /// - /// Checks if the redirect URI is for a native client. - /// - /// - public static bool IsNativeClient(this AuthorizationRequest context) - { - return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal) - && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal); - } + return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal) + && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal); } } \ No newline at end of file diff --git a/src/SecurityTokenService/Extensions/ClientStoreExtensions.cs b/src/SecurityTokenService/Extensions/ClientStoreExtensions.cs index fa859c4..06baf10 100644 --- a/src/SecurityTokenService/Extensions/ClientStoreExtensions.cs +++ b/src/SecurityTokenService/Extensions/ClientStoreExtensions.cs @@ -1,25 +1,24 @@ using System.Threading.Tasks; using IdentityServer4.Stores; -namespace SecurityTokenService.Extensions +namespace SecurityTokenService.Extensions; + +public static class ClientStoreExtensions { - public static class ClientStoreExtensions + /// + /// Determines whether the client is configured to use PKCE. + /// + /// The store. + /// The client identifier. + /// + public static async Task IsPkceClientAsync(this IClientStore store, string clientId) { - /// - /// Determines whether the client is configured to use PKCE. - /// - /// The store. - /// The client identifier. - /// - public static async Task IsPkceClientAsync(this IClientStore store, string clientId) + if (string.IsNullOrWhiteSpace(clientId)) { - if (string.IsNullOrWhiteSpace(clientId)) - { - return false; - } - - var client = await store.FindEnabledClientByIdAsync(clientId); - return client?.RequirePkce == true; + return false; } + + var client = await store.FindEnabledClientByIdAsync(clientId); + return client?.RequirePkce == true; } } \ No newline at end of file diff --git a/src/SecurityTokenService/Extensions/ModelBuilderExtensions.cs b/src/SecurityTokenService/Extensions/ModelBuilderExtensions.cs index b8a2905..f241593 100644 --- a/src/SecurityTokenService/Extensions/ModelBuilderExtensions.cs +++ b/src/SecurityTokenService/Extensions/ModelBuilderExtensions.cs @@ -5,82 +5,81 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; -namespace SecurityTokenService.Extensions +namespace SecurityTokenService.Extensions; + +public static class ModelBuilderExtensions { - public static class ModelBuilderExtensions + public static void SetDefaultStringLength(this ModelBuilder builder) { - public static void SetDefaultStringLength(this ModelBuilder builder) + foreach (var entityType in builder.Model.GetEntityTypes()) { - foreach (var entityType in builder.Model.GetEntityTypes()) + // 配置文本默认长度 + foreach (var property in entityType.GetProperties().Where(p => p.ClrType == typeof(string))) { - // 配置文本默认长度 - foreach (var property in entityType.GetProperties().Where(p => p.ClrType == typeof(string))) + // 未配置 + var maxLength = property.GetMaxLength(); + if (maxLength is > 0) { - // 未配置 - var maxLength = property.GetMaxLength(); - if (maxLength is > 0) - { - continue; - } - - property.SetMaxLength(!property.IsKey() ? 255 : 36); + continue; } + + property.SetMaxLength(!property.IsKey() ? 255 : 36); } } + } - public static void SetTablePrefix(this ModelBuilder builder, string tablePrefix) + public static void SetTablePrefix(this ModelBuilder builder, string tablePrefix) + { + if (tablePrefix == null) { - if (tablePrefix == null) - { - return; - } + return; + } - foreach (var entityType in builder.Model.GetEntityTypes()) + foreach (var entityType in builder.Model.GetEntityTypes()) + { + if (!entityType.IsOwned()) { - if (!entityType.IsOwned()) - { - var tableName = tablePrefix + entityType.GetTableName(); - entityType.SetTableName(tableName); - } + var tableName = tablePrefix + entityType.GetTableName(); + entityType.SetTableName(tableName); } } + } - public static void SetSnakeCaseNaming(this ModelBuilder builder) + public static void SetSnakeCaseNaming(this ModelBuilder builder) + { + var nameRewriter = new SnakeCaseNameRewriter(CultureInfo.InvariantCulture); + + foreach (var entity in builder.Model.GetEntityTypes()) { - var nameRewriter = new SnakeCaseNameRewriter(CultureInfo.InvariantCulture); + if (entity.IsOwned()) + { + continue; + } - foreach (var entity in builder.Model.GetEntityTypes()) + var tableName = entity.GetTableName(); + if (string.IsNullOrWhiteSpace(tableName)) { - if (entity.IsOwned()) - { - continue; - } + throw new ArgumentNullException($"The table name of entity {entity.BaseType} is null/empty"); + } - var tableName = entity.GetTableName(); - if (string.IsNullOrWhiteSpace(tableName)) - { - throw new ArgumentNullException($"The table name of entity {entity.BaseType} is null/empty"); - } + if (tableName.Any(char.IsUpper)) + { + entity.SetTableName(nameRewriter.RewriteName(tableName)); + } - if (tableName.Any(char.IsUpper)) + foreach (var property in entity.GetProperties()) + { + var storeObjectIdentifier = StoreObjectIdentifier.Create(entity, StoreObjectType.Table); + var propertyName = property.GetColumnName(storeObjectIdentifier.GetValueOrDefault()); + if (string.IsNullOrWhiteSpace(propertyName)) { - entity.SetTableName(nameRewriter.RewriteName(tableName)); + throw new ArgumentNullException( + $"The property name of entity {entity.BaseType}, {property.DeclaringType} is null/empty"); } - foreach (var property in entity.GetProperties()) + if (propertyName.Any(char.IsUpper)) { - var storeObjectIdentifier = StoreObjectIdentifier.Create(entity, StoreObjectType.Table); - var propertyName = property.GetColumnName(storeObjectIdentifier.GetValueOrDefault()); - if (string.IsNullOrWhiteSpace(propertyName)) - { - throw new ArgumentNullException( - $"The property name of entity {entity.BaseType}, {property.DeclaringType} is null/empty"); - } - - if (propertyName.Any(char.IsUpper)) - { - property.SetColumnName(nameRewriter.RewriteName(propertyName)); - } + property.SetColumnName(nameRewriter.RewriteName(propertyName)); } } } diff --git a/src/SecurityTokenService/Identity/DefaultIdentityErrorDescriber.cs b/src/SecurityTokenService/Identity/DefaultIdentityErrorDescriber.cs index 29a99dd..6cbec2f 100644 --- a/src/SecurityTokenService/Identity/DefaultIdentityErrorDescriber.cs +++ b/src/SecurityTokenService/Identity/DefaultIdentityErrorDescriber.cs @@ -1,108 +1,107 @@ using Microsoft.AspNetCore.Identity; -namespace SecurityTokenService.Identity +namespace SecurityTokenService.Identity; + +public class SecurityTokenServiceIdentityErrorDescriber : IdentityErrorDescriber { - public class SecurityTokenServiceIdentityErrorDescriber : IdentityErrorDescriber - { - public override IdentityError DefaultError() - { - return new() { Code = nameof(DefaultError), Description = "未知错误!" }; - } - - public override IdentityError ConcurrencyFailure() - { - return new() { Code = nameof(ConcurrencyFailure), Description = "并发错误,对象已被修改!" }; - } - - public override IdentityError PasswordMismatch() - { - return new() { Code = "Password", Description = "密码错误!" }; - } - - public override IdentityError InvalidToken() - { - return new() { Code = nameof(InvalidToken), Description = "Invalid token." }; - } - - public override IdentityError LoginAlreadyAssociated() - { - return new() { Code = nameof(LoginAlreadyAssociated), Description = "当前用户已经登录!" }; - } - - public override IdentityError InvalidUserName(string userName) - { - return new() { Code = "UserName", Description = $"用户名 '{userName}' 错误,只可以包含数字和字母!" }; - } - - public override IdentityError InvalidEmail(string email) - { - return new() { Code = "Email", Description = $"邮箱 '{email}' 格式错误!" }; - } - - public override IdentityError DuplicateUserName(string userName) - { - return new() { Code = "UserName", Description = $"用户名 '{userName}' 已存在!" }; - } - - public override IdentityError DuplicateEmail(string email) - { - return new() { Code = "Email", Description = $"邮箱 '{email}' 已经存在!" }; - } - - public override IdentityError InvalidRoleName(string role) - { - return new() { Code = nameof(InvalidRoleName), Description = $"角色 '{role}' 验证错误!" }; - } - - public override IdentityError DuplicateRoleName(string role) - { - return new() { Code = nameof(DuplicateRoleName), Description = $"角色名 '{role}' 已经存在!" }; - } - - public override IdentityError UserAlreadyHasPassword() - { - return new() { Code = nameof(UserAlreadyHasPassword), Description = "User already has a password set." }; - } - - public override IdentityError UserLockoutNotEnabled() - { - return new() - { Code = nameof(UserLockoutNotEnabled), Description = "Lockout is not enabled for this user." }; - } - - public override IdentityError UserAlreadyInRole(string role) - { - return new() { Code = nameof(UserAlreadyInRole), Description = $"User already in role '{role}'." }; - } - - public override IdentityError UserNotInRole(string role) - { - return new() { Code = nameof(UserNotInRole), Description = $"User is not in role '{role}'." }; - } - - public override IdentityError PasswordTooShort(int length) - { - return new() { Code = "Password", Description = $"密码至少 {length} 位!" }; - } - - public override IdentityError PasswordRequiresNonAlphanumeric() - { - return new() { Code = "Password", Description = "密码必须至少有一个非字母数字字符." }; - } - - public override IdentityError PasswordRequiresDigit() - { - return new() { Code = "Password", Description = "密码至少有一个数字 ('0'-'9')." }; - } - - public override IdentityError PasswordRequiresLower() - { - return new() { Code = "Password", Description = "密码必须包含小写字母 ('a'-'z')." }; - } - - public override IdentityError PasswordRequiresUpper() - { - return new() { Code = "Password", Description = "密码必须包含大写字母 ('A'-'Z')." }; - } + public override IdentityError DefaultError() + { + return new() { Code = nameof(DefaultError), Description = "未知错误!" }; + } + + public override IdentityError ConcurrencyFailure() + { + return new() { Code = nameof(ConcurrencyFailure), Description = "并发错误,对象已被修改!" }; + } + + public override IdentityError PasswordMismatch() + { + return new() { Code = "Password", Description = "密码错误!" }; + } + + public override IdentityError InvalidToken() + { + return new() { Code = nameof(InvalidToken), Description = "Invalid token." }; + } + + public override IdentityError LoginAlreadyAssociated() + { + return new() { Code = nameof(LoginAlreadyAssociated), Description = "当前用户已经登录!" }; + } + + public override IdentityError InvalidUserName(string userName) + { + return new() { Code = "UserName", Description = $"用户名 '{userName}' 错误,只可以包含数字和字母!" }; + } + + public override IdentityError InvalidEmail(string email) + { + return new() { Code = "Email", Description = $"邮箱 '{email}' 格式错误!" }; + } + + public override IdentityError DuplicateUserName(string userName) + { + return new() { Code = "UserName", Description = $"用户名 '{userName}' 已存在!" }; + } + + public override IdentityError DuplicateEmail(string email) + { + return new() { Code = "Email", Description = $"邮箱 '{email}' 已经存在!" }; + } + + public override IdentityError InvalidRoleName(string role) + { + return new() { Code = nameof(InvalidRoleName), Description = $"角色 '{role}' 验证错误!" }; + } + + public override IdentityError DuplicateRoleName(string role) + { + return new() { Code = nameof(DuplicateRoleName), Description = $"角色名 '{role}' 已经存在!" }; + } + + public override IdentityError UserAlreadyHasPassword() + { + return new() { Code = nameof(UserAlreadyHasPassword), Description = "User already has a password set." }; + } + + public override IdentityError UserLockoutNotEnabled() + { + return new() + { Code = nameof(UserLockoutNotEnabled), Description = "Lockout is not enabled for this user." }; + } + + public override IdentityError UserAlreadyInRole(string role) + { + return new() { Code = nameof(UserAlreadyInRole), Description = $"User already in role '{role}'." }; + } + + public override IdentityError UserNotInRole(string role) + { + return new() { Code = nameof(UserNotInRole), Description = $"User is not in role '{role}'." }; + } + + public override IdentityError PasswordTooShort(int length) + { + return new() { Code = "Password", Description = $"密码至少 {length} 位!" }; + } + + public override IdentityError PasswordRequiresNonAlphanumeric() + { + return new() { Code = "Password", Description = "密码必须至少有一个非字母数字字符." }; + } + + public override IdentityError PasswordRequiresDigit() + { + return new() { Code = "Password", Description = "密码至少有一个数字 ('0'-'9')." }; + } + + public override IdentityError PasswordRequiresLower() + { + return new() { Code = "Password", Description = "密码必须包含小写字母 ('a'-'z')." }; + } + + public override IdentityError PasswordRequiresUpper() + { + return new() { Code = "Password", Description = "密码必须包含大写字母 ('A'-'Z')." }; } } \ No newline at end of file diff --git a/src/SecurityTokenService/Identity/IdentityExtensionOptions.cs b/src/SecurityTokenService/Identity/IdentityExtensionOptions.cs index f4979b0..e2a16a1 100644 --- a/src/SecurityTokenService/Identity/IdentityExtensionOptions.cs +++ b/src/SecurityTokenService/Identity/IdentityExtensionOptions.cs @@ -1,17 +1,16 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global -namespace SecurityTokenService.Identity +namespace SecurityTokenService.Identity; + +public sealed class IdentityExtensionOptions { - public sealed class IdentityExtensionOptions - { - /// - /// 软删除的列名 - /// - public string SoftDeleteColumn { get; set; } + /// + /// 软删除的列名 + /// + public string SoftDeleteColumn { get; set; } - /// - /// 表前缀 - /// - public string TablePrefix { get; set; } - } -} + /// + /// 表前缀 + /// + public string TablePrefix { get; set; } +} \ No newline at end of file diff --git a/src/SecurityTokenService/Identity/IdentitySeedData.cs b/src/SecurityTokenService/Identity/IdentitySeedData.cs index ef49ebe..1396063 100644 --- a/src/SecurityTokenService/Identity/IdentitySeedData.cs +++ b/src/SecurityTokenService/Identity/IdentitySeedData.cs @@ -13,63 +13,62 @@ using SecurityTokenService.Extensions; using SecurityTokenService.Stores; -namespace SecurityTokenService.Identity +namespace SecurityTokenService.Identity; + +public static class IdentitySeedData { - public static class IdentitySeedData + public static void Load(IApplicationBuilder app) { - public static void Load(IApplicationBuilder app) + using var scope = app.ApplicationServices.CreateScope(); + var configuration = scope.ServiceProvider.GetRequiredService(); + var connectionString = configuration["ConnectionStrings:Identity"]; + DbContext securityTokenServiceDbContext; + DbConnection conn; + if (configuration.GetDatabaseType() == "MySql") { - using var scope = app.ApplicationServices.CreateScope(); - var configuration = scope.ServiceProvider.GetRequiredService(); - var connectionString = configuration["ConnectionStrings:Identity"]; - DbContext securityTokenServiceDbContext; - DbConnection conn; - if (configuration.GetDatabaseType() == "MySql") - { - conn = new MySqlConnection(connectionString); - conn.Execute($""" - create table if not exists system_data_protection_keys - ( - id int auto_increment primary key, - friendly_name varchar(64) not null, - xml varchar(2000) not null - ); - """ - ); + conn = new MySqlConnection(connectionString); + conn.Execute($""" + create table if not exists system_data_protection_keys + ( + id int auto_increment primary key, + friendly_name varchar(64) not null, + xml varchar(2000) not null + ); + """ + ); - securityTokenServiceDbContext = - scope.ServiceProvider.GetRequiredService(); - } - else - { - conn = new NpgsqlConnection(connectionString); - conn.Execute($""" - create table if not exists system_data_protection_keys - ( - id serial primary key, - friendly_name varchar(64) not null, - xml varchar(2000) not null - ); - """ - ); + securityTokenServiceDbContext = + scope.ServiceProvider.GetRequiredService(); + } + else + { + conn = new NpgsqlConnection(connectionString); + conn.Execute($""" + create table if not exists system_data_protection_keys + ( + id serial primary key, + friendly_name varchar(64) not null, + xml varchar(2000) not null + ); + """ + ); - securityTokenServiceDbContext = - scope.ServiceProvider.GetRequiredService(); - } + securityTokenServiceDbContext = + scope.ServiceProvider.GetRequiredService(); + } - conn.Dispose(); + conn.Dispose(); - if (string.Equals(configuration["Identity:SelfHost"], "true", StringComparison.InvariantCultureIgnoreCase)) - { - securityTokenServiceDbContext.Database.Migrate(); - } + if (string.Equals(configuration["Identity:SelfHost"], "true", StringComparison.InvariantCultureIgnoreCase)) + { + securityTokenServiceDbContext.Database.Migrate(); + } - var phoneCodeStore = scope.ServiceProvider.GetService(); - phoneCodeStore?.InitializeAsync().Wait(); + var phoneCodeStore = scope.ServiceProvider.GetService(); + phoneCodeStore?.InitializeAsync().Wait(); - var seedData = scope.ServiceProvider.GetRequiredService(); - seedData.Load(); - securityTokenServiceDbContext.Dispose(); - } + var seedData = scope.ServiceProvider.GetRequiredService(); + seedData.Load(); + securityTokenServiceDbContext.Dispose(); } -} +} \ No newline at end of file diff --git a/src/SecurityTokenService/Identity/TestUsers.cs b/src/SecurityTokenService/Identity/TestUsers.cs index 85c445b..52c4738 100644 --- a/src/SecurityTokenService/Identity/TestUsers.cs +++ b/src/SecurityTokenService/Identity/TestUsers.cs @@ -5,58 +5,57 @@ using IdentityServer4; using IdentityServer4.Test; -namespace SecurityTokenService.Identity +namespace SecurityTokenService.Identity; + +public class TestUsers { - public class TestUsers + public static List Users { - public static List Users + get { - get + var address = new { - var address = new - { - street_address = "One Hacker Way", - locality = "Heidelberg", - postal_code = 69118, - country = "Germany" - }; + street_address = "One Hacker Way", + locality = "Heidelberg", + postal_code = 69118, + country = "Germany" + }; - return new List + return new List + { + new TestUser { - new TestUser + SubjectId = "818727", + Username = "alice", + Password = "1qazZAQ!", + Claims = { - SubjectId = "818727", - Username = "alice", - Password = "1qazZAQ!", - Claims = - { - new Claim(JwtClaimTypes.Name, "Alice Smith"), - new Claim(JwtClaimTypes.GivenName, "Alice"), - new Claim(JwtClaimTypes.FamilyName, "Smith"), - new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), - new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), - new Claim(JwtClaimTypes.WebSite, "http://alice.com"), - new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) - } - }, - new TestUser + new Claim(JwtClaimTypes.Name, "Alice Smith"), + new Claim(JwtClaimTypes.GivenName, "Alice"), + new Claim(JwtClaimTypes.FamilyName, "Smith"), + new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), + new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), + new Claim(JwtClaimTypes.WebSite, "http://alice.com"), + new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) + } + }, + new TestUser + { + SubjectId = "88421113", + Username = "bob", + Password = "1qazZAQ!", + Claims = { - SubjectId = "88421113", - Username = "bob", - Password = "1qazZAQ!", - Claims = - { - new Claim(JwtClaimTypes.Name, "Bob Smith"), - new Claim(JwtClaimTypes.GivenName, "Bob"), - new Claim(JwtClaimTypes.FamilyName, "Smith"), - new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), - new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), - new Claim(JwtClaimTypes.WebSite, "http://bob.com"), - new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) - } + new Claim(JwtClaimTypes.Name, "Bob Smith"), + new Claim(JwtClaimTypes.GivenName, "Bob"), + new Claim(JwtClaimTypes.FamilyName, "Smith"), + new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), + new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), + new Claim(JwtClaimTypes.WebSite, "http://bob.com"), + new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) } - }; - } + } + }; } } } \ No newline at end of file diff --git a/src/SecurityTokenService/IdentityServer/IdentityServerExtensionOptions.cs b/src/SecurityTokenService/IdentityServer/IdentityServerExtensionOptions.cs index fc4d666..58e6345 100644 --- a/src/SecurityTokenService/IdentityServer/IdentityServerExtensionOptions.cs +++ b/src/SecurityTokenService/IdentityServer/IdentityServerExtensionOptions.cs @@ -1,16 +1,15 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global -namespace SecurityTokenService.IdentityServer +namespace SecurityTokenService.IdentityServer; + +public sealed class IdentityServerExtensionOptions { - public sealed class IdentityServerExtensionOptions - { - public string TablePrefix { get; set; } + public string TablePrefix { get; set; } - /// - /// 若 ID4 在反代的后端,反代和 ID4 实例变成了 HTTP 请求,会导致 ID4 组件返回的所有配置都是 HTTP 的, - /// 使用此配置强制让 ID4 认为是 HTTP - /// 使用此中间件后,ID4 只能以 HTTPS 的模式工作 - /// - public bool EnableHttps { get; set; } - } + /// + /// 若 ID4 在反代的后端,反代和 ID4 实例变成了 HTTP 请求,会导致 ID4 组件返回的所有配置都是 HTTP 的, + /// 使用此配置强制让 ID4 认为是 HTTP + /// 使用此中间件后,ID4 只能以 HTTPS 的模式工作 + /// + public bool EnableHttps { get; set; } } \ No newline at end of file diff --git a/src/SecurityTokenService/IdentityServer/IdentityServerExtensions.cs b/src/SecurityTokenService/IdentityServer/IdentityServerExtensions.cs index cec7cae..dfde757 100644 --- a/src/SecurityTokenService/IdentityServer/IdentityServerExtensions.cs +++ b/src/SecurityTokenService/IdentityServer/IdentityServerExtensions.cs @@ -15,180 +15,179 @@ using SecurityTokenService.Identity; using SecurityTokenService.IdentityServer.Stores; -namespace SecurityTokenService.IdentityServer +namespace SecurityTokenService.IdentityServer; + +public static class IdentityServerExtensions { - public static class IdentityServerExtensions + /// + /// 重写 UseIdentityServer 的原因是默认添加了 BaseUrlMiddleware + /// 其会调用 SetIdentityServerBasePath 设置系统的 BasePath + /// 暂时没有好的办法移除此中间件的注册 + /// 又不能直接把 Request.BasePath 进行全局修改(会影响其它功能) + /// + /// + /// + /// + /// + public static IApplicationBuilder UseIdentityServer(this IApplicationBuilder app, + IConfiguration configuration, + IdentityServerMiddlewareOptions options = null) { - /// - /// 重写 UseIdentityServer 的原因是默认添加了 BaseUrlMiddleware - /// 其会调用 SetIdentityServerBasePath 设置系统的 BasePath - /// 暂时没有好的办法移除此中间件的注册 - /// 又不能直接把 Request.BasePath 进行全局修改(会影响其它功能) - /// - /// - /// - /// - /// - public static IApplicationBuilder UseIdentityServer(this IApplicationBuilder app, - IConfiguration configuration, - IdentityServerMiddlewareOptions options = null) - { - var method = typeof(IdentityServerApplicationBuilderExtensions) - .GetMethod("Validate", BindingFlags.NonPublic | BindingFlags.Static); - method?.Invoke(null, [app]); + var method = typeof(IdentityServerApplicationBuilderExtensions) + .GetMethod("Validate", BindingFlags.NonPublic | BindingFlags.Static); + method?.Invoke(null, [app]); - app.UseMiddleware(configuration); + app.UseMiddleware(configuration); - app.ConfigureCors(); + app.ConfigureCors(); - // it seems ok if we have UseAuthentication more than once in the pipeline -- - // this will just re-run the various callback handlers and the default authN - // handler, which just re-assigns the user on the context. claims transformation - // will run twice, since that's not cached (whereas the authN handler result is) - // related: https://github.com/aspnet/Security/issues/1399 - if (options == null) options = new IdentityServerMiddlewareOptions(); - options.AuthenticationMiddleware(app); + // it seems ok if we have UseAuthentication more than once in the pipeline -- + // this will just re-run the various callback handlers and the default authN + // handler, which just re-assigns the user on the context. claims transformation + // will run twice, since that's not cached (whereas the authN handler result is) + // related: https://github.com/aspnet/Security/issues/1399 + if (options == null) options = new IdentityServerMiddlewareOptions(); + options.AuthenticationMiddleware(app); - app.UseMiddleware(); - app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); - return app; - } + return app; + } - // class Config - // { - // public List ApiScopes { get; set; } - // - // // ReSharper disable once CollectionNeverUpdated.Local - // // ReSharper disable once UnusedAutoPropertyAccessor.Local - // public List ApiResources { get; set; } - // - // // ReSharper disable once CollectionNeverUpdated.Local - // // ReSharper disable once UnusedAutoPropertyAccessor.Local - // public List Clients { get; set; } - // - // // ReSharper disable once CollectionNeverUpdated.Local - // // ReSharper disable once UnusedAutoPropertyAccessor.Local - // public List IdentityResources { get; set; } - // } - - private static class Default - { - public static IEnumerable Ids => - new List { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; + // class Config + // { + // public List ApiScopes { get; set; } + // + // // ReSharper disable once CollectionNeverUpdated.Local + // // ReSharper disable once UnusedAutoPropertyAccessor.Local + // public List ApiResources { get; set; } + // + // // ReSharper disable once CollectionNeverUpdated.Local + // // ReSharper disable once UnusedAutoPropertyAccessor.Local + // public List Clients { get; set; } + // + // // ReSharper disable once CollectionNeverUpdated.Local + // // ReSharper disable once UnusedAutoPropertyAccessor.Local + // public List IdentityResources { get; set; } + // } + + private static class Default + { + public static IEnumerable Ids => + new List { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; - public static IEnumerable Apis => - new List { new ApiResource("api1", "My API") }; + public static IEnumerable Apis => + new List { new ApiResource("api1", "My API") }; - public static IEnumerable Clients => - new List + public static IEnumerable Clients => + new List + { + // machine to machine client + new Client { - // machine to machine client - new Client - { - ClientId = "client", - ClientSecrets = { new Secret("secret".Sha256()) }, - AllowedGrantTypes = GrantTypes.ClientCredentials, - // scopes that client has access to - AllowedScopes = { "api1" } - }, - // interactive ASP.NET Core MVC client - new Client + ClientId = "client", + ClientSecrets = { new Secret("secret".Sha256()) }, + AllowedGrantTypes = GrantTypes.ClientCredentials, + // scopes that client has access to + AllowedScopes = { "api1" } + }, + // interactive ASP.NET Core MVC client + new Client + { + ClientId = "mvc", + ClientSecrets = { new Secret("secret".Sha256()) }, + AllowedGrantTypes = GrantTypes.Code, + RequireConsent = false, + RequirePkce = true, + + // where to redirect to after login + RedirectUris = { "http://localhost:8002/signin-oidc" }, + + // where to redirect to after logout + PostLogoutRedirectUris = { "http://localhost:8002/signout-callback-oidc" }, + AllowedScopes = new List { - ClientId = "mvc", - ClientSecrets = { new Secret("secret".Sha256()) }, - AllowedGrantTypes = GrantTypes.Code, - RequireConsent = false, - RequirePkce = true, - - // where to redirect to after login - RedirectUris = { "http://localhost:8002/signin-oidc" }, - - // where to redirect to after logout - PostLogoutRedirectUris = { "http://localhost:8002/signout-callback-oidc" }, - AllowedScopes = new List - { - IdentityServerConstants.StandardScopes.OpenId, - IdentityServerConstants.StandardScopes.Profile, - "api1" - }, - AllowOfflineAccess = true + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + "api1" }, - // JavaScript Client - new Client + AllowOfflineAccess = true + }, + // JavaScript Client + new Client + { + ClientId = "js", + ClientName = "JavaScript Client", + AllowedGrantTypes = GrantTypes.Code, + RequirePkce = true, + RequireClientSecret = false, + RequireConsent = true, + RedirectUris = { "http://localhost:8003/callback.html" }, + PostLogoutRedirectUris = { "http://localhost:8003/index.html" }, + AllowedCorsOrigins = { "http://localhost:8003" }, + AllowedScopes = { - ClientId = "js", - ClientName = "JavaScript Client", - AllowedGrantTypes = GrantTypes.Code, - RequirePkce = true, - RequireClientSecret = false, - RequireConsent = true, - RedirectUris = { "http://localhost:8003/callback.html" }, - PostLogoutRedirectUris = { "http://localhost:8003/index.html" }, - AllowedCorsOrigins = { "http://localhost:8003" }, - AllowedScopes = - { - IdentityServerConstants.StandardScopes.OpenId, - IdentityServerConstants.StandardScopes.Profile, - "api1" - } + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + "api1" } - }; + } + }; - public static IEnumerable ApiScopes => new List { new ApiScope("api1", "My API1") }; - } + public static IEnumerable ApiScopes => new List { new ApiScope("api1", "My API1") }; + } - public static IIdentityServerBuilder AddStore(this IIdentityServerBuilder builder, - IConfiguration configuration) + public static IIdentityServerBuilder AddStore(this IIdentityServerBuilder builder, + IConfiguration configuration) + { + if (configuration.GetSection("identityResources").GetChildren().Any()) { - if (configuration.GetSection("identityResources").GetChildren().Any()) - { - Console.WriteLine("Load config from configuration"); - // var ids = configuration.GetSection("identityResources").Get>() ?? - // new List(); - // var apiScopes = configuration.GetSection("apiScopes").Get>() ?? new List(); - // var apiResources = configuration.GetSection("apiResources").Get>() ?? - // new List(); - // var clients = configuration.GetSection("clients").Get>() ?? new List(); - // builder.AddInMemoryIdentityResources(ids) - // .AddInMemoryApiScopes(apiScopes) - // .AddInMemoryApiResources(apiResources) - // .AddInMemoryClients(clients); - builder.AddInMemoryCaching(); - builder.AddResourceStoreCache(); - builder.AddClientStoreCache(); - } - else - { - builder.AddInMemoryIdentityResources(Default.Ids) - .AddInMemoryApiScopes(Default.ApiScopes) - .AddInMemoryApiResources(Default.Apis) - .AddInMemoryClients(Default.Clients); - } + Console.WriteLine("Load config from configuration"); + // var ids = configuration.GetSection("identityResources").Get>() ?? + // new List(); + // var apiScopes = configuration.GetSection("apiScopes").Get>() ?? new List(); + // var apiResources = configuration.GetSection("apiResources").Get>() ?? + // new List(); + // var clients = configuration.GetSection("clients").Get>() ?? new List(); + // builder.AddInMemoryIdentityResources(ids) + // .AddInMemoryApiScopes(apiScopes) + // .AddInMemoryApiResources(apiResources) + // .AddInMemoryClients(clients); + builder.AddInMemoryCaching(); + builder.AddResourceStoreCache(); + builder.AddClientStoreCache(); + } + else + { + builder.AddInMemoryIdentityResources(Default.Ids) + .AddInMemoryApiScopes(Default.ApiScopes) + .AddInMemoryApiResources(Default.Apis) + .AddInMemoryClients(Default.Clients); + } #if DEBUG - builder.AddTestUsers(TestUsers.Users); + builder.AddTestUsers(TestUsers.Users); #endif - return builder; - } + return builder; + } - public static void MigrateIdentityServer(this IApplicationBuilder app) + public static void MigrateIdentityServer(this IApplicationBuilder app) + { + var configuration = app.ApplicationServices.GetRequiredService(); + using var scope = app.ApplicationServices.CreateScope(); + if (configuration.GetDatabaseType() == "MySql") { - var configuration = app.ApplicationServices.GetRequiredService(); - using var scope = app.ApplicationServices.CreateScope(); - if (configuration.GetDatabaseType() == "MySql") - { - using var persistedGrantDbContext = - scope.ServiceProvider.GetRequiredService(); - persistedGrantDbContext.Database.Migrate(); - } - else - { - using var persistedGrantDbContext = - scope.ServiceProvider.GetRequiredService(); - persistedGrantDbContext.Database.Migrate(); - } + using var persistedGrantDbContext = + scope.ServiceProvider.GetRequiredService(); + persistedGrantDbContext.Database.Migrate(); + } + else + { + using var persistedGrantDbContext = + scope.ServiceProvider.GetRequiredService(); + persistedGrantDbContext.Database.Migrate(); } } -} +} \ No newline at end of file diff --git a/src/SecurityTokenService/Program.cs b/src/SecurityTokenService/Program.cs index 293bad0..cfb51c4 100644 --- a/src/SecurityTokenService/Program.cs +++ b/src/SecurityTokenService/Program.cs @@ -3,124 +3,165 @@ using System.Linq; using System.Security.Cryptography; using System.Text; -using Microsoft.AspNetCore.Hosting; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SecurityTokenService.Data; +using SecurityTokenService.Identity; +using SecurityTokenService.IdentityServer; using Serilog; -using Serilog.Events; -namespace SecurityTokenService +namespace SecurityTokenService; + +public static class Program { - public class Program + public static async Task Main(string[] args) { - public static void Main(string[] args) + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + GenerateAesKey(args); + + var app = CreateApp(args); + var logger = app.Services.GetRequiredService().CreateLogger("Program"); + IdentitySeedData.Load(app); + app.MigrateIdentityServer(); + if (!app.Environment.IsDevelopment()) { - if (args.Contains("--g-aes-key")) + var htmlFiles = Directory.GetFiles("wwwroot", "*.html"); + foreach (var htmlFile in htmlFiles) { - using Aes aes = Aes.Create(); - aes.KeySize = 128; // 可以设置为 128、192 或 256 位 - aes.GenerateKey(); - Console.WriteLine("生成的 AES 密钥: " + Convert.ToBase64String(aes.Key)); + var html = await File.ReadAllTextAsync(htmlFile); + if (html.Contains("site.js")) + { + await File.WriteAllTextAsync(htmlFile, + html.Replace("site.js", $"site.min.js?_t={DateTimeOffset.Now.ToUnixTimeSeconds()}")); + } } - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + logger.LogInformation("处理 js 引用完成"); + } - CreateHostBuilder(args).Build().Run(); + if (!string.IsNullOrEmpty(app.Configuration["BasePath"])) + { + app.UsePathBase(app.Configuration["BasePath"]); } - internal static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((_, builder) => - { - // if (File.Exists("appsettings.Nacos.json")) - // { - // builder.AddJsonFile("appsettings.Nacos.json", true, true); - // } - - var configuration = builder.Build(); - - var serilogSection = configuration.GetSection("Serilog"); - if (serilogSection.GetChildren().Any()) - { - Log.Logger = new LoggerConfiguration().ReadFrom - .Configuration(configuration) - .CreateLogger(); - } - else - { - var logFile = Environment.GetEnvironmentVariable("LOG"); - if (string.IsNullOrEmpty(logFile)) - { - logFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs/sts.log"); - } - - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Information() - .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .MinimumLevel.Override("System", LogEventLevel.Warning) - .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Warning) - .Enrich.FromLogContext() -#if DEBUG - .WriteTo.Console() -#endif - .WriteTo.Async(x => x.File(logFile, rollingInterval: RollingInterval.Day)) - .CreateLogger(); - } - - var path = "sts.json"; - if (File.Exists(path)) - { - builder.AddJsonFile(path, true, true); - } - - // var nacosSection = configuration.GetSection("Nacos"); - // if (nacosSection.GetChildren().Any()) - // { - // builder.AddNacosV2Configuration(nacosSection); - // } - }) - .ConfigureWebHostDefaults(webBuilder => - { - // webBuilder.ConfigureKestrel(serverOptions => - // { - // serverOptions.Listen(IPAddress.Any, 80); - // - // var certPath = Environment.GetEnvironmentVariable("X509Certificate2"); - // if (string.IsNullOrWhiteSpace(certPath)) - // { - // return; - // } - // - // var privateKeyPath = Path.GetFileNameWithoutExtension(certPath) + ".key"; - // var cert = CreateX509Certificate2(certPath, privateKeyPath); - // - // serverOptions.Listen(IPAddress.Any, 8100, - // (Action)(listenOptions => listenOptions.UseHttps(cert))); - // }); - webBuilder.UseStartup(); - }).UseSerilog(); - - // private static X509Certificate2 CreateX509Certificate2( - // string certificatePath, - // string privateKeyPath) - // { - // using var certificate = new X509Certificate2(certificatePath); - // var strArray = File.ReadAllText(privateKeyPath).Split("-", StringSplitOptions.RemoveEmptyEntries); - // var source = Convert.FromBase64String(strArray[1]); - // using var privateKey = RSA.Create(); - // int bytesRead; - // switch (strArray[0]) - // { - // case "BEGIN PRIVATE KEY": - // privateKey.ImportPkcs8PrivateKey((ReadOnlySpan)source, out bytesRead); - // break; - // case "BEGIN RSA PRIVATE KEY": - // privateKey.ImportRSAPrivateKey((ReadOnlySpan)source, out bytesRead); - // break; - // } - // - // return new X509Certificate2(certificate.CopyWithPrivateKey(privateKey).Export(X509ContentType.Pfx)); - // } + app.UseHealthChecks("/healthz"); + app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax }); + app.UseFileServer(); + app.UseRouting(); + app.UseCors("cors"); + app.UseMiddleware(app.Configuration); + app.UseIdentityServer(); + app.UseAuthorization(); + var inDapr = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DAPR_HTTP_PORT")); + if (inDapr) + { + app.UseCloudEvents(); + app.MapSubscribeHandler(); + } + + app.MapControllers().RequireCors("cors"); + await app.RunAsync(); } + + internal static WebApplication CreateApp(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.AddSerilog(); + + var dataPath = builder.Configuration["DATAPATH"] ?? builder.Configuration["DATA_PATH"] ?? "sts.json"; + if (File.Exists(dataPath)) + { + builder.Configuration.AddJsonFile(dataPath, true, true); + } + + var mvcBuilder = builder.Services.AddControllers(); + var daprHttpPort = builder.Configuration["DaprHttpPort"] ?? builder.Configuration["DAPR_HTTP_PORT"]; + if (!string.IsNullOrWhiteSpace(daprHttpPort)) + { + mvcBuilder.AddDapr(); + } + + builder.AddDataProtection(); + builder.AddSmsSender(); + builder.AddDbContext(); + builder.AddIdentity(); + builder.AddIdentityServer(); + builder.ConfigureOptions(); + builder.Services.AddScoped(); + + builder.Services.AddRouting(options => options.LowercaseUrls = true); + builder.Services.AddHealthChecks(); + builder.Services.AddCors(option => option + .AddPolicy("cors", policy => + policy.AllowAnyMethod() + .SetIsOriginAllowed(_ => true) + .AllowAnyHeader() + .AllowCredentials() + )); + builder.Host.UseSerilog(); + + var app = builder.Build(); + return app; + } + + private static void GenerateAesKey(string[] args) + { + if (args.Contains("--g-aes-key")) + { + using Aes aes = Aes.Create(); + aes.KeySize = 128; // 可以设置为 128、192 或 256 位 + aes.GenerateKey(); + Console.WriteLine("生成的 AES 密钥: " + Convert.ToBase64String(aes.Key)); + } + } + + // internal static IHostBuilder CreateHostBuilder(string[] args) => + // Host.CreateDefaultBuilder(args) + // .ConfigureWebHostDefaults(webBuilder => + // { + // // webBuilder.ConfigureKestrel(serverOptions => + // // { + // // serverOptions.Listen(IPAddress.Any, 80); + // // + // // var certPath = Environment.GetEnvironmentVariable("X509Certificate2"); + // // if (string.IsNullOrWhiteSpace(certPath)) + // // { + // // return; + // // } + // // + // // var privateKeyPath = Path.GetFileNameWithoutExtension(certPath) + ".key"; + // // var cert = CreateX509Certificate2(certPath, privateKeyPath); + // // + // // serverOptions.Listen(IPAddress.Any, 8100, + // // (Action)(listenOptions => listenOptions.UseHttps(cert))); + // // }); + // webBuilder.UseStartup(); + // }).UseSerilog(); + + // private static X509Certificate2 CreateX509Certificate2( + // string certificatePath, + // string privateKeyPath) + // { + // using var certificate = new X509Certificate2(certificatePath); + // var strArray = File.ReadAllText(privateKeyPath).Split("-", StringSplitOptions.RemoveEmptyEntries); + // var source = Convert.FromBase64String(strArray[1]); + // using var privateKey = RSA.Create(); + // int bytesRead; + // switch (strArray[0]) + // { + // case "BEGIN PRIVATE KEY": + // privateKey.ImportPkcs8PrivateKey((ReadOnlySpan)source, out bytesRead); + // break; + // case "BEGIN RSA PRIVATE KEY": + // privateKey.ImportRSAPrivateKey((ReadOnlySpan)source, out bytesRead); + // break; + // } + // + // return new X509Certificate2(certificate.CopyWithPrivateKey(privateKey).Export(X509ContentType.Pfx)); + // } } diff --git a/src/SecurityTokenService/Properties/launchSettings.json b/src/SecurityTokenService/Properties/launchSettings.json index b1dc7af..1d4018a 100644 --- a/src/SecurityTokenService/Properties/launchSettings.json +++ b/src/SecurityTokenService/Properties/launchSettings.json @@ -7,7 +7,8 @@ "applicationUrl": "http://*:8099", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "commandLineArgs": "--g-aes-key" } } } diff --git a/src/SecurityTokenService/SecurityTokenServiceOptions.cs b/src/SecurityTokenService/SecurityTokenServiceOptions.cs index 557d75d..3c522c5 100644 --- a/src/SecurityTokenService/SecurityTokenServiceOptions.cs +++ b/src/SecurityTokenService/SecurityTokenServiceOptions.cs @@ -1,12 +1,11 @@ -namespace SecurityTokenService +namespace SecurityTokenService; + +public class SecurityTokenServiceOptions { - public class SecurityTokenServiceOptions - { - public bool AutomaticRedirectAfterSignOut { get; set; } - public bool AllowLocalLogin { get; set; } - public bool AllowRememberLogin { get; set; } - public bool ShowLogoutPrompt { get; set; } - public int RememberMeLoginDuration { get; set; } - public string WindowsAuthenticationSchemeName { get; set; } - } + public bool AutomaticRedirectAfterSignOut { get; set; } + public bool AllowLocalLogin { get; set; } + public bool AllowRememberLogin { get; set; } + public bool ShowLogoutPrompt { get; set; } + public int RememberMeLoginDuration { get; set; } + public string WindowsAuthenticationSchemeName { get; set; } } \ No newline at end of file diff --git a/src/SecurityTokenService/Startup.cs b/src/SecurityTokenService/Startup.cs index 69b55a1..de0d31b 100644 --- a/src/SecurityTokenService/Startup.cs +++ b/src/SecurityTokenService/Startup.cs @@ -1,297 +1,153 @@ -using System; -using System.IO; -using System.Reflection; -using System.Text; -using IdentityServer4; -using IdentityServer4.Configuration; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using SecurityTokenService.Data; -using SecurityTokenService.Data.MySql; -using SecurityTokenService.Data.PostgreSql; -using SecurityTokenService.Extensions; -using SecurityTokenService.Identity; -using SecurityTokenService.IdentityServer; -using SecurityTokenService.Options; -using SecurityTokenService.Sms; -using SecurityTokenService.Stores; - -namespace SecurityTokenService -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - // var keysFolder = new DirectoryInfo("KEYS"); - // if (!keysFolder.Exists) - // { - // keysFolder.Create(); - // } - // comments by lewis at 20240222 - // 必须是 128、256 位 - - var dataProtectionKey = Configuration["DataProtection:Key"]; - if (!string.IsNullOrEmpty(dataProtectionKey)) - { - Util.DataProtectionKeyAes.Key = Encoding.UTF8.GetBytes(dataProtectionKey); - } - - var builder = services.AddControllers(); - if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DAPR_HTTP_PORT"))) - { - builder.AddDapr(); - } - - // 影响隐私数据加密、AntiToken 加解密 - var dataProtectionBuilder = services.AddDataProtection() - .SetApplicationName("SecurityTokenService") - .SetDefaultKeyLifetime(TimeSpan.FromDays(90)); - - if (Configuration.GetDatabaseType() == "MySql") - { - dataProtectionBuilder.PersistKeysToDbContext(); - } - else - { - dataProtectionBuilder.PersistKeysToDbContext(); - } - - services.AddRouting(options => options.LowercaseUrls = true); - services.AddHealthChecks(); - services.AddScoped(); - - services.AddCors(option => option - .AddPolicy("cors", policy => - policy.AllowAnyMethod() - .SetIsOriginAllowed(_ => true) - .AllowAnyHeader() - .AllowCredentials() - )); - - // 注册短信平台 - switch (Configuration["SecurityTokenService:SmsProvider"]) - { - case "TencentCloud": - services.AddTransient(); - break; - default: - services.AddTransient(); - break; - } - - ConfigureDbContext(services); - ConfigureIdentity(services); - ConfigureIdentityServer(services); - ConfigureOptions(services); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - var logger = app.ApplicationServices.GetRequiredService().CreateLogger("Startup"); - IdentitySeedData.Load(app); - - app.MigrateIdentityServer(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - // app.UseExceptionHandler("/Error"); - var htmlFiles = Directory.GetFiles("wwwroot", "*.html"); - foreach (var htmlFile in htmlFiles) - { - var html = File.ReadAllText(htmlFile); - if (html.Contains("site.js")) - { - File.WriteAllText(htmlFile, - html.Replace("site.js", $"site.min.js?_t={DateTimeOffset.Now.ToUnixTimeSeconds()}")); - } - } - - logger.LogInformation("处理 js 引用完成"); - } - - if (!string.IsNullOrEmpty(Configuration["BasePath"])) - { - app.UsePathBase(Configuration["BasePath"]); - } - - app.UseHealthChecks("/healthz"); - app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax }); - app.UseFileServer(); - app.UseRouting(); - app.UseCors("cors"); - app.UseMiddleware(Configuration); - app.UseIdentityServer(); - app.UseAuthorization(); - var inDapr = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DAPR_HTTP_PORT")); - if (inDapr) - { - app.UseCloudEvents(); - } - - app.UseEndpoints(endpoints => - { - if (inDapr) - { - endpoints.MapSubscribeHandler(); - } - - endpoints.MapControllers().RequireCors("cors"); - }); - } - - // private string[] GetCorsOrigins() - // { - // return Configuration.GetSection("AllowedCorsOrigins").Get() ?? Array.Empty(); - // } - - private void ConfigureIdentity(IServiceCollection services) - { - var identityBuilder = services.AddIdentity(); - identityBuilder.AddDefaultTokenProviders() - .AddErrorDescriber(); - - if (Configuration.GetDatabaseType() == "MySql") - { - identityBuilder.AddEntityFrameworkStores(); - } - else - { - identityBuilder.AddEntityFrameworkStores(); - } - } - - private void ConfigureIdentityServer(IServiceCollection services) - { - // IdentityServer4.Endpoints.TokenEndpoint - var builder = services.AddIdentityServer(options => - { - options.Events.RaiseErrorEvents = true; - options.Events.RaiseInformationEvents = true; - options.Events.RaiseFailureEvents = true; - options.Events.RaiseSuccessEvents = true; - - // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html - options.EmitStaticAudienceClaim = true; - }) - .AddExtensionGrantValidator() - .AddStore(Configuration) - .AddAspNetIdentity() - .AddProfileService() - .AddResourceOwnerValidator(); - - services.AddScoped(); - - if (Configuration.GetDatabaseType() == "MySql") - { - builder.AddOperationalStore(); - } - else if (Configuration.GetDatabaseType() == "Postgre") - { - builder.AddOperationalStore(); - } - else - { - throw new NotSupportedException("不支持的数据库类型"); - } - - // not recommended for production - you need to store your key material somewhere secure - builder.AddDeveloperSigningCredential(); - } - - private void ConfigureDbContext(IServiceCollection services) - { - var connectionString = Configuration.GetConnectionString("Identity"); - if (Configuration.GetDatabaseType() == "MySql") - { - services.AddDbContextPool(b => - { - b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), - o => - { - o.MigrationsAssembly(GetType().GetTypeInfo().Assembly.GetName().Name); - o.MigrationsHistoryTable("___identity_migrations_history"); - }); - }); - services.AddDbContext(b => - { - b.UseMySql(Configuration.GetConnectionString("IdentityServer"), - ServerVersion.AutoDetect(connectionString), - o => - { - o.MigrationsAssembly(GetType().GetTypeInfo().Assembly.GetName().Name); - o.MigrationsHistoryTable("identity_server_migrations_history"); - }); - }); - } - else - { - services.AddDbContextPool(b => - { - b.UseNpgsql(Configuration.GetConnectionString("Identity"), - o => - { - o.MigrationsAssembly(GetType().GetTypeInfo().Assembly.GetName().Name); - o.MigrationsHistoryTable("___identity_migrations_history"); - }); - }); - services.AddDbContext(b => - { - b.UseNpgsql(Configuration.GetConnectionString("IdentityServer"), - o => - { - o.MigrationsAssembly(GetType().GetTypeInfo().Assembly.GetName().Name); - o.MigrationsHistoryTable("identity_server_migrations_history"); - }); - }); - } - } - - private void ConfigureOptions(IServiceCollection services) - { - var identity = Configuration.GetSection("Identity"); - services.Configure(Configuration); - services.Configure(identity); - services.Configure(identity); - services.Configure(Configuration.GetSection("IdentityServer")); - services.Configure(Configuration.GetSection("IdentityServer")); - services.Configure(Configuration.GetSection("SecurityTokenService")); - - services.Configure(IdentityConstants.ApplicationScheme, - Configuration.GetSection("ApplicationCookieAuthentication")); - services.Configure(IdentityConstants.ExternalScheme, - Configuration.GetSection("ExternalCookieAuthentication")); - services.Configure(IdentityConstants.TwoFactorUserIdScheme, - Configuration.GetSection("TwoFactorUserIdCookieAuthentication")); - - services.Configure(IdentityServerConstants.DefaultCookieAuthenticationScheme, - Configuration.GetSection("IdentityServerCookieAuthentication")); - services.Configure(IdentityServerConstants.ExternalCookieAuthenticationScheme, - Configuration.GetSection("IdentityServerExternalCookieAuthentication")); - services.Configure(IdentityServerConstants.DefaultCheckSessionCookieName, - Configuration.GetSection("IdentityServerCheckSessionCookieAuthentication")); - services.Configure(Configuration.GetSection("Aliyun")); - } - } -} +// using System; +// using System.IO; +// using Microsoft.AspNetCore.Builder; +// using Microsoft.AspNetCore.Hosting; +// using Microsoft.AspNetCore.Http; +// using Microsoft.Extensions.Configuration; +// using Microsoft.Extensions.DependencyInjection; +// using Microsoft.Extensions.Hosting; +// using Microsoft.Extensions.Logging; +// using SecurityTokenService.Identity; +// using SecurityTokenService.IdentityServer; +// +// namespace SecurityTokenService; +// +// public class Startup +// { +// public Startup(IConfiguration configuration) +// { +// Configuration = configuration; +// } +// +// public IConfiguration Configuration { get; } +// +// // This method gets called by the runtime. Use this method to add services to the container. +// public void ConfigureServices(IServiceCollection services) +// { +// // var keysFolder = new DirectoryInfo("KEYS"); +// // if (!keysFolder.Exists) +// // { +// // keysFolder.Create(); +// // } +// // comments by lewis at 20240222 +// // 必须是 128、256 位 +// +// // var dataProtectionKey = Configuration["DataProtection:Key"]; +// // if (!string.IsNullOrEmpty(dataProtectionKey)) +// // { +// // Util.DataProtectionKeyAes.Key = Encoding.UTF8.GetBytes(dataProtectionKey); +// // } +// +// // var builder = services.AddControllers(); +// // if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DAPR_HTTP_PORT"))) +// // { +// // builder.AddDapr(); +// // } +// +// // // 影响隐私数据加密、AntiToken 加解密 +// // var dataProtectionBuilder = services.AddDataProtection() +// // .SetApplicationName("SecurityTokenService") +// // .SetDefaultKeyLifetime(TimeSpan.FromDays(90)); +// +// // if (Configuration.GetDatabaseType() == "MySql") +// // { +// // dataProtectionBuilder.PersistKeysToDbContext(); +// // } +// // else +// // { +// // dataProtectionBuilder.PersistKeysToDbContext(); +// // } +// +// // services.AddRouting(options => options.LowercaseUrls = true); +// // services.AddHealthChecks(); +// // services.AddScoped(); +// // +// // services.AddCors(option => option +// // .AddPolicy("cors", policy => +// // policy.AllowAnyMethod() +// // .SetIsOriginAllowed(_ => true) +// // .AllowAnyHeader() +// // .AllowCredentials() +// // )); +// +// // // 注册短信平台 +// // switch (Configuration["SecurityTokenService:SmsProvider"]) +// // { +// // case "TencentCloud": +// // services.AddTransient(); +// // break; +// // default: +// // services.AddTransient(); +// // break; +// // } +// +// // ConfigureDbContext(services); +// // ConfigureIdentity(services); +// // ConfigureIdentityServer(services); +// // ConfigureOptions(services); +// } +// +// // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. +// public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +// { +// var logger = app.ApplicationServices.GetRequiredService().CreateLogger("Startup"); +// IdentitySeedData.Load(app); +// +// app.MigrateIdentityServer(); +// +// if (env.IsDevelopment()) +// { +// app.UseDeveloperExceptionPage(); +// } +// else +// { +// // app.UseExceptionHandler("/Error"); +// var htmlFiles = Directory.GetFiles("wwwroot", "*.html"); +// foreach (var htmlFile in htmlFiles) +// { +// var html = File.ReadAllText(htmlFile); +// if (html.Contains("site.js")) +// { +// File.WriteAllText(htmlFile, +// html.Replace("site.js", $"site.min.js?_t={DateTimeOffset.Now.ToUnixTimeSeconds()}")); +// } +// } +// +// logger.LogInformation("处理 js 引用完成"); +// } +// +// if (!string.IsNullOrEmpty(Configuration["BasePath"])) +// { +// app.UsePathBase(Configuration["BasePath"]); +// } +// +// app.UseHealthChecks("/healthz"); +// app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax }); +// app.UseFileServer(); +// app.UseRouting(); +// app.UseCors("cors"); +// app.UseMiddleware(Configuration); +// app.UseIdentityServer(); +// app.UseAuthorization(); +// var inDapr = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DAPR_HTTP_PORT")); +// if (inDapr) +// { +// app.UseCloudEvents(); +// } +// +// app.UseEndpoints(endpoints => +// { +// if (inDapr) +// { +// endpoints.MapSubscribeHandler(); +// } +// +// endpoints.MapControllers().RequireCors("cors"); +// }); +// } +// +// // private string[] GetCorsOrigins() +// // { +// // return Configuration.GetSection("AllowedCorsOrigins").Get() ?? Array.Empty(); +// // } +// } diff --git a/src/SecurityTokenService/Util.cs b/src/SecurityTokenService/Util.cs index db1dda4..79c6a99 100644 --- a/src/SecurityTokenService/Util.cs +++ b/src/SecurityTokenService/Util.cs @@ -6,7 +6,7 @@ namespace SecurityTokenService; public static class Util { - public static readonly Aes DataProtectionKeyAes = Aes.Create(); + public static Aes DataProtectionKeyAes; public static string Encrypt(Aes aes, string v) { diff --git a/src/SecurityTokenService/WebApplicationBuilderExtensions.cs b/src/SecurityTokenService/WebApplicationBuilderExtensions.cs new file mode 100644 index 0000000..9cac9fc --- /dev/null +++ b/src/SecurityTokenService/WebApplicationBuilderExtensions.cs @@ -0,0 +1,252 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Intrinsics.Arm; +using System.Text; +using IdentityServer4; +using IdentityServer4.Configuration; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using SecurityTokenService.Data.MySql; +using SecurityTokenService.Data.PostgreSql; +using SecurityTokenService.Extensions; +using SecurityTokenService.Identity; +using SecurityTokenService.IdentityServer; +using SecurityTokenService.Options; +using SecurityTokenService.Sms; +using SecurityTokenService.Stores; +using Serilog; +using Serilog.Events; + +namespace SecurityTokenService; + +public static class WebApplicationBuilderExtensions +{ + public static WebApplicationBuilder AddSerilog(this WebApplicationBuilder builder) + { + var serilogSection = builder.Configuration.GetSection("Serilog"); + if (serilogSection.GetChildren().Any()) + { + Log.Logger = new LoggerConfiguration().ReadFrom + .Configuration(builder.Configuration) + .CreateLogger(); + } + else + { + var logPath = builder.Configuration["LOG_PATH"] ?? builder.Configuration["LOGPATH"]; + if (string.IsNullOrEmpty(logPath)) + { + logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs/log.txt"); + } + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Async(x => x.File(logPath, rollingInterval: RollingInterval.Day)) + .CreateLogger(); + } + + return builder; + } + + public static WebApplicationBuilder AddDbContext(this WebApplicationBuilder builder) + { + var connectionString = builder.Configuration.GetConnectionString("Identity"); + if (builder.Configuration.GetDatabaseType() == "MySql") + { + builder.Services.AddDbContextPool(b => + { + b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), + o => + { + o.MigrationsAssembly(typeof(WebApplicationBuilderExtensions).GetTypeInfo().Assembly.GetName() + .Name); + o.MigrationsHistoryTable("___identity_migrations_history"); + }); + }); + builder.Services.AddDbContext(b => + { + b.UseMySql(builder.Configuration.GetConnectionString("IdentityServer"), + ServerVersion.AutoDetect(connectionString), + o => + { + o.MigrationsAssembly(typeof(WebApplicationBuilderExtensions).GetTypeInfo().Assembly.GetName() + .Name); + o.MigrationsHistoryTable("identity_server_migrations_history"); + }); + }); + } + else + { + builder.Services.AddDbContextPool(b => + { + b.UseNpgsql(builder.Configuration.GetConnectionString("Identity"), + o => + { + o.MigrationsAssembly(typeof(WebApplicationBuilderExtensions).GetTypeInfo().Assembly.GetName() + .Name); + o.MigrationsHistoryTable("___identity_migrations_history"); + }); + }); + builder.Services.AddDbContext(b => + { + b.UseNpgsql(builder.Configuration.GetConnectionString("IdentityServer"), + o => + { + o.MigrationsAssembly(typeof(WebApplicationBuilderExtensions).GetTypeInfo().Assembly.GetName() + .Name); + o.MigrationsHistoryTable("identity_server_migrations_history"); + }); + }); + } + + return builder; + } + + public static WebApplicationBuilder AddDataProtection(this WebApplicationBuilder builder) + { + var dataProtectionKey = builder.Configuration["DataProtection:Key"]; + if (!string.IsNullOrEmpty(dataProtectionKey)) + { + Util.DataProtectionKeyAes = System.Security.Cryptography.Aes.Create(); + var key = Encoding.UTF8.GetBytes(dataProtectionKey); + if (Util.DataProtectionKeyAes.ValidKeySize(key.Length)) + { + Util.DataProtectionKeyAes.Key = key; + } + else + { + Log.Logger.Error("DataProtectionKey 长度不正确"); + Environment.Exit(-1); + } + } + + // 影响隐私数据加密、AntiToken 加解密 + var dataProtectionBuilder = builder.Services.AddDataProtection() + .SetApplicationName("SecurityTokenService") + .SetDefaultKeyLifetime(TimeSpan.FromDays(90)); + if (builder.Configuration.GetDatabaseType() == "MySql") + { + dataProtectionBuilder.PersistKeysToDbContext(); + } + else + { + dataProtectionBuilder.PersistKeysToDbContext(); + } + + return builder; + } + + public static WebApplicationBuilder AddSmsSender(this WebApplicationBuilder builder) + { + // 注册短信平台 + switch (builder.Configuration["SecurityTokenService:SmsProvider"]) + { + case "TencentCloud": + builder.Services.AddTransient(); + break; + default: + builder.Services.AddTransient(); + break; + } + + return builder; + } + + public static WebApplicationBuilder AddIdentity(this WebApplicationBuilder builder) + { + var identityBuilder = builder.Services.AddIdentity(); + identityBuilder.AddDefaultTokenProviders() + .AddErrorDescriber(); + + if (builder.Configuration.GetDatabaseType() == "MySql") + { + identityBuilder.AddEntityFrameworkStores(); + } + else + { + identityBuilder.AddEntityFrameworkStores(); + } + + return builder; + } + + public static WebApplicationBuilder AddIdentityServer(this WebApplicationBuilder builder) + { + // IdentityServer4.Endpoints.TokenEndpoint + var identityServerBuilder = builder.Services.AddIdentityServer(options => + { + options.Events.RaiseErrorEvents = true; + options.Events.RaiseInformationEvents = true; + options.Events.RaiseFailureEvents = true; + options.Events.RaiseSuccessEvents = true; + + // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html + options.EmitStaticAudienceClaim = true; + }) + .AddExtensionGrantValidator() + .AddStore(builder.Configuration) + .AddAspNetIdentity() + .AddProfileService() + .AddResourceOwnerValidator(); + + identityServerBuilder.Services.AddScoped(); + + if (builder.Configuration.GetDatabaseType() == "MySql") + { + identityServerBuilder.AddOperationalStore(); + } + else if (builder.Configuration.GetDatabaseType() == "Postgre") + { + identityServerBuilder.AddOperationalStore(); + } + else + { + throw new NotSupportedException("不支持的数据库类型"); + } + + // not recommended for production - you need to store your key material somewhere secure + identityServerBuilder.AddDeveloperSigningCredential(); + + return builder; + } + + public static WebApplicationBuilder ConfigureOptions(this WebApplicationBuilder builder) + { + var identity = builder.Configuration.GetSection("Identity"); + builder.Services.Configure(builder.Configuration); + builder.Services.Configure(identity); + builder.Services.Configure(identity); + builder.Services.Configure(builder.Configuration.GetSection("IdentityServer")); + builder.Services.Configure(builder.Configuration.GetSection("IdentityServer")); + builder.Services.Configure( + builder.Configuration.GetSection("SecurityTokenService")); + + builder.Services.Configure(IdentityConstants.ApplicationScheme, + builder.Configuration.GetSection("ApplicationCookieAuthentication")); + builder.Services.Configure(IdentityConstants.ExternalScheme, + builder.Configuration.GetSection("ExternalCookieAuthentication")); + builder.Services.Configure(IdentityConstants.TwoFactorUserIdScheme, + builder.Configuration.GetSection("TwoFactorUserIdCookieAuthentication")); + + builder.Services.Configure( + IdentityServerConstants.DefaultCookieAuthenticationScheme, + builder.Configuration.GetSection("IdentityServerCookieAuthentication")); + builder.Services.Configure( + IdentityServerConstants.ExternalCookieAuthenticationScheme, + builder.Configuration.GetSection("IdentityServerExternalCookieAuthentication")); + builder.Services.Configure(IdentityServerConstants.DefaultCheckSessionCookieName, + builder.Configuration.GetSection("IdentityServerCheckSessionCookieAuthentication")); + builder.Services.Configure(builder.Configuration.GetSection("Aliyun")); + return builder; + } +}