committed by
GitHub
184 changed files with 6084 additions and 2603 deletions
@ -0,0 +1,53 @@ |
|||
import { useAppConfig } from '@vben/hooks'; |
|||
|
|||
import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; |
|||
|
|||
const { authority, audience, clientId, clientSecret, disablePKCE } = |
|||
useAppConfig(import.meta.env, import.meta.env.PROD); |
|||
|
|||
const userManager = new UserManager({ |
|||
authority, |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
redirect_uri: `${window.location.origin}/signin-callback`, |
|||
response_type: 'code', |
|||
scope: audience, |
|||
post_logout_redirect_uri: `${window.location.origin}/`, |
|||
silent_redirect_uri: `${window.location.origin}/silent-renew.html`, |
|||
automaticSilentRenew: true, |
|||
loadUserInfo: true, |
|||
userStore: new WebStorageStateStore({ store: window.localStorage }), |
|||
disablePKCE, |
|||
}); |
|||
|
|||
export default { |
|||
async login() { |
|||
return userManager.signinRedirect(); |
|||
}, |
|||
|
|||
async logout() { |
|||
return userManager.signoutRedirect(); |
|||
}, |
|||
|
|||
async refreshToken() { |
|||
return userManager.signinSilent(); |
|||
}, |
|||
|
|||
async getAccessToken() { |
|||
const user = await userManager.getUser(); |
|||
return user?.access_token; |
|||
}, |
|||
|
|||
async isAuthenticated() { |
|||
const user = await userManager.getUser(); |
|||
return !!user && !user.expired; |
|||
}, |
|||
|
|||
async handleCallback() { |
|||
return userManager.signinRedirectCallback(); |
|||
}, |
|||
|
|||
async getUser() { |
|||
return userManager.getUser(); |
|||
}, |
|||
}; |
|||
@ -0,0 +1,120 @@ |
|||
<script setup lang="ts"> |
|||
import { useVbenForm, useVbenModal, z } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { usePasswordValidator } from '@abp/identity'; |
|||
|
|||
import { useAuthStore } from '#/store/auth'; |
|||
|
|||
interface FormModel { |
|||
currentPassword: string; |
|||
newPassword: string; |
|||
newPasswordConfirm: string; |
|||
} |
|||
|
|||
interface ModalState { |
|||
changePasswordToken: string; |
|||
password: string; |
|||
username: string; |
|||
userId: string; |
|||
} |
|||
|
|||
const authStore = useAuthStore(); |
|||
const { validate } = usePasswordValidator(); |
|||
|
|||
const [Form, formApi] = useVbenForm({ |
|||
schema: [ |
|||
{ |
|||
component: 'InputPassword', |
|||
fieldName: 'password', |
|||
label: $t('AbpAccount.DisplayName:CurrentPassword'), |
|||
rules: 'required', |
|||
}, |
|||
{ |
|||
component: 'InputPassword', |
|||
fieldName: 'newPassword', |
|||
label: $t('AbpAccount.DisplayName:NewPassword'), |
|||
rules: z |
|||
.string() |
|||
.superRefine(async (newPassword, ctx) => { |
|||
try { |
|||
await validate(newPassword); |
|||
} catch (error) { |
|||
ctx.addIssue({ |
|||
code: z.ZodIssueCode.custom, |
|||
message: String(error), |
|||
}); |
|||
} |
|||
}) |
|||
.refine( |
|||
async (newPassword) => { |
|||
const input = (await formApi.getValues()) as FormModel; |
|||
return input.currentPassword !== newPassword; |
|||
}, |
|||
{ |
|||
message: $t('AbpAccount.NewPasswordSameAsOld'), |
|||
}, |
|||
) |
|||
.refine( |
|||
async (newPassword) => { |
|||
const input = (await formApi.getValues()) as FormModel; |
|||
return input.newPasswordConfirm === newPassword; |
|||
}, |
|||
{ |
|||
message: $t( |
|||
'AbpIdentity.Volo_Abp_Identity:PasswordConfirmationFailed', |
|||
), |
|||
}, |
|||
), |
|||
}, |
|||
{ |
|||
component: 'InputPassword', |
|||
fieldName: 'newPasswordConfirm', |
|||
label: $t('AbpAccount.DisplayName:NewPasswordConfirm'), |
|||
rules: z.string().refine( |
|||
async (newPasswordConfirm) => { |
|||
const input = (await formApi.getValues()) as FormModel; |
|||
return input.newPassword === newPasswordConfirm; |
|||
}, |
|||
{ |
|||
message: $t( |
|||
'AbpIdentity.Volo_Abp_Identity:PasswordConfirmationFailed', |
|||
), |
|||
}, |
|||
), |
|||
}, |
|||
], |
|||
showDefaultActions: false, |
|||
handleSubmit: onSubmit, |
|||
}); |
|||
const [Modal, modalApi] = useVbenModal({ |
|||
draggable: true, |
|||
closeOnClickModal: false, |
|||
closeOnPressEscape: false, |
|||
async onConfirm() { |
|||
await formApi.validateAndSubmitForm(); |
|||
}, |
|||
}); |
|||
async function onSubmit(values: Record<string, any>) { |
|||
modalApi.setState({ submitting: true }); |
|||
try { |
|||
const state = modalApi.getData<ModalState>(); |
|||
await authStore.authLogin({ |
|||
username: state.username, |
|||
password: state.password, |
|||
NewPassword: values.newPassword, |
|||
ChangePasswordToken: state.changePasswordToken, |
|||
}); |
|||
} finally { |
|||
modalApi.setState({ submitting: false }); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Modal :title="$t('AbpAccount.ResetMyPassword')"> |
|||
<Form /> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,15 @@ |
|||
<script lang="ts" setup> |
|||
import { onMounted } from 'vue'; |
|||
|
|||
import { useAuthStore } from '#/store/auth'; |
|||
|
|||
const authStore = useAuthStore(); |
|||
|
|||
onMounted(async () => { |
|||
await authStore.oidcCallback(); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div>{{ $t('page.auth.processingLogin') }}</div> |
|||
</template> |
|||
@ -0,0 +1,46 @@ |
|||
import type { OAuthTokenResult, PhoneNumberTokenRequest } from '../types/token'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function usePhoneLoginApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 手机验证码登录 |
|||
* @param input 登录参数 |
|||
* @returns 用户token |
|||
*/ |
|||
async function loginApi(input: PhoneNumberTokenRequest) { |
|||
const { audience, clientId, clientSecret } = useAppConfig( |
|||
import.meta.env, |
|||
import.meta.env.PROD, |
|||
); |
|||
const result = await request<OAuthTokenResult>('/connect/token', { |
|||
data: { |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
grant_type: 'phone_verify', |
|||
phone_number: input.phoneNumber, |
|||
phone_verify_code: input.code, |
|||
scope: audience, |
|||
}, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
method: 'POST', |
|||
}); |
|||
return { |
|||
accessToken: result.access_token, |
|||
expiresIn: result.expires_in, |
|||
refreshToken: result.refresh_token, |
|||
tokenType: result.token_type, |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
loginApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
using Microsoft.IdentityModel.Logging; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using Volo.Abp.Text.Formatting; |
|||
|
|||
namespace Microsoft.IdentityModel.Tokens; |
|||
|
|||
/// <summary>
|
|||
/// Copy from: https://github.com/maliming/Owl.TokenWildcardIssuerValidator
|
|||
/// https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Tokens/Validators.cs#L207
|
|||
/// </summary>
|
|||
public static class TokenWildcardIssuerValidator |
|||
{ |
|||
private const string IDX10204 = "IDX10204: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace AND validationParameters.ValidIssuers is null."; |
|||
private const string IDX10205 = "IDX10205: Issuer validation failed. Issuer: '{0}'. Did not match: validationParameters.ValidIssuer: '{1}' or validationParameters.ValidIssuers: '{2}'."; |
|||
private const string IDX10211 = "IDX10211: Unable to validate issuer. The 'issuer' parameter is null or whitespace"; |
|||
private const string IDX10235 = "IDX10235: ValidateIssuer property on ValidationParameters is set to false. Exiting without validating the issuer."; |
|||
private const string IDX10236 = "IDX10236: Issuer Validated.Issuer: '{0}'"; |
|||
|
|||
public static readonly IssuerValidator IssuerValidator = (issuer, token, validationParameters) => |
|||
{ |
|||
if (validationParameters == null) |
|||
{ |
|||
throw LogHelper.LogArgumentNullException(nameof(validationParameters)); |
|||
} |
|||
|
|||
if (!validationParameters.ValidateIssuer) |
|||
{ |
|||
LogHelper.LogInformation(IDX10235); |
|||
return issuer; |
|||
} |
|||
|
|||
if (string.IsNullOrWhiteSpace(issuer)) |
|||
{ |
|||
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(IDX10211) |
|||
{ |
|||
InvalidIssuer = issuer |
|||
}); |
|||
} |
|||
|
|||
// Throw if all possible places to validate against are null or empty
|
|||
if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer) && |
|||
validationParameters.ValidIssuers == null) |
|||
{ |
|||
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(IDX10204) |
|||
{ |
|||
InvalidIssuer = issuer |
|||
}); |
|||
} |
|||
|
|||
if (string.Equals(validationParameters.ValidIssuer, issuer, StringComparison.Ordinal)) |
|||
{ |
|||
LogHelper.LogInformation(IDX10236, issuer); |
|||
return issuer; |
|||
} |
|||
|
|||
if (!string.IsNullOrWhiteSpace(validationParameters.ValidIssuer)) |
|||
{ |
|||
var extractResult = FormattedStringValueExtracter.Extract(issuer, validationParameters.ValidIssuer, ignoreCase: true); |
|||
if (extractResult.IsMatch && |
|||
extractResult.Matches.Aggregate(validationParameters.ValidIssuer, (current, nameValue) => current.Replace($"{{{nameValue.Name}}}", nameValue.Value)) |
|||
.IndexOf(issuer, StringComparison.OrdinalIgnoreCase) >= 0) |
|||
{ |
|||
return issuer; |
|||
} |
|||
} |
|||
|
|||
if (null != validationParameters.ValidIssuers) |
|||
{ |
|||
foreach (var str in validationParameters.ValidIssuers) |
|||
{ |
|||
if (string.IsNullOrEmpty(str)) |
|||
{ |
|||
LogHelper.LogInformation(IDX10235); |
|||
continue; |
|||
} |
|||
|
|||
if (string.Equals(str, issuer, StringComparison.Ordinal)) |
|||
{ |
|||
LogHelper.LogInformation(IDX10236, issuer); |
|||
return issuer; |
|||
} |
|||
|
|||
var extractResult = FormattedStringValueExtracter.Extract(issuer, str, ignoreCase: true); |
|||
if (extractResult.IsMatch && |
|||
extractResult.Matches.Aggregate(str, (current, nameValue) => current.Replace($"{{{nameValue.Name}}}", nameValue.Value)) |
|||
.IndexOf(issuer, StringComparison.OrdinalIgnoreCase) >= 0) |
|||
{ |
|||
return issuer; |
|||
} |
|||
} |
|||
} |
|||
|
|||
throw LogHelper.LogExceptionMessage( |
|||
new SecurityTokenInvalidIssuerException(LogHelper.FormatInvariant(IDX10205, issuer, |
|||
(validationParameters.ValidIssuer ?? "null"), |
|||
SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers))) |
|||
{ |
|||
InvalidIssuer = issuer |
|||
}); |
|||
}; |
|||
|
|||
private static string SerializeAsSingleCommaDelimitedString(IEnumerable<string> strings) |
|||
{ |
|||
if (strings == null) |
|||
{ |
|||
return Utility.Null; |
|||
} |
|||
|
|||
var sb = new StringBuilder(); |
|||
var first = true; |
|||
foreach (var str in strings) |
|||
{ |
|||
if (first) |
|||
{ |
|||
sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", str ?? Utility.Null); |
|||
first = false; |
|||
} |
|||
else |
|||
{ |
|||
sb.AppendFormat(CultureInfo.InvariantCulture, ", {0}", str ?? Utility.Null); |
|||
} |
|||
} |
|||
|
|||
return first ? Utility.Empty : sb.ToString(); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks> |
|||
<AssemblyName>LINGYUN.Abp.Account.OAuth</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Account.OAuth</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Remove="LINGYUN\Abp\Account\OAuth\Localization\Resources\*.json" /> |
|||
<EmbeddedResource Include="LINGYUN\Abp\Account\OAuth\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Features" /> |
|||
<PackageReference Include="Volo.Abp.Settings" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,28 @@ |
|||
using LINGYUN.Abp.Account.OAuth.Localization; |
|||
using Volo.Abp.Features; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Settings; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.Account.OAuth; |
|||
|
|||
[DependsOn(typeof(AbpFeaturesModule))] |
|||
[DependsOn(typeof(AbpSettingsModule))] |
|||
public class AbpAccountOAuthModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpAccountOAuthModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Add<AccountOAuthResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/Account/OAuth/Localization/Resources"); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
using LINGYUN.Abp.Account.OAuth.Localization; |
|||
using Volo.Abp.Features; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Validation.StringValues; |
|||
|
|||
namespace LINGYUN.Abp.Account.OAuth.Features; |
|||
|
|||
public class AccountOAuthFeatureDefinitionProvider : FeatureDefinitionProvider |
|||
{ |
|||
public override void Define(IFeatureDefinitionContext context) |
|||
{ |
|||
var group = context.AddGroup( |
|||
name: AccountOAuthFeatureNames.GroupName, |
|||
displayName: L("Features:ExternalOAuthLogin")); |
|||
|
|||
group.AddFeature( |
|||
name: AccountOAuthFeatureNames.GitHub.Enable, |
|||
defaultValue: "false", |
|||
displayName: L("Features:GithubOAuthEnable"), |
|||
description: L("Features:GithubOAuthEnableDesc"), |
|||
valueType: new ToggleStringValueType(new BooleanValueValidator())); |
|||
group.AddFeature( |
|||
name: AccountOAuthFeatureNames.QQ.Enable, |
|||
defaultValue: "false", |
|||
displayName: L("Features:QQOAuthEnable"), |
|||
description: L("Features:QQOAuthEnableDesc"), |
|||
valueType: new ToggleStringValueType(new BooleanValueValidator())); |
|||
group.AddFeature( |
|||
name: AccountOAuthFeatureNames.WeChat.Enable, |
|||
defaultValue: "false", |
|||
displayName: L("Features:WeChatOAuthEnable"), |
|||
description: L("Features:WeChatOAuthEnableDesc"), |
|||
valueType: new ToggleStringValueType(new BooleanValueValidator())); |
|||
group.AddFeature( |
|||
name: AccountOAuthFeatureNames.WeCom.Enable, |
|||
defaultValue: "false", |
|||
displayName: L("Features:WeComOAuthEnable"), |
|||
description: L("Features:WeComOAuthEnableDesc"), |
|||
valueType: new ToggleStringValueType(new BooleanValueValidator())); |
|||
group.AddFeature( |
|||
name: AccountOAuthFeatureNames.Bilibili.Enable, |
|||
defaultValue: "false", |
|||
displayName: L("Features:BilibiliOAuthEnable"), |
|||
description: L("Features:BilibiliOAuthEnableDesc"), |
|||
valueType: new ToggleStringValueType(new BooleanValueValidator())); |
|||
} |
|||
|
|||
private static LocalizableString L(string name) |
|||
{ |
|||
return LocalizableString.Create<AccountOAuthResource>(name); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
namespace LINGYUN.Abp.Account.OAuth.Features; |
|||
|
|||
public static class AccountOAuthFeatureNames |
|||
{ |
|||
public const string GroupName = "Abp.Account.OAuth"; |
|||
public static class GitHub |
|||
{ |
|||
public const string Prefix = GroupName + ".GitHub"; |
|||
/// <summary>
|
|||
/// 启用Github认证登录
|
|||
/// </summary>
|
|||
public const string Enable = Prefix + ".Enable"; |
|||
} |
|||
public static class QQ |
|||
{ |
|||
public const string Prefix = GroupName + ".QQ"; |
|||
/// <summary>
|
|||
/// 启用QQ认证登录
|
|||
/// </summary>
|
|||
public const string Enable = Prefix + ".Enable"; |
|||
} |
|||
public static class WeChat |
|||
{ |
|||
public const string Prefix = GroupName + ".WeChat"; |
|||
/// <summary>
|
|||
/// 启用微信认证登录
|
|||
/// </summary>
|
|||
public const string Enable = Prefix + ".Enable"; |
|||
} |
|||
public static class WeCom |
|||
{ |
|||
public const string Prefix = GroupName + ".WeCom"; |
|||
/// <summary>
|
|||
/// 启用企业微信认证登录
|
|||
/// </summary>
|
|||
public const string Enable = Prefix + ".Enable"; |
|||
} |
|||
public static class Bilibili |
|||
{ |
|||
public const string Prefix = GroupName + ".Bilibili"; |
|||
/// <summary>
|
|||
/// 启用Bilibili认证登录
|
|||
/// </summary>
|
|||
public const string Enable = Prefix + ".Enable"; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINGYUN.Abp.Account.OAuth.Localization; |
|||
|
|||
[LocalizationResourceName("AbpAccountOAuth")] |
|||
public class AccountOAuthResource |
|||
{ |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Permission:ExternalOAuthLogin": "External Oauth Login", |
|||
"Features:ExternalOAuthLogin": "External Oauth Login", |
|||
"Features:GithubOAuthEnable": "GitHub", |
|||
"Features:GithubOAuthEnableDesc": "Enable to enable the application to support login via a GitHub account.", |
|||
"Features:QQOAuthEnable": "QQ", |
|||
"Features:QQOAuthEnableDesc": "Enable to enable the application to support login via QQ account.", |
|||
"Features:WeChatOAuthEnable": "WeChat", |
|||
"Features:WeChatOAuthEnableDesc": "Enable to enable the application to support login via the wechat official account.", |
|||
"Features:WeComOAuthEnable": "WeCom", |
|||
"Features:WeComOAuthEnableDesc": "Enable to enable the application to support login via wecom.", |
|||
"Features:BilibiliOAuthEnable": "Bilibili", |
|||
"Features:BilibiliOAuthEnableDesc": "Enable to allow the application to support login via a Bilibili account.", |
|||
"Settings:ExternalOAuthLogin": "External Oauth Login", |
|||
"Settings:GitHubAuth": "GitHub", |
|||
"Settings:GitHubClientId": "Client Id", |
|||
"Settings:GitHubClientIdDesc": "The client ID received from GitHub during registration. for details: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps", |
|||
"Settings:GitHubClientSecret": "Client Secret", |
|||
"Settings:GitHubClientSecretDesc": "The client key of the OAuth application that you received from GitHub. for details: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps", |
|||
"Settings:BilibiliAuth": "Bilibili", |
|||
"Settings:BilibiliClientId": "Client Id", |
|||
"Settings:BilibiliClientIdDesc": "Client Id, for details: https://open.bilibili.com/doc/4/eaf0e2b5-bde9-b9a0-9be1-019bb455701c#h1-u7B80u4ECB", |
|||
"Settings:BilibiliClientSecret": "Client Secret", |
|||
"Settings:BilibiliClientSecretDesc": "Client Secret, for details: https://open.bilibili.com/doc/4/eaf0e2b5-bde9-b9a0-9be1-019bb455701c#h1-u7B80u4ECB", |
|||
"OAuth:Microsoft": "Microsoft", |
|||
"OAuth:Twitter": "Twitter", |
|||
"OAuth:GitHub": "GitHub", |
|||
"OAuth:Google": "Google", |
|||
"OAuth:QQ": "QQ", |
|||
"OAuth:Weixin": "WeChat", |
|||
"OAuth:WorkWeixin": "WeCom", |
|||
"OAuth:Bilibili": "Bilibili" |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"Permission:ExternalOAuthLogin": "外部登录", |
|||
"Features:ExternalOAuthLogin": "外部登录", |
|||
"Features:GithubOAuthEnable": "GitHub认证", |
|||
"Features:GithubOAuthEnableDesc": "启用以使应用程序支持通过GitHub账号登录.", |
|||
"Features:QQOAuthEnable": "QQ认证", |
|||
"Features:QQOAuthEnableDesc": "启用以使应用程序支持通过QQ账号登录.", |
|||
"Features:WeChatOAuthEnable": "微信认证", |
|||
"Features:WeChatOAuthEnableDesc": "启用以使应用程序支持通过微信公众号登录.", |
|||
"Features:WeComOAuthEnable": "企业微信认证", |
|||
"Features:WeComOAuthEnableDesc": "启用以使应用程序支持通过企业微信登录.", |
|||
"Features:BilibiliOAuthEnable": "Bilibili认证", |
|||
"Features:BilibiliOAuthEnableDesc": "启用以使应用程序支持通过Bilibili账号登录.", |
|||
"Settings:ExternalOAuthLogin": "外部登录", |
|||
"Settings:GitHubAuth": "GitHub登录", |
|||
"Settings:GitHubClientId": "Client Id", |
|||
"Settings:GitHubClientIdDesc": "注册时从 GitHub 收到的客户端 ID.详见: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps", |
|||
"Settings:GitHubClientSecret": "Client Secret", |
|||
"Settings:GitHubClientSecretDesc": "您从 GitHub 收到的 OAuth 应用程序的客户端密钥.详见: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps", |
|||
"Settings:BilibiliAuth": "Bilibili登录", |
|||
"Settings:BilibiliClientId": "Client Id", |
|||
"Settings:BilibiliClientIdDesc": "应用Id, 详见: https://open.bilibili.com/doc/4/eaf0e2b5-bde9-b9a0-9be1-019bb455701c#h1-u7B80u4ECB", |
|||
"Settings:BilibiliClientSecret": "Client Secret", |
|||
"Settings:BilibiliClientSecretDesc": "应用密钥, 详见: https://open.bilibili.com/doc/4/eaf0e2b5-bde9-b9a0-9be1-019bb455701c#h1-u7B80u4ECB", |
|||
"OAuth:Microsoft": "Microsoft", |
|||
"OAuth:Twitter": "Twitter", |
|||
"OAuth:GitHub": "GitHub", |
|||
"OAuth:Google": "Google", |
|||
"OAuth:QQ": "QQ", |
|||
"OAuth:Weixin": "微信", |
|||
"OAuth:WorkWeixin": "企业微信", |
|||
"OAuth:Bilibili": "Bilibili" |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
using LINGYUN.Abp.Account.OAuth.Localization; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Account.OAuth.Settings; |
|||
|
|||
public class AccountOAuthSettingDefinitionProvider : SettingDefinitionProvider |
|||
{ |
|||
public override void Define(ISettingDefinitionContext context) |
|||
{ |
|||
context.Add(GetGitHubSettings()); |
|||
context.Add(GetBilibiliSettings()); |
|||
} |
|||
|
|||
private SettingDefinition[] GetGitHubSettings() |
|||
{ |
|||
return new SettingDefinition[] |
|||
{ |
|||
new SettingDefinition( |
|||
AccountOAuthSettingNames.GitHub.ClientId, |
|||
displayName: L("Settings:GitHubClientId"), |
|||
description: L("Settings:GitHubClientIdDesc"), |
|||
isVisibleToClients: false, |
|||
isEncrypted: true) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
AccountOAuthSettingNames.GitHub.ClientSecret, |
|||
displayName: L("Settings:GitHubClientSecret"), |
|||
description: L("Settings:GitHubClientSecretDesc"), |
|||
isVisibleToClients: false, |
|||
isEncrypted: true) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
}; |
|||
} |
|||
|
|||
private SettingDefinition[] GetBilibiliSettings() |
|||
{ |
|||
return new SettingDefinition[] |
|||
{ |
|||
new SettingDefinition( |
|||
AccountOAuthSettingNames.Bilibili.ClientId, |
|||
displayName: L("Settings:BilibiliClientId"), |
|||
description: L("Settings:BilibiliClientIdDesc"), |
|||
isVisibleToClients: false, |
|||
isEncrypted: true) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
AccountOAuthSettingNames.Bilibili.ClientSecret, |
|||
displayName: L("Settings:BilibiliClientSecret"), |
|||
description: L("Settings:BilibiliClientSecretDesc"), |
|||
isVisibleToClients: false, |
|||
isEncrypted: true) |
|||
.WithProviders( |
|||
DefaultValueSettingValueProvider.ProviderName, |
|||
ConfigurationSettingValueProvider.ProviderName, |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
}; |
|||
} |
|||
|
|||
protected ILocalizableString L(string name) |
|||
{ |
|||
return LocalizableString.Create<AccountOAuthResource>(name); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
namespace LINGYUN.Abp.Account.OAuth.Settings; |
|||
|
|||
public static class AccountOAuthSettingNames |
|||
{ |
|||
public const string GroupName = "Abp.Account.OAuth"; |
|||
public static class GitHub |
|||
{ |
|||
public const string Prefix = GroupName + ".GitHub"; |
|||
/// <summary>
|
|||
/// ClientId
|
|||
/// </summary>
|
|||
public const string ClientId = Prefix + ".ClientId"; |
|||
/// <summary>
|
|||
/// ClientSecret
|
|||
/// </summary>
|
|||
public const string ClientSecret = Prefix + ".ClientSecret"; |
|||
} |
|||
public static class Bilibili |
|||
{ |
|||
public const string Prefix = GroupName + ".Bilibili"; |
|||
/// <summary>
|
|||
/// ClientId
|
|||
/// </summary>
|
|||
public const string ClientId = Prefix + ".ClientId"; |
|||
/// <summary>
|
|||
/// ClientSecret
|
|||
/// </summary>
|
|||
public const string ClientSecret = Prefix + ".ClientSecret"; |
|||
} |
|||
} |
|||
@ -0,0 +1,188 @@ |
|||
using IdentityServer4.Events; |
|||
using IdentityServer4.Models; |
|||
using IdentityServer4.Services; |
|||
using IdentityServer4.Stores; |
|||
using LINGYUN.Abp.Account.Web.ExternalProviders; |
|||
using LINGYUN.Abp.Account.Web.Models; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Account.Settings; |
|||
using Volo.Abp.Account.Web; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.IdentityServer.AspNetIdentity; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Settings; |
|||
using static Volo.Abp.Account.Web.Pages.Account.LoginModel; |
|||
using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account |
|||
{ |
|||
/// <summary>
|
|||
/// 重写登录模型,实现双因素登录
|
|||
/// </summary>
|
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices( |
|||
typeof(LINGYUN.Abp.Account.Web.Pages.Account.LoginModel), |
|||
typeof(IdentityServerLoginModel))] |
|||
public class IdentityServerLoginModel : LINGYUN.Abp.Account.Web.Pages.Account.LoginModel |
|||
{ |
|||
protected IIdentityServerInteractionService Interaction { get; } |
|||
protected IEventService IdentityServerEvents { get; } |
|||
protected IClientStore ClientStore { get; } |
|||
public IdentityServerLoginModel( |
|||
IExternalProviderService externalProviderService, |
|||
IAuthenticationSchemeProvider schemeProvider, |
|||
IOptions<AbpAccountOptions> accountOptions, |
|||
IOptions<IdentityOptions> identityOptions, |
|||
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache, |
|||
IIdentityServerInteractionService interaction, |
|||
IEventService identityServerEvents, |
|||
IClientStore clientStore) |
|||
: base(externalProviderService, schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache) |
|||
{ |
|||
Interaction = interaction; |
|||
ClientStore = clientStore; |
|||
IdentityServerEvents = identityServerEvents; |
|||
} |
|||
|
|||
public override async Task<IActionResult> 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 ExternalLoginProviderModel { 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<IActionResult> 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); |
|||
} |
|||
} |
|||
} |
|||
@ -1,68 +0,0 @@ |
|||
using IdentityServer4.Services; |
|||
using IdentityServer4.Stores; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Account.Web; |
|||
using Volo.Abp.Account.Web.Pages.Account; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Identity; |
|||
using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account |
|||
{ |
|||
/// <summary>
|
|||
/// 重写登录模型,实现双因素登录
|
|||
/// </summary>
|
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices(typeof(LoginModel), typeof(IdentityServerSupportedLoginModel))] |
|||
public class TwoFactorSupportedLoginModel : IdentityServerSupportedLoginModel |
|||
{ |
|||
public TwoFactorSupportedLoginModel( |
|||
IAuthenticationSchemeProvider schemeProvider, |
|||
IOptions<AbpAccountOptions> accountOptions, |
|||
IOptions<IdentityOptions> identityOptions, |
|||
IIdentityServerInteractionService interaction, |
|||
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache, |
|||
IClientStore clientStore, |
|||
IEventService identityServerEvents) |
|||
: base(schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache, interaction, clientStore, identityServerEvents) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected async override Task<List<ExternalProviderModel>> GetExternalProviders() |
|||
{ |
|||
var providers = await base.GetExternalProviders(); |
|||
|
|||
foreach (var provider in providers) |
|||
{ |
|||
var localizedDisplayName = L[provider.DisplayName]; |
|||
if (localizedDisplayName.ResourceNotFound) |
|||
{ |
|||
localizedDisplayName = L["AuthenticationScheme:" + provider.DisplayName]; |
|||
} |
|||
|
|||
if (!localizedDisplayName.ResourceNotFound) |
|||
{ |
|||
provider.DisplayName = localizedDisplayName.Value; |
|||
} |
|||
} |
|||
|
|||
return providers; |
|||
} |
|||
|
|||
protected override Task<IActionResult> TwoFactorLoginResultAsync() |
|||
{ |
|||
// 重定向双因素认证页面
|
|||
return Task.FromResult<IActionResult>(RedirectToPage("SendCode", new |
|||
{ |
|||
returnUrl = ReturnUrl, |
|||
returnUrlHash = ReturnUrlHash, |
|||
rememberMe = LoginInput.RememberMe |
|||
})); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,105 @@ |
|||
using AspNet.Security.OAuth.Bilibili; |
|||
using AspNet.Security.OAuth.GitHub; |
|||
using AspNet.Security.OAuth.QQ; |
|||
using AspNet.Security.OAuth.Weixin; |
|||
using AspNet.Security.OAuth.WorkWeixin; |
|||
using LINGYUN.Abp.Account.OAuth; |
|||
using LINGYUN.Abp.Account.OAuth.Localization; |
|||
using LINGYUN.Abp.Account.Web.OAuth.ExternalProviders.Bilibili; |
|||
using LINGYUN.Abp.Account.Web.OAuth.ExternalProviders.GitHub; |
|||
using LINGYUN.Abp.Account.Web.OAuth.ExternalProviders.QQ; |
|||
using LINGYUN.Abp.Account.Web.OAuth.ExternalProviders.WeChat; |
|||
using LINGYUN.Abp.Account.Web.OAuth.ExternalProviders.WeCom; |
|||
using LINGYUN.Abp.Account.Web.OAuth.Microsoft.Extensions.DependencyInjection; |
|||
using LINGYUN.Abp.Tencent.QQ; |
|||
using LINGYUN.Abp.WeChat.Official; |
|||
using LINGYUN.Abp.WeChat.Work; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Account.Localization; |
|||
using Volo.Abp.AspNetCore.Mvc.Localization; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth; |
|||
|
|||
[DependsOn(typeof(AbpAccountWebModule))] |
|||
[DependsOn(typeof(AbpAccountOAuthModule))] |
|||
[DependsOn(typeof(AbpTencentQQModule))] |
|||
[DependsOn(typeof(AbpWeChatOfficialModule))] |
|||
[DependsOn(typeof(AbpWeChatWorkModule))] |
|||
public class AbpAccountWebOAuthModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.PreConfigure<AbpMvcDataAnnotationsLocalizationOptions>(options => |
|||
{ |
|||
options.AddAssemblyResource(typeof(AccountResource), typeof(AbpAccountWebOAuthModule).Assembly); |
|||
}); |
|||
|
|||
PreConfigure<IMvcBuilder>(mvcBuilder => |
|||
{ |
|||
mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpAccountWebOAuthModule).Assembly); |
|||
}); |
|||
} |
|||
|
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpAccountWebOAuthModule>("LINGYUN.Abp.Account.Web.OAuth"); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<AccountResource>() |
|||
.AddBaseTypes(typeof(AccountOAuthResource)); |
|||
}); |
|||
|
|||
context.Services |
|||
.AddAuthentication() |
|||
.AddGitHub(options => |
|||
{ |
|||
options.ClientId = "ClientId"; |
|||
options.ClientSecret = "ClientSecret"; |
|||
|
|||
options.Scope.Add("user:email"); |
|||
}).UseSettingProvider< |
|||
GitHubAuthenticationOptions, |
|||
GitHubAuthenticationHandler, |
|||
GitHubAuthHandlerOptionsProvider>() |
|||
.AddQQ(options => |
|||
{ |
|||
options.ClientId = "ClientId"; |
|||
options.ClientSecret = "ClientSecret"; |
|||
}).UseSettingProvider< |
|||
QQAuthenticationOptions, |
|||
QQAuthenticationHandler, |
|||
QQAuthHandlerOptionsProvider>() |
|||
.AddWeixin(options => |
|||
{ |
|||
options.ClientId = "ClientId"; |
|||
options.ClientSecret = "ClientSecret"; |
|||
}).UseSettingProvider< |
|||
WeixinAuthenticationOptions, |
|||
WeixinAuthenticationHandler, |
|||
WeChatAuthHandlerOptionsProvider>() |
|||
.AddWorkWeixin(options => |
|||
{ |
|||
options.ClientId = "ClientId"; |
|||
options.ClientSecret = "ClientSecret"; |
|||
}).UseSettingProvider< |
|||
WorkWeixinAuthenticationOptions, |
|||
WorkWeixinAuthenticationHandler, |
|||
WeComAuthHandlerOptionsProvider>() |
|||
.AddBilibili(options => |
|||
{ |
|||
options.ClientId = "ClientId"; |
|||
options.ClientSecret = "ClientSecret"; |
|||
}).UseSettingProvider< |
|||
BilibiliAuthenticationOptions, |
|||
BilibiliAuthenticationHandler, |
|||
BilibiliAuthHandlerOptionsProvider>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Http; |
|||
using System; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.ExternalProviders; |
|||
|
|||
public class AccountAuthenticationRequestHandler<TOptions, THandler> : IAuthenticationRequestHandler |
|||
where TOptions : RemoteAuthenticationOptions, new() |
|||
where THandler : RemoteAuthenticationHandler<TOptions> |
|||
{ |
|||
protected THandler InnerHandler { get; } |
|||
protected IOAuthHandlerOptionsProvider<TOptions> OptionsProvider { get; } |
|||
public AccountAuthenticationRequestHandler( |
|||
THandler innerHandler, |
|||
IOAuthHandlerOptionsProvider<TOptions> optionsProvider) |
|||
{ |
|||
InnerHandler = innerHandler; |
|||
OptionsProvider = optionsProvider; |
|||
} |
|||
|
|||
public virtual async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) |
|||
{ |
|||
await InnerHandler.InitializeAsync(scheme, context); |
|||
} |
|||
|
|||
public virtual async Task<AuthenticateResult> AuthenticateAsync() |
|||
{ |
|||
return await InnerHandler.AuthenticateAsync(); |
|||
} |
|||
|
|||
public virtual async Task ChallengeAsync(AuthenticationProperties? properties) |
|||
{ |
|||
await InitializeOptionsAsync(); |
|||
|
|||
await InnerHandler.ChallengeAsync(properties); |
|||
} |
|||
|
|||
public virtual async Task ForbidAsync(AuthenticationProperties? properties) |
|||
{ |
|||
await InnerHandler.ForbidAsync(properties); |
|||
} |
|||
|
|||
public async Task SignOutAsync(AuthenticationProperties properties) |
|||
{ |
|||
if (!(InnerHandler is IAuthenticationSignOutHandler signOutHandler)) |
|||
{ |
|||
throw new InvalidOperationException($"The authentication handler registered for scheme '{InnerHandler.Scheme}' is '{InnerHandler.GetType().Name}' which cannot be used for SignOutAsync"); |
|||
} |
|||
|
|||
await InitializeOptionsAsync(); |
|||
await signOutHandler.SignOutAsync(properties); |
|||
} |
|||
|
|||
public async Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) |
|||
{ |
|||
if (!(InnerHandler is IAuthenticationSignInHandler signInHandler)) |
|||
{ |
|||
throw new InvalidOperationException($"The authentication handler registered for scheme '{InnerHandler.Scheme}' is '{InnerHandler.GetType().Name}' which cannot be used for SignInAsync"); |
|||
} |
|||
|
|||
await InitializeOptionsAsync(); |
|||
await signInHandler.SignInAsync(user, properties); |
|||
} |
|||
|
|||
public virtual async Task<bool> HandleRequestAsync() |
|||
{ |
|||
if (await InnerHandler.ShouldHandleRequestAsync()) |
|||
{ |
|||
await InitializeOptionsAsync(); |
|||
} |
|||
|
|||
return await InnerHandler.HandleRequestAsync(); |
|||
} |
|||
|
|||
protected async virtual Task InitializeOptionsAsync() |
|||
{ |
|||
await OptionsProvider.SetOptionsAsync(InnerHandler.Options); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using AspNet.Security.OAuth.Bilibili; |
|||
using LINGYUN.Abp.Account.OAuth.Settings; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.ExternalProviders.Bilibili; |
|||
|
|||
public class BilibiliAuthHandlerOptionsProvider : OAuthHandlerOptionsProvider<BilibiliAuthenticationOptions> |
|||
{ |
|||
public BilibiliAuthHandlerOptionsProvider(ISettingProvider settingProvider) : base(settingProvider) |
|||
{ |
|||
} |
|||
|
|||
public async override Task SetOptionsAsync(BilibiliAuthenticationOptions options) |
|||
{ |
|||
var clientId = await SettingProvider.GetOrNullAsync(AccountOAuthSettingNames.Bilibili.ClientId); |
|||
var clientSecret = await SettingProvider.GetOrNullAsync(AccountOAuthSettingNames.Bilibili.ClientSecret); |
|||
|
|||
if (!clientId.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.ClientId = clientId; |
|||
} |
|||
if (!clientSecret.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.ClientSecret = clientSecret; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using AspNet.Security.OAuth.GitHub; |
|||
using LINGYUN.Abp.Account.OAuth.Settings; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.ExternalProviders.GitHub; |
|||
|
|||
public class GitHubAuthHandlerOptionsProvider : OAuthHandlerOptionsProvider<GitHubAuthenticationOptions> |
|||
{ |
|||
public GitHubAuthHandlerOptionsProvider(ISettingProvider settingProvider) : base(settingProvider) |
|||
{ |
|||
} |
|||
|
|||
public async override Task SetOptionsAsync(GitHubAuthenticationOptions options) |
|||
{ |
|||
var clientId = await SettingProvider.GetOrNullAsync(AccountOAuthSettingNames.GitHub.ClientId); |
|||
var clientSecret = await SettingProvider.GetOrNullAsync(AccountOAuthSettingNames.GitHub.ClientSecret); |
|||
|
|||
if (!clientId.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.ClientId = clientId; |
|||
} |
|||
if (!clientSecret.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.ClientSecret = clientSecret; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.ExternalProviders; |
|||
|
|||
public interface IOAuthHandlerOptionsProvider<TOptions> |
|||
where TOptions : RemoteAuthenticationOptions, new() |
|||
{ |
|||
Task SetOptionsAsync(TOptions options); |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
using AspNet.Security.OAuth.Bilibili; |
|||
using AspNet.Security.OAuth.GitHub; |
|||
using AspNet.Security.OAuth.QQ; |
|||
using AspNet.Security.OAuth.Weixin; |
|||
using AspNet.Security.OAuth.WorkWeixin; |
|||
using LINGYUN.Abp.Account.OAuth.Features; |
|||
using LINGYUN.Abp.Account.Web.ExternalProviders; |
|||
using LINGYUN.Abp.Account.Web.Models; |
|||
using LINGYUN.Abp.Account.Web.OAuth.Pages.Account.Components.ExternalProviders; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.Extensions.Localization; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Account.Localization; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Features; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.ExternalProviders; |
|||
|
|||
public class OAuthExternalProviderService : IExternalProviderService, ITransientDependency |
|||
{ |
|||
private static readonly Dictionary<string, string> _providerFeaturesMap = new Dictionary<string, string> |
|||
{ |
|||
[GitHubAuthenticationDefaults.AuthenticationScheme] = AccountOAuthFeatureNames.GitHub.Enable, |
|||
[QQAuthenticationDefaults.AuthenticationScheme] = AccountOAuthFeatureNames.QQ.Enable, |
|||
[WeixinAuthenticationDefaults.AuthenticationScheme] = AccountOAuthFeatureNames.WeChat.Enable, |
|||
[WorkWeixinAuthenticationDefaults.AuthenticationScheme] = AccountOAuthFeatureNames.WeCom.Enable, |
|||
[BilibiliAuthenticationDefaults.AuthenticationScheme] = AccountOAuthFeatureNames.Bilibili.Enable |
|||
}; |
|||
|
|||
private readonly IFeatureChecker _featureChecker; |
|||
private readonly IStringLocalizer<AccountResource> _stringLocalizer; |
|||
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; |
|||
public OAuthExternalProviderService( |
|||
IFeatureChecker featureChecker, |
|||
IStringLocalizer<AccountResource> stringLocalizer, |
|||
IAuthenticationSchemeProvider authenticationSchemeProvider) |
|||
{ |
|||
_featureChecker = featureChecker; |
|||
_stringLocalizer = stringLocalizer; |
|||
_authenticationSchemeProvider = authenticationSchemeProvider; |
|||
} |
|||
public async virtual Task<List<ExternalLoginProviderModel>> GetAllAsync() |
|||
{ |
|||
var models = new List<ExternalLoginProviderModel>(); |
|||
|
|||
var schemas = await _authenticationSchemeProvider.GetAllSchemesAsync(); |
|||
|
|||
foreach (var schema in schemas) |
|||
{ |
|||
if (_providerFeaturesMap.TryGetValue(schema.Name, out var schemaFeature)) |
|||
{ |
|||
if (await _featureChecker.IsEnabledAsync(schemaFeature)) |
|||
{ |
|||
models.Add(new ExternalLoginProviderModel |
|||
{ |
|||
Name = schema.Name, |
|||
AuthenticationScheme = schema.Name, |
|||
DisplayName = _stringLocalizer[$"OAuth:{schema.Name}"], |
|||
ComponentType = typeof(ExternalProviderViewComponent), |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return models; |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.ExternalProviders; |
|||
|
|||
public abstract class OAuthHandlerOptionsProvider<TOptions> : IOAuthHandlerOptionsProvider<TOptions>, ITransientDependency |
|||
where TOptions : RemoteAuthenticationOptions, new() |
|||
{ |
|||
protected ISettingProvider SettingProvider { get; } |
|||
public OAuthHandlerOptionsProvider(ISettingProvider settingProvider) |
|||
{ |
|||
SettingProvider = settingProvider; |
|||
} |
|||
|
|||
public abstract Task SetOptionsAsync(TOptions options); |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using AspNet.Security.OAuth.QQ; |
|||
using LINGYUN.Abp.Tencent.QQ.Settings; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.ExternalProviders.QQ; |
|||
|
|||
public class QQAuthHandlerOptionsProvider : OAuthHandlerOptionsProvider<QQAuthenticationOptions> |
|||
{ |
|||
public QQAuthHandlerOptionsProvider(ISettingProvider settingProvider) : base(settingProvider) |
|||
{ |
|||
} |
|||
|
|||
public async override Task SetOptionsAsync(QQAuthenticationOptions options) |
|||
{ |
|||
var clientId = await SettingProvider.GetOrNullAsync(TencentQQSettingNames.QQConnect.AppId); |
|||
var clientSecret = await SettingProvider.GetOrNullAsync(TencentQQSettingNames.QQConnect.AppKey); |
|||
|
|||
if (!clientId.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.ClientId = clientId; |
|||
} |
|||
if (!clientSecret.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.ClientSecret = clientSecret; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using AspNet.Security.OAuth.Weixin; |
|||
using LINGYUN.Abp.WeChat.Official.Settings; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.ExternalProviders.WeChat; |
|||
|
|||
public class WeChatAuthHandlerOptionsProvider : OAuthHandlerOptionsProvider<WeixinAuthenticationOptions> |
|||
{ |
|||
public WeChatAuthHandlerOptionsProvider(ISettingProvider settingProvider) : base(settingProvider) |
|||
{ |
|||
} |
|||
|
|||
public async override Task SetOptionsAsync(WeixinAuthenticationOptions options) |
|||
{ |
|||
var clientId = await SettingProvider.GetOrNullAsync(WeChatOfficialSettingNames.AppId); |
|||
var clientSecret = await SettingProvider.GetOrNullAsync(WeChatOfficialSettingNames.AppSecret); |
|||
|
|||
if (!clientId.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.ClientId = clientId; |
|||
} |
|||
if (!clientSecret.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.ClientSecret = clientSecret; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using AspNet.Security.OAuth.WorkWeixin; |
|||
using LINGYUN.Abp.WeChat.Work.Settings; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.ExternalProviders.WeCom; |
|||
|
|||
public class WeComAuthHandlerOptionsProvider : OAuthHandlerOptionsProvider<WorkWeixinAuthenticationOptions> |
|||
{ |
|||
public WeComAuthHandlerOptionsProvider(ISettingProvider settingProvider) : base(settingProvider) |
|||
{ |
|||
} |
|||
|
|||
public async override Task SetOptionsAsync(WorkWeixinAuthenticationOptions options) |
|||
{ |
|||
var clientId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.CorpId); |
|||
var clientSecret = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.Secret); |
|||
var agentId = await SettingProvider.GetOrNullAsync(WeChatWorkSettingNames.Connection.AgentId); |
|||
|
|||
if (!clientId.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.ClientId = clientId; |
|||
} |
|||
if (!clientSecret.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.ClientSecret = clientSecret; |
|||
} |
|||
if (!agentId.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.AgentId = agentId; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,44 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.Web"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net9.0</TargetFramework> |
|||
<AssemblyName>LINGYUN.Abp.Account.Web.OAuth</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Account.Web.OAuth</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace>LINGYUN.Abp.Account.Web.OAuth</RootNamespace> |
|||
<OutputType>Library</OutputType> |
|||
<IsPackable>true</IsPackable> |
|||
<Nullable>enable</Nullable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="wwwroot\**\*.js" /> |
|||
<EmbeddedResource Include="wwwroot\**\*.css" /> |
|||
<EmbeddedResource Include="wwwroot\**\*.png" /> |
|||
<Content Remove="wwwroot\**\*.js" /> |
|||
<Content Remove="wwwroot\**\*.css" /> |
|||
<Content Remove="wwwroot\**\*.png" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="AspNet.Security.OAuth.Bilibili" /> |
|||
<PackageReference Include="AspNet.Security.OAuth.GitHub" /> |
|||
<PackageReference Include="AspNet.Security.OAuth.QQ" /> |
|||
<PackageReference Include="AspNet.Security.OAuth.Weixin" /> |
|||
<PackageReference Include="AspNet.Security.OAuth.WorkWeixin" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\..\framework\cloud-tencent\LINGYUN.Abp.Tencent.QQ\LINGYUN.Abp.Tencent.QQ.csproj" /> |
|||
<ProjectReference Include="..\..\..\framework\wechat\LINGYUN.Abp.WeChat.Official\LINGYUN.Abp.WeChat.Official.csproj" /> |
|||
<ProjectReference Include="..\..\..\framework\wechat\LINGYUN.Abp.WeChat.Work\LINGYUN.Abp.WeChat.Work.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Account.OAuth\LINGYUN.Abp.Account.OAuth.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Account.Web\LINGYUN.Abp.Account.Web.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,32 @@ |
|||
using JetBrains.Annotations; |
|||
using LINGYUN.Abp.Account.Web.OAuth.ExternalProviders; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using System; |
|||
using System.Linq; |
|||
using Volo.Abp; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.Microsoft.Extensions.DependencyInjection; |
|||
|
|||
public static class AuthenticationBuilderExtensions |
|||
{ |
|||
public static AuthenticationBuilder UseSettingProvider<TOptions, THandler, TOptionsProvider>( |
|||
[NotNull] this AuthenticationBuilder authenticationBuilder) |
|||
where TOptions : RemoteAuthenticationOptions, new() |
|||
where THandler : RemoteAuthenticationHandler<TOptions> |
|||
where TOptionsProvider : IOAuthHandlerOptionsProvider<TOptions> |
|||
{ |
|||
Check.NotNull(authenticationBuilder, nameof(authenticationBuilder)); |
|||
|
|||
var handler = authenticationBuilder.Services.LastOrDefault(x => x.ServiceType == typeof(THandler)); |
|||
authenticationBuilder.Services.Replace(new ServiceDescriptor( |
|||
typeof(THandler), |
|||
provider => new AccountAuthenticationRequestHandler<TOptions, THandler>( |
|||
(THandler)ActivatorUtilities.CreateInstance(provider, typeof(THandler)), |
|||
provider.GetRequiredService<TOptionsProvider>()), |
|||
handler?.Lifetime ?? ServiceLifetime.Transient)); |
|||
|
|||
return authenticationBuilder; |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Volo.Abp.Account.Localization |
|||
@inject IHtmlLocalizer<AccountResource> L |
|||
@model LINGYUN.Abp.Account.Web.Models.ExternalLoginProviderModel |
|||
|
|||
<button |
|||
type="submit" |
|||
class="btn btn-outline-secondary m-1" |
|||
name="provider" |
|||
value="@Model.AuthenticationScheme" |
|||
title="@L["LogInUsingYourProviderAccount", @Model.DisplayName]"> |
|||
<img src="~/images/bilibili_logo_18x18.png" /> |
|||
<span>@Model.DisplayName</span> |
|||
</button> |
|||
@ -0,0 +1,13 @@ |
|||
using LINGYUN.Abp.Account.Web.Models; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OAuth.Pages.Account.Components.ExternalProviders; |
|||
|
|||
public class ExternalProviderViewComponent : AbpViewComponent |
|||
{ |
|||
public virtual IViewComponentResult Invoke(ExternalLoginProviderModel model) |
|||
{ |
|||
return View($"~/Pages/Account/Components/ExternalProviders/{model.AuthenticationScheme}/Default.cshtml", model); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Volo.Abp.Account.Localization |
|||
@inject IHtmlLocalizer<AccountResource> L |
|||
@model LINGYUN.Abp.Account.Web.Models.ExternalLoginProviderModel |
|||
|
|||
<button |
|||
type="submit" |
|||
class="btn btn-outline-secondary m-1" |
|||
name="provider" |
|||
value="@Model.AuthenticationScheme" |
|||
title="@L["LogInUsingYourProviderAccount", @Model.DisplayName]"> |
|||
<i class="fa fa-github"></i> |
|||
<span>@Model.DisplayName</span> |
|||
</button> |
|||
@ -0,0 +1,14 @@ |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Volo.Abp.Account.Localization |
|||
@inject IHtmlLocalizer<AccountResource> L |
|||
@model LINGYUN.Abp.Account.Web.Models.ExternalLoginProviderModel |
|||
|
|||
<button |
|||
type="submit" |
|||
class="btn btn-outline-secondary m-1" |
|||
name="provider" |
|||
value="@Model.AuthenticationScheme" |
|||
title="@L["LogInUsingYourProviderAccount", @Model.DisplayName]"> |
|||
<img src="~/images/qq_logo_15x18.png" /> |
|||
<span>@Model.DisplayName</span> |
|||
</button> |
|||
@ -0,0 +1,14 @@ |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Volo.Abp.Account.Localization |
|||
@inject IHtmlLocalizer<AccountResource> L |
|||
@model LINGYUN.Abp.Account.Web.Models.ExternalLoginProviderModel |
|||
|
|||
<button |
|||
type="submit" |
|||
class="btn btn-outline-secondary m-1" |
|||
name="provider" |
|||
value="@Model.AuthenticationScheme" |
|||
title="@L["LogInUsingYourProviderAccount", @Model.DisplayName]"> |
|||
<i class="fa fa-weixin"></i> |
|||
<span>@Model.DisplayName</span> |
|||
</button> |
|||
@ -0,0 +1,13 @@ |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Volo.Abp.Account.Localization |
|||
@inject IHtmlLocalizer<AccountResource> L |
|||
@model LINGYUN.Abp.Account.Web.Models.ExternalLoginProviderModel |
|||
|
|||
<button |
|||
type="submit" |
|||
class="btn btn-outline-secondary m-1" |
|||
name="provider" |
|||
value="@Model.AuthenticationScheme" |
|||
title="@L["LogInUsingYourProviderAccount", @Model.DisplayName]"> |
|||
<img src="~/images/wecom_logo_77x18.png" /> |
|||
</button> |
|||
@ -0,0 +1,12 @@ |
|||
{ |
|||
"profiles": { |
|||
"LINGYUN.Abp.Account.Web.OAuth": { |
|||
"commandName": "Project", |
|||
"launchBrowser": true, |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
}, |
|||
"applicationUrl": "https://localhost:50897;http://localhost:50898" |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 628 B |
|
After Width: | Height: | Size: 1022 B |
@ -0,0 +1,60 @@ |
|||
using LINGYUN.Abp.Account.Web.ExternalProviders; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Account.Web; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.OpenIddict; |
|||
using static Volo.Abp.Account.Web.Pages.Account.LoginModel; |
|||
using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account |
|||
{ |
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices( |
|||
typeof(LINGYUN.Abp.Account.Web.Pages.Account.LoginModel), |
|||
typeof(OpenIddictLoginModel))] |
|||
public class OpenIddictLoginModel : LINGYUN.Abp.Account.Web.Pages.Account.LoginModel |
|||
{ |
|||
protected AbpOpenIddictRequestHelper OpenIddictRequestHelper { get; } |
|||
public OpenIddictLoginModel( |
|||
IExternalProviderService externalProviderService, |
|||
IAuthenticationSchemeProvider schemeProvider, |
|||
IOptions<AbpAccountOptions> accountOptions, |
|||
IOptions<IdentityOptions> identityOptions, |
|||
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache, |
|||
AbpOpenIddictRequestHelper openIddictRequestHelper) |
|||
: base(externalProviderService, schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache) |
|||
{ |
|||
OpenIddictRequestHelper = openIddictRequestHelper; |
|||
} |
|||
|
|||
public async override Task<IActionResult> OnGetAsync() |
|||
{ |
|||
LoginInput = new LoginInputModel(); |
|||
|
|||
var request = await OpenIddictRequestHelper.GetFromReturnUrlAsync(ReturnUrl); |
|||
if (request?.ClientId != null) |
|||
{ |
|||
// TODO: Find a proper cancel way.
|
|||
// ShowCancelButton = true;
|
|||
|
|||
LoginInput.UserNameOrEmailAddress = request.LoginHint; |
|||
|
|||
//TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key!
|
|||
var tenant = request.GetParameter(TenantResolverConsts.DefaultTenantKey)?.ToString(); |
|||
if (!string.IsNullOrEmpty(tenant)) |
|||
{ |
|||
CurrentTenant.Change(Guid.Parse(tenant)); |
|||
Response.Cookies.Append(TenantResolverConsts.DefaultTenantKey, tenant); |
|||
} |
|||
} |
|||
|
|||
return await base.OnGetAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,64 +0,0 @@ |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Account.Web; |
|||
using Volo.Abp.Account.Web.Pages.Account; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.OpenIddict; |
|||
using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account |
|||
{ |
|||
/// <summary>
|
|||
/// 重写登录模型,实现双因素登录
|
|||
/// </summary>
|
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices(typeof(LoginModel), typeof(OpenIddictSupportedLoginModel))] |
|||
public class TwoFactorSupportedLoginModel : OpenIddictSupportedLoginModel |
|||
{ |
|||
public TwoFactorSupportedLoginModel( |
|||
IAuthenticationSchemeProvider schemeProvider, |
|||
IOptions<AbpAccountOptions> accountOptions, |
|||
IOptions<IdentityOptions> identityOptions, |
|||
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache, |
|||
AbpOpenIddictRequestHelper openIddictRequestHelper) |
|||
: base(schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache, openIddictRequestHelper) |
|||
{ |
|||
} |
|||
|
|||
protected async override Task<List<ExternalProviderModel>> GetExternalProviders() |
|||
{ |
|||
var providers = await base.GetExternalProviders(); |
|||
|
|||
foreach (var provider in providers) |
|||
{ |
|||
var localizedDisplayName = L[provider.DisplayName]; |
|||
if (localizedDisplayName.ResourceNotFound) |
|||
{ |
|||
localizedDisplayName = L["AuthenticationScheme:" + provider.DisplayName]; |
|||
} |
|||
|
|||
if (!localizedDisplayName.ResourceNotFound) |
|||
{ |
|||
provider.DisplayName = localizedDisplayName.Value; |
|||
} |
|||
} |
|||
|
|||
return providers; |
|||
} |
|||
|
|||
protected override Task<IActionResult> TwoFactorLoginResultAsync() |
|||
{ |
|||
// 重定向双因素认证页面
|
|||
return Task.FromResult<IActionResult>(RedirectToPage("SendCode", new |
|||
{ |
|||
returnUrl = ReturnUrl, |
|||
returnUrlHash = ReturnUrlHash, |
|||
rememberMe = LoginInput.RememberMe |
|||
})); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace LINGYUN.Abp.Account.Web; |
|||
|
|||
public static class AbpAccountAuthenticationTypes |
|||
{ |
|||
public const string ShouldChangePassword = "Abp.Account.ShouldChangePassword"; |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
namespace LINGYUN.Abp.Account.Web.Bundling; |
|||
|
|||
public static class AccountBundles |
|||
{ |
|||
public static class Scripts |
|||
{ |
|||
public const string Global = "Abp.Account"; |
|||
|
|||
public const string ChangePassword = Global + ".ChangePassword"; |
|||
} |
|||
|
|||
public static class Styles |
|||
{ |
|||
public const string Global = "Abp.Account"; |
|||
|
|||
public const string UserLoginLink = Global + ".UserLoginLink"; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.Bundling; |
|||
|
|||
[DependsOn(typeof(JQueryScriptContributor))] |
|||
public class ChangePasswordScriptContributor : BundleContributor |
|||
{ |
|||
public override Task ConfigureBundleAsync(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.Add("/Pages/Account/ChangePassword.js"); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.Bundling; |
|||
|
|||
public class UserLoginLinkStyleContributor : BundleContributor |
|||
{ |
|||
public override Task ConfigureBundleAsync(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.Add("/styles/user-login-link/fix-style.css"); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using LINGYUN.Abp.Account.Web.Models; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.ExternalProviders; |
|||
|
|||
public interface IExternalProviderService |
|||
{ |
|||
Task<List<ExternalLoginProviderModel>> GetAllAsync(); |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.Models; |
|||
|
|||
public class ExternalLoginProviderModel |
|||
{ |
|||
public Type ComponentType { get; set; } |
|||
public string Name { get; set; } |
|||
public string DisplayName { get; set; } |
|||
public string AuthenticationScheme { get; set; } |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
@using Localization.Resources.AbpUi |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using LINGYUN.Abp.Account.Web.Bundling; |
|||
@inject IHtmlLocalizer<AbpUiResource> L |
|||
|
|||
<abp-style-bundle name="@AccountBundles.Styles.UserLoginLink" /> |
|||
|
|||
<a class="nav-link fix-margin" role="button" href="~/Account/Login">@L["Login"]</a> |
|||
@ -0,0 +1,4 @@ |
|||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
@ -0,0 +1,46 @@ |
|||
@page |
|||
@using Volo.Abp.Account.Localization |
|||
@using Volo.Abp.Identity |
|||
@using Volo.Abp.Users |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using LINGYUN.Abp.Account.Web.Bundling; |
|||
@using LINGYUN.Abp.Account.Web.Pages.Account |
|||
@inject IHtmlLocalizer<AccountResource> L |
|||
@model LINGYUN.Abp.Account.Web.Pages.Account.ChangePasswordModel |
|||
|
|||
<div class="card mt-3 shadow-sm rounded"> |
|||
<div class="card-body p-5"> |
|||
<h4>@L["ChangePassword"]</h4> |
|||
<form id="ChangePasswordForm" method="post"> |
|||
<div class="mb-3"> |
|||
@if (!Model.HideOldPasswordInput) |
|||
{ |
|||
<label asp-for="Input.CurrentPassword" class="form-label"></label> |
|||
<div class="input-group"> |
|||
<input type="password" class="form-control" autocomplete="new-password" maxlength="@IdentityUserConsts.MaxPasswordLength" asp-for="Input.CurrentPassword" /> |
|||
<button class="btn btn-secondary password-visibility-button" type="button"><i class="fa fa-eye-slash" aria-hidden="true"></i></button> |
|||
</div> |
|||
<span asp-validation-for="Input.CurrentPassword"></span> |
|||
|
|||
<br /> |
|||
} |
|||
<label asp-for="Input.NewPassword" class="form-label"></label> |
|||
<div class="input-group"> |
|||
<input type="password" class="form-control" autocomplete="new-password" maxlength="@IdentityUserConsts.MaxPasswordLength" asp-for="Input.NewPassword" /> |
|||
<button class="btn btn-secondary password-visibility-button" type="button"><i class="fa fa-eye-slash" aria-hidden="true"></i></button> |
|||
</div> |
|||
<span asp-validation-for="Input.NewPassword"></span><br /> |
|||
|
|||
<label asp-for="Input.NewPasswordConfirm" class="form-label"></label> |
|||
<div class="input-group"> |
|||
<input type="password" class="form-control" autocomplete="new-password" maxlength="@IdentityUserConsts.MaxPasswordLength" asp-for="Input.NewPasswordConfirm" /> |
|||
<button class="btn btn-secondary password-visibility-button" type="button"><i class="fa fa-eye-slash" aria-hidden="true"></i></button> |
|||
</div> |
|||
<span asp-validation-for="Input.NewPasswordConfirm"></span> |
|||
</div> |
|||
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value" /> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
|
|||
<abp-script-bundle name="@AccountBundles.Scripts.ChangePassword" /> |
|||
@ -0,0 +1,174 @@ |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Security.Principal; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Account.Web.Pages.Account; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Identity.AspNetCore; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Validation; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.Pages.Account; |
|||
|
|||
public class UserInfoModel : IMultiTenant |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public Guid? TenantId { get; set; } |
|||
} |
|||
|
|||
public class ChangePasswordInputModel |
|||
{ |
|||
[Required] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] |
|||
[Display(Name = "DisplayName:CurrentPassword")] |
|||
[DataType(DataType.Password)] |
|||
[DisableAuditing] |
|||
public string CurrentPassword { get; set; } |
|||
|
|||
[Required] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] |
|||
[Display(Name = "DisplayName:NewPassword")] |
|||
[DataType(DataType.Password)] |
|||
[DisableAuditing] |
|||
public string NewPassword { get; set; } |
|||
|
|||
[Required] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] |
|||
[Display(Name = "DisplayName:NewPasswordConfirm")] |
|||
[DataType(DataType.Password)] |
|||
[DisableAuditing] |
|||
public string NewPasswordConfirm { get; set; } |
|||
} |
|||
|
|||
public class ChangePasswordModel : AccountPageModel |
|||
{ |
|||
[BindProperty] |
|||
public UserInfoModel UserInfo { get; set; } |
|||
|
|||
[BindProperty] |
|||
public ChangePasswordInputModel Input { get; set; } |
|||
|
|||
[BindProperty(SupportsGet = true)] |
|||
public string ReturnUrl { get; set; } |
|||
|
|||
[BindProperty(SupportsGet = true)] |
|||
public string ReturnUrlHash { get; set; } |
|||
|
|||
[BindProperty(SupportsGet = true)] |
|||
public bool RememberMe { get; set; } |
|||
|
|||
public bool HideOldPasswordInput { get; set; } |
|||
|
|||
public AbpSignInManager AbpSignInManager => LazyServiceProvider.LazyGetRequiredService<AbpSignInManager>(); |
|||
public IdentityDynamicClaimsPrincipalContributorCache IdentityDynamicClaimsPrincipalContributorCache => LazyServiceProvider.LazyGetRequiredService<IdentityDynamicClaimsPrincipalContributorCache>(); |
|||
|
|||
public async virtual Task<IActionResult> OnGetAsync() |
|||
{ |
|||
Input = new ChangePasswordInputModel(); |
|||
UserInfo = await GetCurrentUser(); |
|||
|
|||
if (UserInfo == null || UserInfo.TenantId != CurrentTenant.Id) |
|||
{ |
|||
await HttpContext.SignOutAsync(AbpAccountAuthenticationTypes.ShouldChangePassword); |
|||
return RedirectToPage("/Login", new { ReturnUrl, ReturnUrlHash }); |
|||
} |
|||
|
|||
HideOldPasswordInput = (await UserManager.GetByIdAsync(UserInfo.Id)).PasswordHash == null; |
|||
return Page(); |
|||
} |
|||
|
|||
public async virtual Task<IActionResult> OnPostAsync() |
|||
{ |
|||
if (Input.CurrentPassword == Input.NewPassword) |
|||
{ |
|||
Alerts.Warning(L["NewPasswordSameAsOld"]); |
|||
return Page(); |
|||
} |
|||
if (Input.NewPassword != Input.NewPasswordConfirm) |
|||
{ |
|||
Alerts.Warning(L["NewPasswordConfirmFailed"]); |
|||
return Page(); |
|||
} |
|||
|
|||
var userInfo = await GetCurrentUser(); |
|||
if (userInfo != null) |
|||
{ |
|||
if (userInfo.TenantId == CurrentTenant.Id) |
|||
{ |
|||
try |
|||
{ |
|||
await IdentityOptions.SetAsync(); |
|||
var user = await UserManager.GetByIdAsync(userInfo.Id); |
|||
if (user.PasswordHash == null) |
|||
{ |
|||
(await UserManager.AddPasswordAsync(user, Input.NewPassword)).CheckErrors(); |
|||
} |
|||
else |
|||
{ |
|||
(await UserManager.ChangePasswordAsync(user, Input.CurrentPassword, Input.NewPassword)).CheckErrors(); |
|||
} |
|||
|
|||
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() |
|||
{ |
|||
Identity = IdentitySecurityLogIdentityConsts.Identity, |
|||
Action = IdentitySecurityLogActionConsts.ChangePassword |
|||
}); |
|||
user.SetShouldChangePasswordOnNextLogin(false); |
|||
(await UserManager.UpdateAsync(user)).CheckErrors(); |
|||
|
|||
await HttpContext.SignOutAsync(AbpAccountAuthenticationTypes.ShouldChangePassword); |
|||
|
|||
await SignInManager.SignInAsync(user, RememberMe); |
|||
|
|||
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() |
|||
{ |
|||
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal, |
|||
Action = IdentitySecurityLogActionConsts.LoginSucceeded, |
|||
UserName = user.UserName |
|||
}); |
|||
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); |
|||
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Alerts.Warning(GetLocalizeExceptionMessage(ex)); |
|||
return Page(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
await HttpContext.SignOutAsync(AbpAccountAuthenticationTypes.ShouldChangePassword); |
|||
|
|||
return RedirectToPage("/Login", new { ReturnUrl, ReturnUrlHash }); |
|||
} |
|||
|
|||
protected async virtual Task<UserInfoModel> GetCurrentUser() |
|||
{ |
|||
var result = await HttpContext.AuthenticateAsync(AbpAccountAuthenticationTypes.ShouldChangePassword); |
|||
|
|||
var userId = result?.Principal?.FindUserId(); |
|||
if (!userId.HasValue) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var tenantId = result.Principal.FindTenantId(); |
|||
using (CurrentTenant.Change(tenantId, null)) |
|||
{ |
|||
var identityUser = await UserManager.FindByIdAsync(userId.Value.ToString()); |
|||
return identityUser == null |
|||
? null |
|||
: new UserInfoModel() |
|||
{ |
|||
Id = identityUser.Id, |
|||
TenantId = identityUser.TenantId |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
$(function () { |
|||
$(".password-visibility-button").click(function (e) { |
|||
let button = $(this); |
|||
let passwordInput = button.parent().find("input"); |
|||
if (!passwordInput) { |
|||
return; |
|||
} |
|||
|
|||
if (passwordInput.attr("type") === "password") { |
|||
passwordInput.attr("type", "text"); |
|||
} |
|||
else { |
|||
passwordInput.attr("type", "password"); |
|||
} |
|||
|
|||
let icon = button.find("i"); |
|||
if (icon) { |
|||
icon.toggleClass("fa-eye-slash").toggleClass("fa-eye"); |
|||
} |
|||
}); |
|||
}); |
|||
@ -0,0 +1,105 @@ |
|||
@page |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Volo.Abp.Account.Localization |
|||
@using Volo.Abp.Account.Settings |
|||
@using Volo.Abp.Account.Web.Pages.Account; |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Theming; |
|||
@using Volo.Abp.Identity; |
|||
@using Volo.Abp.Settings |
|||
@model LINGYUN.Abp.Account.Web.Pages.Account.LoginModel |
|||
@inject IHtmlLocalizer<AccountResource> L |
|||
@inject IThemeManager ThemeManager |
|||
@inject Volo.Abp.Settings.ISettingProvider SettingProvider |
|||
|
|||
@{ |
|||
Layout = ThemeManager.CurrentTheme.GetAccountLayout(); |
|||
} |
|||
|
|||
@section scripts |
|||
{ |
|||
<abp-script-bundle name="@typeof(LoginModel).FullName"> |
|||
<abp-script src="/Pages/Account/Login.js" /> |
|||
</abp-script-bundle> |
|||
} |
|||
|
|||
<div class="card mt-3 shadow-sm rounded"> |
|||
<div class="card-body p-5"> |
|||
<h4>@L["Login"]</h4> |
|||
@if (Model.EnableLocalLogin) |
|||
{ |
|||
<form method="post" class="mt-4"> |
|||
<div class="mb-3"> |
|||
<label asp-for="LoginInput.UserNameOrEmailAddress" class="form-label"></label> |
|||
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" /> |
|||
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label asp-for="LoginInput.Password" class="form-label"></label> |
|||
<div class="input-group"> |
|||
<input type="password" class="form-control" autocomplete="new-password" maxlength="@IdentityUserConsts.MaxPasswordLength" asp-for="LoginInput.Password" /> |
|||
<button class="btn btn-secondary" type="button" id="PasswordVisibilityButton"><i class="fa fa-eye-slash" aria-hidden="true"></i></button> |
|||
</div> |
|||
<span asp-validation-for="LoginInput.Password"></span> |
|||
</div> |
|||
<abp-row> |
|||
<abp-column> |
|||
<abp-input asp-for="LoginInput.RememberMe" class="mb-4" /> |
|||
</abp-column> |
|||
<abp-column class="text-end"> |
|||
<a href="@Url.Page("./ForgotPassword", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})">@L["ForgotPassword"]</a> |
|||
</abp-column> |
|||
</abp-row> |
|||
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) |
|||
{ |
|||
<strong> |
|||
@L["AreYouANewUser"] |
|||
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a> |
|||
</strong> |
|||
} |
|||
<div class="d-grid gap-2"> |
|||
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-lg mt-3">@L["Login"]</abp-button> |
|||
@if (Model.ShowCancelButton) |
|||
{ |
|||
<abp-button type="submit" button-type="Secondary" formnovalidate="formnovalidate" name="Action" value="Cancel" class="btn-lg mt-3">@L["Cancel"]</abp-button> |
|||
} |
|||
</div> |
|||
</form> |
|||
} |
|||
|
|||
@if (Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="mt-2"> |
|||
<h5>@L["OrLoginWith"]</h5> |
|||
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post"> |
|||
@foreach (var provider in Model.VisibleExternalProviders) |
|||
{ |
|||
@* <button |
|||
type="submit" |
|||
class="btn btn-primary m-1" |
|||
name="provider" |
|||
value="@provider.AuthenticationScheme" |
|||
title="@L["LogInUsingYourProviderAccount", provider.DisplayName]" |
|||
> |
|||
@if (provider.Icon != null) |
|||
{ |
|||
<i class="@provider.Icon"></i> |
|||
} |
|||
<span>@provider.DisplayName</span> |
|||
</button> *@ |
|||
@await Component.InvokeAsync(provider.ComponentType, provider); |
|||
} |
|||
</form> |
|||
</div> |
|||
} |
|||
|
|||
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="alert alert-warning"> |
|||
<strong>@L["InvalidLoginRequest"]</strong> |
|||
@L["ThereAreNoLoginSchemesConfiguredForThisClient"] |
|||
</div> |
|||
} |
|||
|
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,387 @@ |
|||
using LINGYUN.Abp.Account.Web.ExternalProviders; |
|||
using LINGYUN.Abp.Account.Web.Models; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
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.Identity.AspNetCore; |
|||
using Volo.Abp.Reflection; |
|||
using Volo.Abp.Security.Claims; |
|||
using Volo.Abp.Settings; |
|||
using Volo.Abp.Validation; |
|||
using static Volo.Abp.Account.Web.Pages.Account.LoginModel; |
|||
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.Pages.Account; |
|||
|
|||
//[ExposeServices(typeof(Volo.Abp.Account.Web.Pages.Account.LoginModel))]
|
|||
public class LoginModel : AccountPageModel |
|||
{ |
|||
[HiddenInput] |
|||
[BindProperty(SupportsGet = true)] |
|||
public string ReturnUrl { get; set; } |
|||
|
|||
[HiddenInput] |
|||
[BindProperty(SupportsGet = true)] |
|||
public string ReturnUrlHash { get; set; } |
|||
|
|||
[BindProperty] |
|||
public LoginInputModel LoginInput { get; set; } |
|||
|
|||
public bool EnableLocalLogin { get; set; } |
|||
|
|||
public bool ShowCancelButton { get; set; } |
|||
public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; |
|||
public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; |
|||
|
|||
public IEnumerable<ExternalLoginProviderModel> ExternalProviders { get; set; } |
|||
public IEnumerable<ExternalLoginProviderModel> VisibleExternalProviders => ExternalProviders.Where(x => !x.DisplayName.IsNullOrWhiteSpace()); |
|||
|
|||
|
|||
protected IExternalProviderService ExternalProviderService { get; } |
|||
protected IAuthenticationSchemeProvider SchemeProvider { get; } |
|||
protected AbpAccountOptions AccountOptions { get; } |
|||
protected IdentityDynamicClaimsPrincipalContributorCache IdentityDynamicClaimsPrincipalContributorCache { get; } |
|||
public LoginModel( |
|||
IExternalProviderService externalProviderService, |
|||
IAuthenticationSchemeProvider schemeProvider, |
|||
IOptions<AbpAccountOptions> accountOptions, |
|||
IOptions<IdentityOptions> identityOptions, |
|||
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache) |
|||
{ |
|||
ExternalProviderService = externalProviderService; |
|||
SchemeProvider = schemeProvider; |
|||
IdentityOptions = identityOptions; |
|||
AccountOptions = accountOptions.Value; |
|||
IdentityDynamicClaimsPrincipalContributorCache = identityDynamicClaimsPrincipalContributorCache; |
|||
} |
|||
|
|||
public virtual async Task<IActionResult> OnGetAsync() |
|||
{ |
|||
LoginInput = new LoginInputModel(); |
|||
|
|||
ExternalProviders = await GetExternalProviders(); |
|||
|
|||
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); |
|||
|
|||
if (IsExternalLoginOnly) |
|||
{ |
|||
return await OnPostExternalLogin(ExternalProviders.First().AuthenticationScheme); |
|||
} |
|||
|
|||
return Page(); |
|||
} |
|||
|
|||
public async virtual Task<IActionResult> OnPostAsync(string action) |
|||
{ |
|||
await CheckLocalLoginAsync(); |
|||
|
|||
ValidateModel(); |
|||
|
|||
ExternalProviders = await GetExternalProviders(); |
|||
|
|||
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); |
|||
|
|||
await ReplaceEmailToUsernameOfInputIfNeeds(); |
|||
|
|||
await IdentityOptions.SetAsync(); |
|||
|
|||
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 |
|||
}); |
|||
|
|||
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 GetIdentityUserAsync(LoginInput.UserNameOrEmailAddress); |
|||
|
|||
Debug.Assert(user != null, nameof(user) + " != null"); |
|||
|
|||
// Clear the dynamic claims cache.
|
|||
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); |
|||
|
|||
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); |
|||
} |
|||
|
|||
public virtual async Task<IActionResult> OnPostExternalLogin(string provider) |
|||
{ |
|||
var redirectUrl = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash }); |
|||
var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); |
|||
properties.Items["scheme"] = provider; |
|||
|
|||
return await Task.FromResult(Challenge(properties, provider)); |
|||
} |
|||
|
|||
public virtual async Task<IActionResult> OnGetExternalLoginCallbackAsync(string returnUrl = "", string returnUrlHash = "", string remoteError = null) |
|||
{ |
|||
//TODO: Did not implemented Identity Server 4 sample for this method (see ExternalLoginCallback in Quickstart of IDS4 sample)
|
|||
/* Also did not implement these: |
|||
* - Logout(string logoutId) |
|||
*/ |
|||
|
|||
if (remoteError != null) |
|||
{ |
|||
Logger.LogWarning($"External login callback error: {remoteError}"); |
|||
return RedirectToPage("./Login"); |
|||
} |
|||
|
|||
await IdentityOptions.SetAsync(); |
|||
|
|||
var loginInfo = await SignInManager.GetExternalLoginInfoAsync(); |
|||
if (loginInfo == null) |
|||
{ |
|||
Logger.LogWarning("External login info is not available"); |
|||
return RedirectToPage("./Login"); |
|||
} |
|||
|
|||
var result = await SignInManager.ExternalLoginSignInAsync( |
|||
loginInfo.LoginProvider, |
|||
loginInfo.ProviderKey, |
|||
isPersistent: false, |
|||
bypassTwoFactor: true |
|||
); |
|||
|
|||
if (!result.Succeeded) |
|||
{ |
|||
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() |
|||
{ |
|||
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal, |
|||
Action = "Login" + result |
|||
}); |
|||
} |
|||
|
|||
if (result.IsLockedOut) |
|||
{ |
|||
Logger.LogWarning($"External login callback error: user is locked out!"); |
|||
throw new UserFriendlyException("Cannot proceed because user is locked out!"); |
|||
} |
|||
|
|||
if (result.IsNotAllowed) |
|||
{ |
|||
Logger.LogWarning($"External login callback error: user is not allowed!"); |
|||
throw new UserFriendlyException("Cannot proceed because user is not allowed!"); |
|||
} |
|||
|
|||
IdentityUser user; |
|||
if (result.Succeeded) |
|||
{ |
|||
user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); |
|||
if (user != null) |
|||
{ |
|||
// Clear the dynamic claims cache.
|
|||
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); |
|||
} |
|||
|
|||
return await RedirectSafelyAsync(returnUrl, returnUrlHash); |
|||
} |
|||
|
|||
//TODO: Handle other cases for result!
|
|||
|
|||
var email = loginInfo.Principal.FindFirstValue(AbpClaimTypes.Email) ?? loginInfo.Principal.FindFirstValue(ClaimTypes.Email); |
|||
if (email.IsNullOrWhiteSpace()) |
|||
{ |
|||
return RedirectToPage("./Register", new |
|||
{ |
|||
IsExternalLogin = true, |
|||
ExternalLoginAuthSchema = loginInfo.LoginProvider, |
|||
ReturnUrl = returnUrl |
|||
}); |
|||
} |
|||
|
|||
user = await UserManager.FindByEmailAsync(email); |
|||
if (user == null) |
|||
{ |
|||
return RedirectToPage("./Register", new |
|||
{ |
|||
IsExternalLogin = true, |
|||
ExternalLoginAuthSchema = loginInfo.LoginProvider, |
|||
ReturnUrl = returnUrl |
|||
}); |
|||
} |
|||
|
|||
if (await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey) == null) |
|||
{ |
|||
CheckIdentityErrors(await UserManager.AddLoginAsync(user, loginInfo)); |
|||
} |
|||
|
|||
await SignInManager.SignInAsync(user, false); |
|||
|
|||
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() |
|||
{ |
|||
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal, |
|||
Action = result.ToIdentitySecurityLogAction(), |
|||
UserName = user.Name |
|||
}); |
|||
|
|||
// Clear the dynamic claims cache.
|
|||
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); |
|||
|
|||
return await RedirectSafelyAsync(returnUrl, returnUrlHash); |
|||
} |
|||
|
|||
protected virtual Task<IActionResult> TwoFactorLoginResultAsync() |
|||
{ |
|||
// 重定向双因素认证页面
|
|||
return Task.FromResult<IActionResult>(RedirectToPage("SendCode", new |
|||
{ |
|||
returnUrl = ReturnUrl, |
|||
returnUrlHash = ReturnUrlHash, |
|||
rememberMe = LoginInput.RememberMe |
|||
})); |
|||
} |
|||
|
|||
|
|||
protected virtual async Task<IdentityUser> GetIdentityUserAsync(string userNameOrEmailAddress) |
|||
{ |
|||
return await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ?? |
|||
await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); |
|||
} |
|||
|
|||
protected async virtual Task<List<ExternalLoginProviderModel>> GetExternalProviders() |
|||
{ |
|||
var schemes = await SchemeProvider.GetAllSchemesAsync(); |
|||
var externalProviders = await ExternalProviderService.GetAllAsync(); |
|||
|
|||
var externalProviderModels = new List<ExternalLoginProviderModel>(); |
|||
foreach (var scheme in schemes) |
|||
{ |
|||
if (TryGetExternalLoginProvider(scheme, externalProviders, out var externalLoginProvider) || |
|||
scheme.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
externalProviderModels.Add(new ExternalLoginProviderModel |
|||
{ |
|||
Name = externalLoginProvider.Name, |
|||
AuthenticationScheme = scheme.Name, |
|||
DisplayName = externalLoginProvider.DisplayName, |
|||
ComponentType = externalLoginProvider.ComponentType, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
return externalProviderModels; |
|||
} |
|||
|
|||
protected virtual bool TryGetExternalLoginProvider(AuthenticationScheme scheme, List<ExternalLoginProviderModel> externalProviders, out ExternalLoginProviderModel externalLoginProvider) |
|||
{ |
|||
if (ReflectionHelper.IsAssignableToGenericType(scheme.HandlerType, typeof(RemoteAuthenticationHandler<>))) |
|||
{ |
|||
externalLoginProvider = externalProviders.FirstOrDefault(x => x.Name == scheme.Name); |
|||
return externalLoginProvider != null; |
|||
} |
|||
|
|||
externalLoginProvider = null; |
|||
return false; |
|||
} |
|||
|
|||
protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds() |
|||
{ |
|||
if (!ValidationHelper.IsValidEmailAddress(LoginInput.UserNameOrEmailAddress)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var userByUsername = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress); |
|||
if (userByUsername != null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var userByEmail = await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); |
|||
if (userByEmail == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
LoginInput.UserNameOrEmailAddress = userByEmail.UserName; |
|||
} |
|||
|
|||
protected virtual async Task CheckLocalLoginAsync() |
|||
{ |
|||
if (!await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin)) |
|||
{ |
|||
throw new UserFriendlyException(L["LocalLoginDisabledMessage"]); |
|||
} |
|||
} |
|||
|
|||
protected virtual Task<IActionResult> HandleUserLockedOut() |
|||
{ |
|||
Alerts.Warning(L["UserLockedOutMessage"]); |
|||
return Task.FromResult<IActionResult>(Page()); |
|||
} |
|||
|
|||
protected async virtual Task<IActionResult> HandleUserNotAllowed() |
|||
{ |
|||
var notAllowedUser = await GetIdentityUserAsync(LoginInput.UserNameOrEmailAddress); |
|||
if (await UserManager.CheckPasswordAsync(notAllowedUser, LoginInput.Password)) |
|||
{ |
|||
// 用户必须修改密码
|
|||
if (notAllowedUser.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser)) |
|||
{ |
|||
var changePwdIdentity = new ClaimsIdentity(AbpAccountAuthenticationTypes.ShouldChangePassword); |
|||
changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.UserId, notAllowedUser.Id.ToString())); |
|||
if (notAllowedUser.TenantId.HasValue) |
|||
{ |
|||
changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.TenantId, notAllowedUser.TenantId.ToString())); |
|||
} |
|||
|
|||
await HttpContext.SignInAsync(AbpAccountAuthenticationTypes.ShouldChangePassword, new ClaimsPrincipal(changePwdIdentity)); |
|||
|
|||
return RedirectToPage("ChangePassword", new |
|||
{ |
|||
returnUrl = ReturnUrl, |
|||
returnUrlHash = ReturnUrlHash, |
|||
rememberMe = LoginInput.RememberMe |
|||
}); |
|||
} |
|||
} |
|||
Alerts.Warning(L["LoginIsNotAllowed"]); |
|||
return Page(); |
|||
} |
|||
|
|||
protected virtual Task<IActionResult> HandleUserNameOrPasswordInvalid() |
|||
{ |
|||
Alerts.Danger(L["InvalidUserNameOrPassword"]); |
|||
return Task.FromResult<IActionResult>(Page()); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
@page |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Volo.Abp.Account.Localization |
|||
@model LINGYUN.Abp.Account.Web.Pages.Account.RegisterModel |
|||
@inject IHtmlLocalizer<AccountResource> L |
|||
|
|||
<div class="card mt-3 shadow-sm rounded"> |
|||
<div class="card-body p-5"> |
|||
<h4>@L["Register"]</h4> |
|||
<form method="post" class="mt-4"> |
|||
@if (Model.EnableLocalRegister || Model.IsExternalLogin) |
|||
{ |
|||
<abp-input asp-for="Input.UserName" auto-focus="true" /> |
|||
} |
|||
|
|||
@if (Model.EnableLocalRegister || Model.IsExternalLogin) |
|||
{ |
|||
<abp-input asp-for="Input.EmailAddress" /> |
|||
} |
|||
|
|||
@if (!Model.IsExternalLogin && Model.EnableLocalRegister) |
|||
{ |
|||
<abp-input asp-for="Input.Password" /> |
|||
} |
|||
|
|||
<strong> |
|||
@L["AlreadyRegistered"] |
|||
<a href="@Url.Page("./Login", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Login"]</a> |
|||
</strong> |
|||
|
|||
@if (Model.EnableLocalRegister || Model.IsExternalLogin) |
|||
{ |
|||
<div class="d-grid gap-2"> |
|||
<abp-button button-type="Primary" type="submit" class="btn-lg mt-4">@L["Register"]</abp-button> |
|||
</div> |
|||
} |
|||
|
|||
</form> |
|||
|
|||
@if (!Model.IsExternalLogin && Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="mt-2"> |
|||
<h5>@L["OrRegisterWith"]</h5> |
|||
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post"> |
|||
@foreach (var provider in Model.VisibleExternalProviders) |
|||
{ |
|||
@await Component.InvokeAsync(provider.ComponentType, provider); |
|||
} |
|||
</form> |
|||
</div> |
|||
} |
|||
|
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,291 @@ |
|||
using LINGYUN.Abp.Account.Web.ExternalProviders; |
|||
using LINGYUN.Abp.Account.Web.Models; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Account; |
|||
using Volo.Abp.Account.Settings; |
|||
using Volo.Abp.Account.Web; |
|||
using Volo.Abp.Account.Web.Pages.Account; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Reflection; |
|||
using Volo.Abp.Security.Claims; |
|||
using Volo.Abp.Settings; |
|||
using Volo.Abp.Validation; |
|||
using IAbpAccountAppService = Volo.Abp.Account.IAccountAppService; |
|||
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
|||
|
|||
namespace LINGYUN.Abp.Account.Web.Pages.Account; |
|||
|
|||
public class RegisterModel : AccountPageModel |
|||
{ |
|||
|
|||
[BindProperty(SupportsGet = true)] |
|||
public string ReturnUrl { get; set; } |
|||
|
|||
[BindProperty(SupportsGet = true)] |
|||
public string ReturnUrlHash { get; set; } |
|||
|
|||
[BindProperty] |
|||
public PostInput Input { get; set; } |
|||
|
|||
[BindProperty(SupportsGet = true)] |
|||
public bool IsExternalLogin { get; set; } |
|||
|
|||
[BindProperty(SupportsGet = true)] |
|||
public string ExternalLoginAuthSchema { get; set; } |
|||
|
|||
public IEnumerable<ExternalLoginProviderModel> ExternalProviders { get; set; } |
|||
public IEnumerable<ExternalLoginProviderModel> VisibleExternalProviders => ExternalProviders.Where(x => !string.IsNullOrWhiteSpace(x.DisplayName)); |
|||
public bool EnableLocalRegister { get; set; } |
|||
public bool IsExternalLoginOnly => EnableLocalRegister == false && ExternalProviders?.Count() == 1; |
|||
public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; |
|||
|
|||
protected IExternalProviderService ExternalProviderService { get; } |
|||
protected IAuthenticationSchemeProvider SchemeProvider { get; } |
|||
|
|||
protected AbpAccountOptions AccountOptions { get; } |
|||
protected IdentityDynamicClaimsPrincipalContributorCache IdentityDynamicClaimsPrincipalContributorCache { get; } |
|||
|
|||
public RegisterModel( |
|||
IExternalProviderService externalProviderService, |
|||
IAbpAccountAppService accountAppService, |
|||
IAuthenticationSchemeProvider schemeProvider, |
|||
IOptions<AbpAccountOptions> accountOptions, |
|||
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache) |
|||
{ |
|||
ExternalProviderService = externalProviderService; |
|||
SchemeProvider = schemeProvider; |
|||
IdentityDynamicClaimsPrincipalContributorCache = identityDynamicClaimsPrincipalContributorCache; |
|||
AccountAppService = accountAppService; |
|||
AccountOptions = accountOptions.Value; |
|||
} |
|||
|
|||
public virtual async Task<IActionResult> OnGetAsync() |
|||
{ |
|||
ExternalProviders = await GetExternalProviders(); |
|||
|
|||
if (!await CheckSelfRegistrationAsync()) |
|||
{ |
|||
if (IsExternalLoginOnly) |
|||
{ |
|||
return await OnPostExternalLogin(ExternalLoginScheme); |
|||
} |
|||
|
|||
Alerts.Warning(L["SelfRegistrationDisabledMessage"]); |
|||
} |
|||
|
|||
await TrySetEmailAsync(); |
|||
|
|||
return Page(); |
|||
} |
|||
|
|||
protected virtual async Task TrySetEmailAsync() |
|||
{ |
|||
if (IsExternalLogin) |
|||
{ |
|||
var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); |
|||
if (externalLoginInfo == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (!externalLoginInfo.Principal.Identities.Any()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var identity = externalLoginInfo.Principal.Identities.First(); |
|||
var emailClaim = identity.FindFirst(AbpClaimTypes.Email) ?? identity.FindFirst(ClaimTypes.Email); |
|||
|
|||
if (emailClaim == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var userName = await UserManager.GetUserNameFromEmailAsync(emailClaim.Value); |
|||
Input = new PostInput { UserName = userName, EmailAddress = emailClaim.Value }; |
|||
} |
|||
} |
|||
|
|||
public virtual async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
try |
|||
{ |
|||
ExternalProviders = await GetExternalProviders(); |
|||
|
|||
if (!await CheckSelfRegistrationAsync()) |
|||
{ |
|||
throw new UserFriendlyException(L["SelfRegistrationDisabledMessage"]); |
|||
} |
|||
|
|||
if (IsExternalLogin) |
|||
{ |
|||
var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); |
|||
if (externalLoginInfo == null) |
|||
{ |
|||
Logger.LogWarning("External login info is not available"); |
|||
return RedirectToPage("./Login"); |
|||
} |
|||
if (Input.UserName.IsNullOrWhiteSpace()) |
|||
{ |
|||
Input.UserName = await UserManager.GetUserNameFromEmailAsync(Input.EmailAddress); |
|||
} |
|||
await RegisterExternalUserAsync(externalLoginInfo, Input.UserName, Input.EmailAddress); |
|||
} |
|||
else |
|||
{ |
|||
await RegisterLocalUserAsync(); |
|||
} |
|||
|
|||
return Redirect(ReturnUrl ?? "~/"); //TODO: How to ensure safety? IdentityServer requires it however it should be checked somehow!
|
|||
} |
|||
catch (BusinessException e) |
|||
{ |
|||
Alerts.Danger(GetLocalizeExceptionMessage(e)); |
|||
return Page(); |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task RegisterLocalUserAsync() |
|||
{ |
|||
ValidateModel(); |
|||
|
|||
var userDto = await AccountAppService.RegisterAsync( |
|||
new RegisterDto |
|||
{ |
|||
AppName = "MVC", |
|||
EmailAddress = Input.EmailAddress, |
|||
Password = Input.Password, |
|||
UserName = Input.UserName |
|||
} |
|||
); |
|||
|
|||
var user = await UserManager.GetByIdAsync(userDto.Id); |
|||
await SignInManager.SignInAsync(user, isPersistent: true); |
|||
|
|||
// Clear the dynamic claims cache.
|
|||
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); |
|||
} |
|||
|
|||
protected virtual async Task RegisterExternalUserAsync(ExternalLoginInfo externalLoginInfo, string userName, string emailAddress) |
|||
{ |
|||
await IdentityOptions.SetAsync(); |
|||
|
|||
var user = new IdentityUser(GuidGenerator.Create(), userName, emailAddress, CurrentTenant.Id); |
|||
|
|||
(await UserManager.CreateAsync(user)).CheckErrors(); |
|||
(await UserManager.AddDefaultRolesAsync(user)).CheckErrors(); |
|||
|
|||
var userLoginAlreadyExists = user.Logins.Any(x => |
|||
x.TenantId == user.TenantId && |
|||
x.LoginProvider == externalLoginInfo.LoginProvider && |
|||
x.ProviderKey == externalLoginInfo.ProviderKey); |
|||
|
|||
if (!userLoginAlreadyExists) |
|||
{ |
|||
(await UserManager.AddLoginAsync(user, new UserLoginInfo( |
|||
externalLoginInfo.LoginProvider, |
|||
externalLoginInfo.ProviderKey, |
|||
externalLoginInfo.ProviderDisplayName |
|||
))).CheckErrors(); |
|||
} |
|||
|
|||
await SignInManager.SignInAsync(user, isPersistent: true, ExternalLoginAuthSchema); |
|||
|
|||
// Clear the dynamic claims cache.
|
|||
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); |
|||
} |
|||
|
|||
protected virtual async Task<bool> CheckSelfRegistrationAsync() |
|||
{ |
|||
EnableLocalRegister = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin) && |
|||
await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled); |
|||
|
|||
if (IsExternalLogin) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if (!EnableLocalRegister) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
protected async virtual Task<List<ExternalLoginProviderModel>> GetExternalProviders() |
|||
{ |
|||
var schemes = await SchemeProvider.GetAllSchemesAsync(); |
|||
var externalProviders = await ExternalProviderService.GetAllAsync(); |
|||
|
|||
var externalProviderModels = new List<ExternalLoginProviderModel>(); |
|||
foreach (var scheme in schemes) |
|||
{ |
|||
if (TryGetExternalLoginProvider(scheme, externalProviders, out var externalLoginProvider) || |
|||
scheme.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
externalProviderModels.Add(new ExternalLoginProviderModel |
|||
{ |
|||
Name = externalLoginProvider.Name, |
|||
AuthenticationScheme = scheme.Name, |
|||
DisplayName = externalLoginProvider.DisplayName, |
|||
ComponentType = externalLoginProvider.ComponentType, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
return externalProviderModels; |
|||
} |
|||
|
|||
protected virtual bool TryGetExternalLoginProvider(AuthenticationScheme scheme, List<ExternalLoginProviderModel> externalProviders, out ExternalLoginProviderModel externalLoginProvider) |
|||
{ |
|||
if (ReflectionHelper.IsAssignableToGenericType(scheme.HandlerType, typeof(RemoteAuthenticationHandler<>))) |
|||
{ |
|||
externalLoginProvider = externalProviders.FirstOrDefault(x => x.Name == scheme.Name); |
|||
return externalLoginProvider != null; |
|||
} |
|||
|
|||
externalLoginProvider = null; |
|||
return false; |
|||
} |
|||
|
|||
protected virtual async Task<IActionResult> OnPostExternalLogin(string provider) |
|||
{ |
|||
var redirectUrl = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash }); |
|||
var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); |
|||
properties.Items["scheme"] = provider; |
|||
|
|||
return await Task.FromResult(Challenge(properties, provider)); |
|||
} |
|||
|
|||
public class PostInput |
|||
{ |
|||
[Required] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))] |
|||
public string UserName { get; set; } |
|||
|
|||
[Required] |
|||
[EmailAddress] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] |
|||
public string EmailAddress { get; set; } |
|||
|
|||
[Required] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] |
|||
[DataType(DataType.Password)] |
|||
[DisableAuditing] |
|||
public string Password { get; set; } |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
.fix-margin { |
|||
margin: 10px 0; |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue