diff --git a/apps/vben5/apps/app-antd/.env.production b/apps/vben5/apps/app-antd/.env.production index 5375847a6..40c957808 100644 --- a/apps/vben5/apps/app-antd/.env.production +++ b/apps/vben5/apps/app-antd/.env.production @@ -10,10 +10,23 @@ VITE_COMPRESS=none VITE_PWA=false # vue-router 的模式 -VITE_ROUTER_HISTORY=hash +# oauth2.0协议要求回调必须是完整的url +# VITE_ROUTER_HISTORY=hash # 是否注入全局loading VITE_INJECT_APP_LOADING=true # 打包后是否生成dist.zip VITE_ARCHIVER=true + +# 是否仅允许OIDC登录 +VITE_GLOB_ONLY_OIDC=false + +# 认证服务器 +VITE_GLOB_AUTHORITY="http://127.0.0.1:30001" + +# 授权范围 +VITE_GLOB_AUDIENCE="openid email address phone profile offline_access lingyun-abp-application" + +# 客户端Id +VITE_GLOB_CLIENT_ID=vue-oauth-client diff --git a/apps/vben5/apps/app-antd/src/auth/authService.ts b/apps/vben5/apps/app-antd/src/auth/authService.ts index 110620bcd..aaeaa626b 100644 --- a/apps/vben5/apps/app-antd/src/auth/authService.ts +++ b/apps/vben5/apps/app-antd/src/auth/authService.ts @@ -2,10 +2,8 @@ import { useAppConfig } from '@vben/hooks'; import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; -const { authority, audience, clientId, clientSecret } = useAppConfig( - import.meta.env, - import.meta.env.PROD, -); +const { authority, audience, clientId, clientSecret, disablePKCE } = + useAppConfig(import.meta.env, import.meta.env.PROD); const userManager = new UserManager({ authority, @@ -19,6 +17,7 @@ const userManager = new UserManager({ automaticSilentRenew: true, loadUserInfo: true, userStore: new WebStorageStateStore({ store: window.localStorage }), + disablePKCE, }); export default { diff --git a/apps/vben5/packages/effects/hooks/src/use-app-config.ts b/apps/vben5/packages/effects/hooks/src/use-app-config.ts index 5dd2ccba2..5d2a1ae5d 100644 --- a/apps/vben5/packages/effects/hooks/src/use-app-config.ts +++ b/apps/vben5/packages/effects/hooks/src/use-app-config.ts @@ -22,6 +22,7 @@ export function useAppConfig( VITE_GLOB_CLIENT_ID, VITE_GLOB_CLIENT_SECRET, VITE_GLOB_ONLY_OIDC, + VITE_GLOB_DISABLE_PKCE, VITE_GLOB_UI_FRAMEWORK, } = config; @@ -32,6 +33,7 @@ export function useAppConfig( clientId: VITE_GLOB_CLIENT_ID, clientSecret: VITE_GLOB_CLIENT_SECRET, onlyOidc: VITE_GLOB_ONLY_OIDC === 'true', + disablePKCE: VITE_GLOB_DISABLE_PKCE === 'true', uiFramework: VITE_GLOB_UI_FRAMEWORK, }; } diff --git a/apps/vben5/packages/types/global.d.ts b/apps/vben5/packages/types/global.d.ts index 7390a53fa..1effbc506 100644 --- a/apps/vben5/packages/types/global.d.ts +++ b/apps/vben5/packages/types/global.d.ts @@ -14,6 +14,7 @@ export interface VbenAdminProAppConfigRaw { VITE_GLOB_AUTHORITY: string; VITE_GLOB_AUDIENCE?: string; VITE_GLOB_ONLY_OIDC?: string; + VITE_GLOB_DISABLE_PKCE?: string; VITE_GLOB_UI_FRAMEWORK: string; } @@ -24,6 +25,7 @@ export interface ApplicationConfig { clientId: string; clientSecret: string; onlyOidc?: boolean; + disablePKCE?: boolean; uiFramework: string; } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/TwoFactorSupportedLoginModel.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/TwoFactorSupportedLoginModel.cs index 29a46ffd6..a10306ef5 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/TwoFactorSupportedLoginModel.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/TwoFactorSupportedLoginModel.cs @@ -1,14 +1,23 @@ -using IdentityServer4.Services; +using IdentityServer4.Events; +using IdentityServer4.Models; +using IdentityServer4.Services; using IdentityServer4.Stores; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; +using Volo.Abp.Account.Settings; using Volo.Abp.Account.Web; using Volo.Abp.Account.Web.Pages.Account; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; +using Volo.Abp.IdentityServer.AspNetIdentity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Settings; using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions; namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account @@ -17,20 +26,161 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account /// 重写登录模型,实现双因素登录 /// [Dependency(ReplaceServices = true)] - [ExposeServices(typeof(LoginModel), typeof(IdentityServerSupportedLoginModel))] - public class TwoFactorSupportedLoginModel : IdentityServerSupportedLoginModel + [ExposeServices( + typeof(LINGYUN.Abp.Account.Web.Pages.Account.LoginModel), + typeof(IdentityServerSupportedLoginModel))] + public class TwoFactorSupportedLoginModel : LINGYUN.Abp.Account.Web.Pages.Account.LoginModel { + protected IIdentityServerInteractionService Interaction { get; } + protected IEventService IdentityServerEvents { get; } + protected IClientStore ClientStore { get; } public TwoFactorSupportedLoginModel( - IAuthenticationSchemeProvider schemeProvider, - IOptions accountOptions, + IAuthenticationSchemeProvider schemeProvider, + IOptions accountOptions, IOptions identityOptions, - IIdentityServerInteractionService interaction, IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache, - IClientStore clientStore, - IEventService identityServerEvents) - : base(schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache, interaction, clientStore, identityServerEvents) + IIdentityServerInteractionService interaction, + IEventService identityServerEvents, + IClientStore clientStore) + : base(schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache) + { + Interaction = interaction; + ClientStore = clientStore; + IdentityServerEvents = identityServerEvents; + } + + public override async Task OnGetAsync() { + LoginInput = new LoginInputModel(); + + var context = await Interaction.GetAuthorizationContextAsync(ReturnUrl); + + if (context != null) + { + // TODO: Find a proper cancel way. + // ShowCancelButton = true; + + LoginInput.UserNameOrEmailAddress = context.LoginHint; + + //TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key! + var tenant = context.Parameters[TenantResolverConsts.DefaultTenantKey]; + if (!string.IsNullOrEmpty(tenant)) + { + CurrentTenant.Change(Guid.Parse(tenant)); + Response.Cookies.Append(TenantResolverConsts.DefaultTenantKey, tenant); + } + } + + if (context?.IdP != null) + { + LoginInput.UserNameOrEmailAddress = context.LoginHint; + ExternalProviders = new[] { new ExternalProviderModel { AuthenticationScheme = context.IdP } }; + return Page(); + } + + var providers = await GetExternalProviders(); + ExternalProviders = providers.ToList(); + + EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); + + if (context?.Client?.ClientId != null) + { + var client = await ClientStore.FindEnabledClientByIdAsync(context?.Client?.ClientId); + if (client != null) + { + EnableLocalLogin = client.EnableLocalLogin; + + if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) + { + providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); + } + } + } + + if (IsExternalLoginOnly) + { + return await base.OnPostExternalLogin(providers.First().AuthenticationScheme); + } + + return Page(); + } + + public override async Task OnPostAsync(string action) + { + var context = await Interaction.GetAuthorizationContextAsync(ReturnUrl); + if (action == "Cancel") + { + if (context == null) + { + return Redirect("~/"); + } + + await Interaction.GrantConsentAsync(context, new ConsentResponse() + { + Error = AuthorizationError.AccessDenied + }); + + return Redirect(ReturnUrl); + } + + await CheckLocalLoginAsync(); + + ValidateModel(); + + await IdentityOptions.SetAsync(); + + ExternalProviders = await GetExternalProviders(); + + EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); + + await ReplaceEmailToUsernameOfInputIfNeeds(); + + var result = await SignInManager.PasswordSignInAsync( + LoginInput.UserNameOrEmailAddress, + LoginInput.Password, + LoginInput.RememberMe, + true + ); + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = IdentitySecurityLogIdentityConsts.Identity, + Action = result.ToIdentitySecurityLogAction(), + UserName = LoginInput.UserNameOrEmailAddress, + ClientId = context?.Client?.ClientId + }); + + if (result.RequiresTwoFactor) + { + return await TwoFactorLoginResultAsync(); + } + if (result.IsLockedOut) + { + return await HandleUserLockedOut(); + } + + if (result.IsNotAllowed) + { + return await HandleUserNotAllowed(); + } + + if (!result.Succeeded) + { + return await HandleUserNameOrPasswordInvalid(); + } + + //TODO: Find a way of getting user's id from the logged in user and do not query it again like that! + var user = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ?? + await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + + Debug.Assert(user != null, nameof(user) + " != null"); + await IdentityServerEvents.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id.ToString(), user.UserName)); //TODO: Use user's name once implemented + + // Clear the dynamic claims cache. + await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); + + return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); } protected async override Task> GetExternalProviders() @@ -53,16 +203,5 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account return providers; } - - protected override Task TwoFactorLoginResultAsync() - { - // 重定向双因素认证页面 - return Task.FromResult(RedirectToPage("SendCode", new - { - returnUrl = ReturnUrl, - returnUrlHash = ReturnUrlHash, - rememberMe = LoginInput.RememberMe - })); - } } }