Как добавить дополнительные утверждения, которые будут включены в access_token, используя ASP.Net Identity с IdentityServer4

Как добавить дополнительные утверждения, которые будут включены в токен?

Как только API получает токен-носитель, объект User.Identity заполняется следующими утверждениями.

[
  {
    "key": "nbf",
    "value": "1484614344"
  },
  {
    "key": "exp",
    "value": "1484615244"
  },
  {
    "key": "iss",
    "value": "http://localhost:85"
  },
  {
    "key": "aud",
    "value": "http://localhost:85/resources"
  },
  {
    "key": "aud",
    "value": "WebAPI"
  },
  {
    "key": "client_id",
    "value": "MyClient"
  },
  {
    "key": "sub",
    "value": "d74c815a-7ed3-4671-b4e4-faceb0854bf6"
  },
  {
    "key": "auth_time",
    "value": "1484611732"
  },
  {
    "key": "idp",
    "value": "local"
  },
  {
    "key": "role",
    "value": "AccountsManager"
  },
  {
    "key": "scope",
    "value": "openid"
  },
  {
    "key": "scope",
    "value": "profile"
  },
  {
    "key": "scope",
    "value": "roles"
  },
  {
    "key": "scope",
    "value": "WebAPI"
  },
  {
    "key": "scope",
    "value": "offline_access"
  },
  {
    "key": "amr",
    "value": "pwd"
  }
]

Мне нужны дополнительные утверждения, такие как username, email, legacySystemUserId и т. Д. Эти поля уже существуют в таблице AspNetUsers (и не существуют повторно в таблице AspNetUserClaims) и доступны в приложении ASP .Net Core в моем объекте ApplicationUser.

Я хочу, чтобы они были включены в токен доступа, который возвращается после аутентификации с использованием имени пользователя и пароля. Хотите использовать то же самое в моем приложении WebAPI, которое не имеет доступа к базе данных сервера идентификации, а его собственная база данных имеет данные, хранящиеся на основе адреса электронной почты пользователя, а не UserId (который представляет собой руководство, созданное в ASP .NET Identity и полученное как SUB претензия).


person ravi punjwani    schedule 17.01.2017    source источник


Ответы (1)


Я часами боролся с этой же проблемой и наконец нашел решение. Эта статья был большим подспорьем, но подведем итоги и поделимся своей реализацией:

Чтобы получить утверждения, назначенные пользователю, и прикрепить их к токену доступа, вам необходимо реализовать два интерфейса на сервере идентификации: IResourceOwnerPasswordValidator и IProfileService. Следующие мои реализации двух классов являются черновыми набросками, но они работают.

** Убедитесь, что у вас установлена ​​последняя версия IdentityServer4 - 1.0.2.

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
    private readonly UserManager<ApplicationUser> _userManager;

    public ResourceOwnerPasswordValidator(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
        var userTask = _userManager.FindByNameAsync(context.UserName);
        var user = userTask.Result;

        context.Result = new GrantValidationResult(user.Id, "password", null, "local", null);
        return Task.FromResult(context.Result);
    }
}

а также

public class AspNetIdentityProfileService : IProfileService
{
    private readonly UserManager<ApplicationUser> _userManager;

    public AspNetIdentityProfileService(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var subject = context.Subject;
        if (subject == null) throw new ArgumentNullException(nameof(context.Subject));

        var subjectId = subject.GetSubjectId();

        var user = await _userManager.FindByIdAsync(subjectId);
        if (user == null)
            throw new ArgumentException("Invalid subject identifier");

        var claims = await GetClaimsFromUser(user);

        var siteIdClaim = claims.SingleOrDefault(x => x.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
        context.IssuedClaims.Add(new Claim(JwtClaimTypes.Email, user.Email));
        context.IssuedClaims.Add(new Claim("siteid", siteIdClaim.Value));
        context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, "User"));

