【问题标题】:ASP.NET Core 5 + IdentityServer4 doesn't send refresh tokenASP.NET Core 5 + IdentityServer4 不发送刷新令牌
【发布时间】:2021-07-03 15:52:40
【问题描述】:

我将 Angular 11 与 ASP.NET Core 5 和 IdentityServer4 一起使用,并在以后支持 Active Directory。我目前正在尝试完成一个围绕/connect/token 的包装器和另一个刷新令牌的端点。问题是tokenResponse.RefreshTokennull。这是为什么呢?

日志

info: IdentityServer4.Startup[0]
      Starting IdentityServer4 version 4.1.1+cebd52f5bc61bdefc262fd20739d4d087c6f961f
info: IdentityServer4.Startup[0]
      You are using the in-memory version of the persisted grant store. This will store consent decisions, authorization codes, refresh and reference tokens in memory only. If you are using any of those features in production, you want to switch to a different store implementation.
info: IdentityServer4.Startup[0]
      Using the default authentication scheme idsrv for IdentityServer
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: E:\GitHub\server\src\AcademicSchedule.Web
info: System.Net.Http.HttpClient.token_client.LogicalHandler[100]
      Start processing HTTP request POST https://localhost:5001/connect/token
info: System.Net.Http.HttpClient.token_client.ClientHandler[100]
      Sending HTTP request POST https://localhost:5001/connect/token
info: IdentityServer4.Hosting.IdentityServerMiddleware[0]
      Invoking IdentityServer endpoint: IdentityServer4.Endpoints.TokenEndpoint for /connect/token
info: IdentityServer4.Events.DefaultEventService[0]
      {
        "ClientId": "client",
        "AuthenticationMethod": "SharedSecret",
        "Category": "Authentication",
        "Name": "Client Authentication Success",
        "EventType": "Success",
        "Id": 1010,
        "ActivityId": "0HM7PV8S5CPCE:00000002",
        "TimeStamp": "2021-04-07T19:20:47Z",
        "ProcessId": 119508,
        "LocalIpAddress": "::1:5001",
        "RemoteIpAddress": "::1"
      }
info: IdentityServer4.Events.DefaultEventService[0]
      {
        "Username": "admin",
        "SubjectId": "1",
        "Endpoint": "Token",
        "ClientId": "client",
        "Category": "Authentication",
        "Name": "User Login Success",
        "EventType": "Success",
        "Id": 1000,
        "ActivityId": "0HM7PV8S5CPCE:00000002",
        "TimeStamp": "2021-04-07T19:20:47Z",
        "ProcessId": 119508,
        "LocalIpAddress": "::1:5001",
        "RemoteIpAddress": "::1"
      }
info: IdentityServer4.Validation.TokenRequestValidator[0]
      Token request validation success, {
        "ClientId": "client",
        "ClientName": "Academic Schedule API",
        "GrantType": "password",
        "Scopes": "assapi openid profile",
        "AuthorizationCode": "********",
        "RefreshToken": "********",
        "UserName": "admin",
        "Raw": {
          "grant_type": "password",
          "username": "admin",
          "password": "***REDACTED***",
          "scope": "openid profile assapi",
          "client_id": "client",
          "client_secret": "***REDACTED***"
        }
      }
info: IdentityServer4.Events.DefaultEventService[0]
      {
        "ClientId": "client",
        "ClientName": "Academic Schedule API",
        "Endpoint": "Token",
        "SubjectId": "1",
        "Scopes": "assapi openid profile",
        "GrantType": "password",
        "Tokens": [
          {
            "TokenType": "access_token",
            "TokenValue": "****7K8A"
          }
        ],
        "Category": "Token",
        "Name": "Token Issued Success",
        "EventType": "Success",
        "Id": 2000,
        "ActivityId": "0HM7PV8S5CPCE:00000002",
        "TimeStamp": "2021-04-07T19:20:47Z",
        "ProcessId": 119508,
        "LocalIpAddress": "::1:5001",
        "RemoteIpAddress": "::1"
      }
info: System.Net.Http.HttpClient.token_client.ClientHandler[101]
      Received HTTP response headers after 390.42ms - 200
