ASP.NET Core 5 + IdentityServer4 不发送刷新令牌
ASP.NET Core 5 + IdentityServer4 doesn't send refresh token
我将 Angular 11 与 ASP.NET Core 5 和 IdentityServer4 一起使用,后者支持 Active Directory。我目前正在尝试围绕 /connect/token
和另一个刷新令牌的端点完成包装。问题是 tokenResponse.RefreshToken
是 null。这是为什么?
日志
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 范围 属性.
中指定它
我将 Angular 11 与 ASP.NET Core 5 和 IdentityServer4 一起使用,后者支持 Active Directory。我目前正在尝试围绕 /connect/token
和另一个刷新令牌的端点完成包装。问题是 tokenResponse.RefreshToken
是 null。这是为什么?
日志
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 范围 属性.
中指定它