I'm using Angular 11 with ASP.NET Core 5 and IdentityServer4 with later support for Active Directory. I'm currently trying to accomplish a wrapper around /connect/token
and another endpoint which refreshes thetoken. The problem is that tokenResponse.RefreshToken
is null. Why is that?
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"
}
}
};
}
I'm assuming a password grant supports refresh tokens here but I think you need to request "offline_access" scope to obtain a refresh token. Firstly, ensure your client configuration allows that scope and then specify it in your RequestPasswordTokenAsync call's PasswordTokenRequest Scope property.