info: System.Net.Http.HttpClient.token_client.LogicalHandler[101]
      End processing HTTP request after 408.8932ms - 200

问题

{
  "accessToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjU3NkQwRkQwNzczRTdBNDZDRUVBOTQ2Q0MxM0U4NjYzIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2MTc4MjMwMzQsImV4cCI6MTYxNzgyMzE1NCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMSIsImF1ZCI6WyJhc3NhcGkiLCJodHRwczovL2xvY2FsaG9zdDo1MDAxL3Jlc291cmNlcyJdLCJjbGllbnRfaWQiOiJjbGllbnQiLCJzdWIiOiIxIiwiYXV0aF90aW1lIjoxNjE3ODIzMDM0LCJpZHAiOiJsb2NhbCIsInVzZXJuYW1lIjoiYWRtaW4iLCJlbWFpbCI6ImFkbWluQHVuaS1ydXNlLmJnIiwicm9sZSI6IkFkbWluaXN0cmF0b3IiLCJqdGkiOiI5RUI4OEJGQkJGQTIxQUNFNEUyNzM3NERDMjQxNjBCMyIsImlhdCI6MTYxNzgyMzAzNCwic2NvcGUiOlsiYXNzYXBpIiwib3BlbmlkIiwicHJvZmlsZSJdLCJhbXIiOlsicHdkIl19.VscU-FmGuXZKyObXVEKhDZZ_Q4ACoqn820RCRrMKDo__X8CLskEOSXrHC03ybt-jnNKZE0plhWd6OO3JSMn54QEqhQjVVN62SSOgwewlh9zEFEpzw-vo0bGvIBSiwgcOwF2N6poeGx5kmgzsUCb-YAcR8m47VIpVOU0jkPeyn-WHwkkYE5z9sWJYZiT0uJCLbqJEIYlTow1EVNnokD-bMw6PisAL8S0pq7Pvf-lp-yFF24wVISKDc9YWSScmWc29KE5E_Fr43poQBrPNRXkeQ_RtxWX9D-i1cbo58sTncLggjX9NHYSTpbjq3tKYON349DAXW9EGZNB2SCRuOsJHsw",
  "refreshToken": null, // Here
  "expiresIn": 120,
  "expiresAtUtc": "2021-04-07T19:19:15.1026438Z"
}

片段

services.AddIdentityServer(options =>
{
    options.Events.RaiseErrorEvents = true;
    options.Events.RaiseInformationEvents = true;
    options.Events.RaiseFailureEvents = true;
    options.Events.RaiseSuccessEvents = true;

    options.EmitStaticAudienceClaim = true;
})
    .AddDeveloperSigningCredential()
    .AddInMemoryIdentityResources(Configuration.GetIdentityResources())
    .AddInMemoryApiScopes(Configuration.GetApiScopes())
    .AddInMemoryApiResources(Configuration.GetApiResources(configuration))
    .AddInMemoryClients(Configuration.GetClients(configuration))
    .AddCustomUserStore();

public class TokenLoginModel
{
    public string Username { get; set; }
    public string Password { get; set; }
}

public class RefreshTokenModel
{
    public string AccessToken { get; set; }
    public string RefreshToken { get; set; }
}

[ApiController]
[Route("api/[controller]")]
public class AccountsController : ControllerBase
{
    private readonly IConfiguration _configuration;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IUserRepository _userRepository;

    public AccountsController(IConfiguration configuration, IHttpClientFactory httpClientFactory, IUserRepository userRepository)
    {
        _configuration = configuration;
        _httpClientFactory = httpClientFactory;
        _userRepository = userRepository;
    }

    [HttpPost("token/create")]
    public async Task<IActionResult> CreateToken([FromBody] TokenLoginModel login)
    {
        var client = _httpClientFactory.CreateClient("token_client");
        
        var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
        {
            Address = $"{_configuration["AuthConfiguration:ClientUrl"]}/connect/token",
            ClientId = _configuration["AuthConfiguration:ClientId"],
            ClientSecret = _configuration["AuthConfiguration:ClientSecret"],
            Scope = $"{IdentityServerConstants.StandardScopes.OpenId} {IdentityServerConstants.StandardScopes.Profile} assapi",
            
            UserName = login.Username,
            Password = login.Password
        }).ConfigureAwait(false);
        