        var roleClaims = claims.Where(x => x.Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
        foreach (var roleClaim in roleClaims)
        {
            context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, roleClaim.Value));
        }
    }

    public async Task IsActiveAsync(IsActiveContext context)
    {
        var subject = context.Subject;
        if (subject == null) throw new ArgumentNullException(nameof(context.Subject));

        var subjectId = subject.GetSubjectId();
        var user = await _userManager.FindByIdAsync(subjectId);

        context.IsActive = false;

        if (user != null)
        {
            if (_userManager.SupportsUserSecurityStamp)
            {
                var security_stamp = subject.Claims.Where(c => c.Type == "security_stamp").Select(c => c.Value).SingleOrDefault();
                if (security_stamp != null)
                {
                    var db_security_stamp = await _userManager.GetSecurityStampAsync(user);
                    if (db_security_stamp != security_stamp)
                        return;
                }
            }

            context.IsActive =
                !user.LockoutEnabled ||
                !user.LockoutEnd.HasValue ||
                user.LockoutEnd <= DateTime.Now;
        }
    }

    private async Task<IEnumerable<Claim>> GetClaimsFromUser(ApplicationUser user)
    {
        var claims = new List<Claim>
        {
            new Claim(JwtClaimTypes.Subject, user.Id),
            new Claim(JwtClaimTypes.PreferredUserName, user.UserName)
        };

        if (_userManager.SupportsUserEmail)
        {
            claims.AddRange(new[]
            {
                new Claim(JwtClaimTypes.Email, user.Email),
                new Claim(JwtClaimTypes.EmailVerified, user.EmailConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
            });
        }

        if (_userManager.SupportsUserPhoneNumber && !string.IsNullOrWhiteSpace(user.PhoneNumber))
        {
            claims.AddRange(new[]
            {
                new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber),
                new Claim(JwtClaimTypes.PhoneNumberVerified, user.PhoneNumberConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
            });
        }

        if (_userManager.SupportsUserClaim)
        {
            claims.AddRange(await _userManager.GetClaimsAsync(user));
        }

        if (_userManager.SupportsUserRole)
        {
            var roles = await _userManager.GetRolesAsync(user);
            claims.AddRange(roles.Select(role => new Claim(JwtClaimTypes.Role, role)));
        }

        return claims;
    }
}

Как только они у вас появятся, их нужно добавить к вашим сервисам в startup.cs:

services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
services.AddTransient<IProfileService, AspNetIdentityProfileService>();

Вот беглый взгляд на мою конфигурацию:

public static IEnumerable<IdentityResource> GetIdentityResources()
{
    return new List<IdentityResource>
    {
        new IdentityResources.OpenId()
    };
}

public static IEnumerable<ApiResource> GetApiResources()
{
    return new List<ApiResource>
    {
        new ApiResource
        {
            Name = "api1",
            Description = "My Api",
            Scopes =
            {
                new Scope()
                {
                    Name = "api1",
                    DisplayName = "Full access to Api"
                }
            }
        }
    };
}

public static IEnumerable<Client> GetClients()
{
    return new List<Client>
    {
        new Client
        {
            ClientId = "apiClient",
            ClientName = "Api Angular2 Client",
            AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
            AlwaysSendClientClaims = true,
            AlwaysIncludeUserClaimsInIdToken = true,
            ClientSecrets =
            {
                new Secret("secret".Sha256())
            },

            AllowedScopes =
            {
                "api1"
            }
        }
    };
}

После этого звонок на сервер идентификации от клиента:

var discoTask = DiscoveryClient.GetAsync("http://localhost:5000");
var disco = discoTask.Result;

var tokenClient = new TokenClient(disco.TokenEndpoint, "apiClient", "secret");
var tokenResponseTask = tokenClient.RequestResourceOwnerPasswordAsync("[email protected]", "my-password", "api1");

var tokenResponse = tokenResponseTask.Result;
var accessToken = tokenResponse.AccessToken;

if (tokenResponse.IsError)
{
    Console.WriteLine(tokenResponse.Error);
    return;
}

Проверьте токен на jwt.io и посмотрите свои результаты ...

person adova    schedule 18.01.2017
comment
Это как раз та проблема, с которой я столкнулся, только ваше решение, похоже, не работает, поскольку код в этих двух новых классах никогда не вызывается. Их даже не строят. Я не уверен, что мне не хватает. Какие-либо предложения? - person Phileosophos; 12.04.2019
comment
@Phileosophos В текущей версии (на сегодняшний день 4.1.1) вы должны добавить свой IProfileService в Startup.cs следующим образом, и вы обнаружите, что он действительно вызывается фреймворком: .AddProfileService<AspNetIdentityProfileService>() Вам также не нужно включать настраиваемый реализация IResourceOwnerPasswordValidator, если вы этого не хотите. - person Jeff Barnard; 18.05.2021