【发布时间】:2021-07-03 15:52:40
【问题描述】:
我将 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 Scope 属性中指定它。
-
@Mark,非常感谢!现在把它写成答案,所以我可以给你 50 分。
标签: c# identityserver4 asp.net-core-5.0