        if (tokenResponse.IsError)
        {
            return BadRequest(tokenResponse.ErrorDescription);
        }
        
        return Ok(new
        {
            AccessToken = tokenResponse.AccessToken,
            RefreshToken = tokenResponse.RefreshToken,
            ExpiresIn = tokenResponse.ExpiresIn,
            ExpiresAtUtc = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn)
        });
    }

    [HttpPost("token/refresh")]
    public async Task<IActionResult> RefreshToken(RefreshTokenModel model)
    {
        var client = _httpClientFactory.CreateClient("token_client");

        var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
        {
            Address = $"{_configuration["AuthConfiguration:ClientUrl"]}/connect/token",
            ClientId = _configuration["AuthConfiguration:ClientId"],
            ClientSecret = _configuration["AuthConfiguration:ClientSecret"],
            Scope = $"{IdentityServerConstants.StandardScopes.OpenId} {IdentityServerConstants.StandardScopes.Profile} assapi",
            
            RefreshToken = model.RefreshToken
        }).ConfigureAwait(false);

        if (tokenResponse.IsError)
        {
            return BadRequest(tokenResponse.ErrorDescription);
        }

        return Ok(new
        {
            AccessToken = tokenResponse.AccessToken,
            RefreshToken = tokenResponse.RefreshToken,
            ExpiresIn = tokenResponse.ExpiresIn,
            ExpiresAtUtc = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn)
        });
    }
}

public static class Configuration
{
    public static IEnumerable<IdentityResource> GetIdentityResources() =>
        new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile()
        };

    public static IEnumerable<ApiScope> GetApiScopes() =>
        new List<ApiScope>
        {
            new("assapi", "Academic Schedule API")
        };

    public static IEnumerable<ApiResource> GetApiResources(IConfiguration configuration) =>
        new List<ApiResource>
        {
            new("assapi", "Academic Schedule API")
            {
                ApiSecrets = new List<Secret>
                {
                    new(configuration["AuthConfiguration:ClientSecret"].Sha256())
                },
                Scopes =
                {
                    "assapi"
                }
            }
        };

    public static IEnumerable<Client> GetClients(IConfiguration configuration) =>
        new List<Client>
        {
            new()
            {
                ClientName = configuration["AuthConfiguration:ClientName"],
                ClientId = configuration["AuthConfiguration:ClientId"],
                ClientSecrets = { new Secret(configuration["AuthConfiguration:ClientSecret"].Sha256()) },

                AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
                AccessTokenType = AccessTokenType.Jwt,
                AllowOfflineAccess = true,

                AccessTokenLifetime = 120,
                IdentityTokenLifetime = 120,
                UpdateAccessTokenClaimsOnRefresh = true,
                SlidingRefreshTokenLifetime = 300,
                RefreshTokenExpiration = TokenExpiration.Absolute,
                RefreshTokenUsage = TokenUsage.OneTimeOnly,
                AlwaysSendClientClaims = true,
                
                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "assapi"
                }
            }
        };
}

【问题讨论】:

  • 我假设密码授权在这里支持刷新令牌,但我认为您需要请求“offline_access”范围来获取刷新令牌。首先,确保您的客户端配置允许该范围,然后在 RequestPasswordTokenAsync 调用的 PasswordTokenRequest Scope 属性中指定它。
  • @Mark,非常感谢!现在把它写成答案,所以我可以给你 50 分。

标签: c# identityserver4 asp.net-core-5.0


【解决方案1】:

我假设密码授权在这里支持刷新令牌,但我认为您需要请求“offline_access”范围来获取刷新令牌。首先,确保您的客户端配置允许该范围,然后在 RequestPasswordTokenAsync 调用的 PasswordTokenRequest Scope 属性中指定它。

【讨论】:

  • 再次非常感谢您!上面写着,我可以在 5 小时内给你赏金。
猜你喜欢
  • 2018-07-07
  • 2019-03-23
  • 2020-02-23
  • 2018-03-30
  • 2020-09-12
  • 2017-03-30
  • 2019-12-23
  • 2015-03-19
  • 2020-07-06
相关资源
最近更新 更多