1.IdentityServerClientScopes 分配的Scope太多,会报错
“Scope parameter exceeds max allowed length”
在Domain.Shared层改MyAppModuleExtensionConfigurator可以改abp identityserver定义的const值
private static void ConfigureExistingProperties() { ClientScopeConsts.ScopeMaxLength = 1000; }
这表里面字段长度,而Scope parameter validation是identityserver4的包里做的,这样改并没有效果
应该在Host层改变IdentityServerOptions , PreConfigureServices 和 PostConfigureServices 都可以
public override void PostConfigureServices(ServiceConfigurationContext context) { PostConfigure<IdentityServerOptions>(options => { options.InputLengthRestrictions.Scope = 2000; }); }
2.How to integrate Identity Server with Active Directory #2636
https://github.com/abpframework/abp/issues/2636
context.Services.Replace(ServiceDescriptor .Transient<UserManager<IdentityUser>, LdapUserManager<IdentityUser>>());
public class LdapUserManager<TUser> : Microsoft.AspNetCore.Identity.UserManager<TUser> where TUser : IdentityUser { private readonly IEventService _events; private readonly ILdapManager _ldapManager; private readonly AbpLdapOptions _ldapOptions; public LdapUserManager( IUserStore<TUser> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<TUser> passwordHasher, IEnumerable<IUserValidator<TUser>> userValidators, IEnumerable<IPasswordValidator<TUser>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<TUser>> logger, IEventService events, ILdapManager ldapManager, IOptions<AbpLdapOptions> ldapOptions ) : base( store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger ) { _events = events; _ldapManager = ldapManager; _ldapOptions = ldapOptions.Value; } public override async Task<bool> CheckPasswordAsync(TUser user, string password) { if (string.IsNullOrEmpty(_ldapOptions.DomainName)) { throw new InvalidOperationException("The LDAP Hostname cannot be empty or null."); } if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(user.UserName)) { throw new InvalidOperationException("The LDAP User and Password cannot be empty or null."); } var userName = string.IsNullOrWhiteSpace(_ldapOptions.DomainName) ? user.UserName : $"{user.UserName}@{_ldapOptions.DomainName}"; var success = _ldapManager.Authenticate(userName, password); if (success) { Logger.LogInformation("Credentials validated for username: {username}", userName); await _events.RaiseAsync(new UserLoginSuccessEvent(userName, user.Id.ToString(), userName, interactive: false)); return true; } Logger.LogInformation("Authentication failed for username: {username}, reason: invalid credentials", userName); await _events.RaiseAsync(new UserLoginFailureEvent(userName, "invalid credentials", interactive: false)); return false; } //... }
3. Identity 接口
IIdentityServerInteractionService:用户交互相关接口
IResourceStore:获取资源接口:这里包括2中资源 一种是IdentityResource 和 ApiResource
IClientStore:获取客户端相关接口
IEventService:事件服务
UserStoreServices:自定义的用户服务,这里我没有用IdentityServer4的TestUserStore是为了方面自定义处理
4.证书 参考
IdentityServer4环境部署失败分析贴(一)
https://www.cnblogs.com/Imaigne/p/10519493.html
//在正式环境中,这可能会报错 builder.AddDeveloperSigningCredential(true, "tempkey.rsa"); //正确方式 builder.AddSigningCredential(new X509Certificate2(path, Configuration["Certificates:Password"])) 这里可以参见郭的随笔: https://www.cnblogs.com/guolianyu/p/9872661.html
5.JWT与Reference Token的区别
IdentityServer4之JWT签名(RSA加密证书)及验签
https://www.cnblogs.com/guolianyu/p/9872661.html
6.OIDC
[认证 & 授权] 4. OIDC(OpenId Connect)身份认证(核心部分)
https://www.cnblogs.com/linianhui/p/openid-connect-core.html#auto-id-0
https://github.com/solenovex/Identity-Server-4-Tutorial-Demo-Code
7.Oauth2.0
OAuth2.0 知多少
https://www.cnblogs.com/sheng-jie/p/6564520.html
8.自定义登录认证,在domain层override IResourceOwnerPasswordValidator 的 ValidateAsync 方法
用 SignInManager.SignInWithClaimsAsync 可以加 Customer Claims
using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using IdentityModel; using IdentityServer4.AspNetIdentity; using IdentityServer4.Events; using IdentityServer4.Models; using IdentityServer4.Services; using IdentityServer4.Validation; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; using Volo.Abp.IdentityServer.Localization; using Volo.Abp.Security.Claims; using Volo.Abp.Uow; using Volo.Abp.Validation; using IdentityUser = Volo.Abp.Identity.IdentityUser; namespace Volo.Abp.IdentityServer.AspNetIdentity { [Dependency(ReplaceServices = true)] public class MyAbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator, ITransientDependency { protected SignInManager<IdentityUser> SignInManager { get; } protected UserManager<IdentityUser> UserManager { get; } protected IdentitySecurityLogManager IdentitySecurityLogManager { get; } protected ILogger<ResourceOwnerPasswordValidator<IdentityUser>> Logger { get; } protected IStringLocalizer<AbpIdentityServerResource> Localizer { get; } protected IHybridServiceScopeFactory ServiceScopeFactory { get; } protected AbpIdentityOptions AbpIdentityOptions { get; } public MyAbpResourceOwnerPasswordValidator( UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IdentitySecurityLogManager identitySecurityLogManager, ILogger<ResourceOwnerPasswordValidator<IdentityUser>> logger, IStringLocalizer<AbpIdentityServerResource> localizer, IOptions<AbpIdentityOptions> abpIdentityOptions, IHybridServiceScopeFactory serviceScopeFactory) { UserManager = userManager; SignInManager = signInManager; IdentitySecurityLogManager = identitySecurityLogManager; Logger = logger; Localizer = localizer; ServiceScopeFactory = serviceScopeFactory; AbpIdentityOptions = abpIdentityOptions.Value; } /// <summary> /// https://github.com/IdentityServer/IdentityServer4/blob/master/src/AspNetIdentity/src/ResourceOwnerPasswordValidator.cs#L53 /// </summary> /// <param name="context"></param> /// <returns></returns> [UnitOfWork] public virtual async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { var clientId = context.Request?.Client?.ClientId; using var scope = ServiceScopeFactory.CreateScope(); await ReplaceEmailToUsernameOfInputIfNeeds(context); IdentityUser user = null; var additionalClaims = new List<Claim>(); async Task SetSuccessResultAsync() { var sub = await UserManager.GetUserIdAsync(user); Logger.LogInformation("Credentials validated for username: {username}", context.UserName); await AddCustomClaimsAsync(additionalClaims, user, context); await SignInManager.SignInWithClaimsAsync(user, new AuthenticationProperties { IsPersistent = true }, additionalClaims); context.Result = new GrantValidationResult( sub, OidcConstants.AuthenticationMethods.Password, additionalClaims.ToArray() ); await IdentitySecurityLogManager.SaveAsync( new IdentitySecurityLogContext { Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, Action = IdentityServerSecurityLogActionConsts.LoginSucceeded, UserName = context.UserName, ClientId = clientId } ); } if (AbpIdentityOptions.ExternalLoginProviders.Any()) { foreach (var externalLoginProviderInfo in AbpIdentityOptions.ExternalLoginProviders.Values) { var externalLoginProvider = (IExternalLoginProvider)scope.ServiceProvider .GetRequiredService(externalLoginProviderInfo.Type); if (await externalLoginProvider.TryAuthenticateAsync(context.UserName, context.Password)) { user = await UserManager.FindByNameAsync(context.UserName); if (user == null) { user = await externalLoginProvider.CreateUserAsync(context.UserName, externalLoginProviderInfo.Name); } else { await externalLoginProvider.UpdateUserAsync(user, externalLoginProviderInfo.Name); } await SetSuccessResultAsync(); return; } } } user = await UserManager.FindByNameAsync(context.UserName); string errorDescription = string.Empty; if (user != null) { AuthenticationProperties auth = new AuthenticationProperties(); var result = await SignInManager.CheckPasswordSignInAsync(user, context.Password, true); if (result.Succeeded) { await SetSuccessResultAsync(); return; } else if (result.IsLockedOut) { Logger.LogInformation("Authentication failed for username: {username}, reason: locked out", context.UserName); errorDescription = Localizer["UserLockedOut"]; } else if (result.IsNotAllowed) { Logger.LogInformation("Authentication failed for username: {username}, reason: not allowed", context.UserName); errorDescription = Localizer["LoginIsNotAllowed"]; } else { Logger.LogInformation("Authentication failed for username: {username}, reason: invalid credentials", context.UserName); errorDescription = Localizer["InvalidUserNameOrPassword"]; } await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext { Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, Action = result.ToIdentitySecurityLogAction(), UserName = context.UserName, ClientId = clientId }); await SetSuccessResultAsync(); return; } else { Logger.LogInformation("No user found matching username: {username}", context.UserName); errorDescription = Localizer["InvalidUsername"]; await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() { Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, Action = IdentityServerSecurityLogActionConsts.LoginInvalidUserName, UserName = context.UserName, ClientId = clientId }); } context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, errorDescription); } protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds(ResourceOwnerPasswordValidationContext context) { if (!ValidationHelper.IsValidEmailAddress(context.UserName)) { return; } var userByUsername = await UserManager.FindByNameAsync(context.UserName); if (userByUsername != null) { return; } var userByEmail = await UserManager.FindByEmailAsync(context.UserName); if (userByEmail == null) { return; } context.UserName = userByEmail.UserName; } protected virtual Task AddCustomClaimsAsync(List<Claim> customClaims, IdentityUser user, ResourceOwnerPasswordValidationContext context) { if (user.TenantId.HasValue) { customClaims.Add(new Claim(AbpClaimTypes.TenantId, user.TenantId?.ToString())); } customClaims.Add(new Claim(AbpClaimTypes.PhoneNumber,"testphone")); customClaims.Add(new Claim(JwtClaimTypes.Locale, "testLocale")); customClaims.Add(new Claim("testname", "testvalue")); return Task.CompletedTask; } } }
之后在AppService上可以用CurrentUser 获取 Customer Claims
AbpClaimTypes 可以在 configuration api 获取
{{baseUrl}}/api/abp/application-configuration
9. AuthenticationTypes.Federated vs. AuthenticationType Identity.Application
HttpContext.User.Identity.AuthenticationType == "Identity.Application"
ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity( new List<Claim>{ new Claim(ClaimTypes.Name, username), new Claim(PartnersUserDataClaim, userData), new Claim(ModuleNameClaim, moduleName) }, "SSO/Windows")); await HttpContext.Authentication.SignInAsync(APIAuthSchemeName, principal);
然而这种方法我在另一个应用使用却有问题,Claim会被覆盖掉,而 AuthenticationType 是 "AuthenticationTypes.Federation"。
后来注入了AbpUserClaimsPrincipalFactory,还是不行。
然后试着在表 IdentityServerApiResources 把 新加的Claim Name加进去,就可以了。
public static readonly string DefaultAuthenticationType = "AuthenticationTypes.Federation";
var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");
IdentityServer4-master\IdentityServer4\src\IdentityServer4\src\Services\Default\DefaultClaimsService.cs
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using IdentityModel; using IdentityServer4.Extensions; using IdentityServer4.Models; using IdentityServer4.Validation; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; namespace IdentityServer4.Services { /// <summary> /// Default claims provider implementation /// </summary> public class DefaultClaimsService : IClaimsService { /// <summary> /// The logger /// </summary> protected readonly ILogger Logger; /// <summary> /// The user service /// </summary> protected readonly IProfileService Profile; /// <summary> /// Initializes a new instance of the <see cref="DefaultClaimsService"/> class. /// </summary> /// <param name="profile">The profile service</param> /// <param name="logger">The logger</param> public DefaultClaimsService(IProfileService profile, ILogger<DefaultClaimsService> logger) { Logger = logger; Profile = profile; } /// <summary> /// Returns claims for an identity token /// </summary> /// <param name="subject">The subject</param> /// <param name="resources">The requested resources</param> /// <param name="includeAllIdentityClaims">Specifies if all claims should be included in the token, or if the userinfo endpoint can be used to retrieve them</param> /// <param name="request">The raw request</param> /// <returns> /// Claims for the identity token /// </returns> public virtual async Task<IEnumerable<Claim>> GetIdentityTokenClaimsAsync(ClaimsPrincipal subject, ResourceValidationResult resources, bool includeAllIdentityClaims, ValidatedRequest request) { Logger.LogDebug("Getting claims for identity token for subject: {subject} and client: {clientId}", subject.GetSubjectId(), request.Client.ClientId); var outputClaims = new List<Claim>(GetStandardSubjectClaims(subject)); outputClaims.AddRange(GetOptionalClaims(subject)); // fetch all identity claims that need to go into the id token if (includeAllIdentityClaims || request.Client.AlwaysIncludeUserClaimsInIdToken) { var additionalClaimTypes = new List<string>(); foreach (var identityResource in resources.Resources.IdentityResources) { foreach (var userClaim in identityResource.UserClaims) { additionalClaimTypes.Add(userClaim); } } // filter so we don't ask for claim types that we will eventually filter out additionalClaimTypes = FilterRequestedClaimTypes(additionalClaimTypes).ToList(); var context = new ProfileDataRequestContext( subject, request.Client, IdentityServerConstants.ProfileDataCallers.ClaimsProviderIdentityToken, additionalClaimTypes) { RequestedResources = request.ValidatedResources, ValidatedRequest = request }; await Profile.GetProfileDataAsync(context); var claims = FilterProtocolClaims(context.IssuedClaims); if (claims != null) { outputClaims.AddRange(claims); } } else { Logger.LogDebug("In addition to an id_token, an access_token was requested. No claims other than sub are included in the id_token. To obtain more user claims, either use the user info endpoint or set AlwaysIncludeUserClaimsInIdToken on the client configuration."); } return outputClaims; } /// <summary> /// Returns claims for an identity token. /// </summary> /// <param name="subject">The subject.</param> /// <param name="resourceResult">The validated resource result</param> /// <param name="request">The raw request.</param> /// <returns> /// Claims for the access token /// </returns> public virtual async Task<IEnumerable<Claim>> GetAccessTokenClaimsAsync(ClaimsPrincipal subject, ResourceValidationResult resourceResult, ValidatedRequest request) { Logger.LogDebug("Getting claims for access token for client: {clientId}", request.Client.ClientId); var outputClaims = new List<Claim>() { new Claim(JwtClaimTypes.ClientId, request.ClientId) }; // log if client ID is overwritten if (!string.Equals(request.ClientId, request.Client.ClientId)) { Logger.LogDebug("Client {clientId} is impersonating {impersonatedClientId}", request.Client.ClientId, request.ClientId); } // check for client claims if (request.ClientClaims != null && request.ClientClaims.Any()) { if (subject == null || request.Client.AlwaysSendClientClaims) { foreach (var claim in request.ClientClaims) { var claimType = claim.Type; if (request.Client.ClientClaimsPrefix.IsPresent()) { claimType = request.Client.ClientClaimsPrefix + claimType; } outputClaims.Add(new Claim(claimType, claim.Value, claim.ValueType)); } } } // add scopes (filter offline_access) // we use the ScopeValues collection rather than the Resources.Scopes because we support dynamic scope values // from the request, so this issues those in the token. foreach (var scope in resourceResult.ScopeValues.Where(x => x != IdentityServerConstants.StandardScopes.OfflineAccess)) { outputClaims.Add(new Claim(JwtClaimTypes.Scope, scope)); } // a user is involved if (subject != null) { if (resourceResult.Resources.OfflineAccess) { outputClaims.Add(new Claim(JwtClaimTypes.Scope, IdentityServerConstants.StandardScopes.OfflineAccess)); } Logger.LogDebug("Getting claims for access token for subject: {subject}", subject.GetSubjectId()); outputClaims.AddRange(GetStandardSubjectClaims(subject)); outputClaims.AddRange(GetOptionalClaims(subject)); // fetch all resource claims that need to go into the access token var additionalClaimTypes = new List<string>(); foreach (var api in resourceResult.Resources.ApiResources) { // add claims configured on api resource if (api.UserClaims != null) { foreach (var claim in api.UserClaims) { additionalClaimTypes.Add(claim); } } } foreach(var scope in resourceResult.Resources.ApiScopes) { // add claims configured on scopes if (scope.UserClaims != null) { foreach (var claim in scope.UserClaims) { additionalClaimTypes.Add(claim); } } } // filter so we don't ask for claim types that we will eventually filter out additionalClaimTypes = FilterRequestedClaimTypes(additionalClaimTypes).ToList(); var context = new ProfileDataRequestContext( subject, request.Client, IdentityServerConstants.ProfileDataCallers.ClaimsProviderAccessToken, additionalClaimTypes.Distinct()) { RequestedResources = resourceResult, ValidatedRequest = request }; await Profile.GetProfileDataAsync(context); var claims = FilterProtocolClaims(context.IssuedClaims); if (claims != null) { outputClaims.AddRange(claims); } } return outputClaims; } /// <summary> /// Gets the standard subject claims. /// </summary> /// <param name="subject">The subject.</param> /// <returns>A list of standard claims</returns> protected virtual IEnumerable<Claim> GetStandardSubjectClaims(ClaimsPrincipal subject) { var claims = new List<Claim> { new Claim(JwtClaimTypes.Subject, subject.GetSubjectId()), new Claim(JwtClaimTypes.AuthenticationTime, subject.GetAuthenticationTimeEpoch().ToString(), ClaimValueTypes.Integer64), new Claim(JwtClaimTypes.IdentityProvider, subject.GetIdentityProvider()) }; claims.AddRange(subject.GetAuthenticationMethods()); return claims; } /// <summary> /// Gets additional (and optional) claims from the cookie or incoming subject. /// </summary> /// <param name="subject">The subject.</param> /// <returns>Additional claims</returns> protected virtual IEnumerable<Claim> GetOptionalClaims(ClaimsPrincipal subject) { var claims = new List<Claim>(); var acr = subject.FindFirst(JwtClaimTypes.AuthenticationContextClassReference); if (acr != null) claims.Add(acr); return claims; } /// <summary> /// Filters out protocol claims like amr, nonce etc.. /// </summary> /// <param name="claims">The claims.</param> /// <returns></returns> protected virtual IEnumerable<Claim> FilterProtocolClaims(IEnumerable<Claim> claims) { var claimsToFilter = claims.Where(x => Constants.Filters.ClaimsServiceFilterClaimTypes.Contains(x.Type)); if (claimsToFilter.Any()) { var types = claimsToFilter.Select(x => x.Type); Logger.LogDebug("Claim types from profile service that were filtered: {claimTypes}", types); } return claims.Except(claimsToFilter); } /// <summary> /// Filters out protocol claims like amr, nonce etc.. /// </summary> /// <param name="claimTypes">The claim types.</param> protected virtual IEnumerable<string> FilterRequestedClaimTypes(IEnumerable<string> claimTypes) { var claimTypesToFilter = claimTypes.Where(x => Constants.Filters.ClaimsServiceFilterClaimTypes.Contains(x)); return claimTypes.Except(claimTypesToFilter); } } }
10.AbpUserClaimsPrincipalFactory
using System; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; using Volo.Abp.Security.Claims; using Volo.Abp.Uow; using IdentityRole = Volo.Abp.Identity.IdentityRole; using IdentityUser = Volo.Abp.Identity.IdentityUser; namespace DRS.Identity { [Dependency(ReplaceServices = true)] [ExposeServices(typeof(AbpUserClaimsPrincipalFactory))] // 替换旧的AbpUserClaimsPrincipalFactory public class MyUserClaimsPrincipalFactory : AbpUserClaimsPrincipalFactory, IScopedDependency { public MyUserClaimsPrincipalFactory( UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> options) : base( userManager, roleManager, options) { } public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user) { var principal = await base.CreateAsync(user); var identityPrincipal = principal.Identities.First(); /// add custom claim identityPrincipal.AddClaim(new Claim("fff", "ddddd")); return principal; } } }
11. Permission 分为 Client 和 Role
GO /****** Object: StoredProcedure [dbo].[AddAPIScope] Script Date: 2021/1/29 22:53:11 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO ALTER PROCEDURE [dbo].[AddAPIScope] @scope nvarchar(500), @client nvarchar(500)='TEST_APP' AS BEGIN -- exec AddAPIScope 'DRS.TEST','TEST_APP' select @scope,@client merge into AbpPermissionGrants p using(select newid() as id,null as tenantid,@scope as name,'R' as providername,'admin' as providerkey) as t on(p.name=t.name and p.providername=t.providername and p.providerkey=t.providerkey) when not matched then insert(id,tenantid,name,providername,providerkey) values(t.id,t.tenantid,t.name,t.providername,t.providerkey); merge into AbpPermissionGrants p using(select newid() as id,null as tenantid,@scope as name,'C' as providername,@client as providerkey) as t on(p.name=t.name and p.providername=t.providername and p.providerkey=t.providerkey) when not matched then insert(id,tenantid,name,providername,providerkey) values(t.id,t.tenantid,t.name,t.providername,t.providerkey); merge into IdentityServerApiScopes p using(select id as apiresourceid,@scope as name,@scope+' API' as displayname,null as description,0 as required,0 as emphasize,1 as showindiscoverydocument from [dbo].[IdentityServerApiResources] where name='TEST') as t on(p.apiresourceid =t.apiresourceid and p.name=t.name) when not matched then insert(apiresourceid,name,displayname,description,required,emphasize,showindiscoverydocument) values(t.apiresourceid,t.name,t.displayname,t.description,t.required,t.emphasize,t.showindiscoverydocument); merge into IdentityServerClientScopes p using(select id as clientid,@scope as scope from [dbo].[IdentityServerClients] where ClientId = @client) as t on(p.clientid =t.clientid and p.scope=t.scope) when not matched then insert(clientid,scope) values(t.clientid,t.scope); select * from [AbpPermissionGrants] where name = @scope select * from [IdentityServerApiScopes] where name = @scope select c.ClientId,cs.* from [IdentityServerClients] c,[IdentityServerClientScopes] cs where c.id=cs.ClientId and scope = @scope END