Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

170 wiersze
5.6KB

  1. using System.Net;
  2. using System.Text.Json.Serialization;
  3. using Microsoft.Extensions.Options;
  4. namespace Campaign_Tracker.Server.Authentication;
  5. public interface IKeycloakTokenClient
  6. {
  7. Task<AuthTokenSetResponse> ExchangeAuthorizationCodeAsync(
  8. string code,
  9. string redirectUri,
  10. CancellationToken cancellationToken);
  11. Task<AuthTokenSetResponse> RefreshAccessTokenAsync(
  12. string refreshToken,
  13. CancellationToken cancellationToken);
  14. Task EndSessionAsync(string idTokenHint, CancellationToken cancellationToken);
  15. }
  16. public sealed class KeycloakTokenClient : IKeycloakTokenClient
  17. {
  18. private readonly HttpClient _httpClient;
  19. private readonly ILogger<KeycloakTokenClient> _logger;
  20. private readonly KeycloakOptions _options;
  21. public KeycloakTokenClient(
  22. HttpClient httpClient,
  23. IOptions<KeycloakOptions> options,
  24. ILogger<KeycloakTokenClient> logger)
  25. {
  26. _httpClient = httpClient;
  27. _logger = logger;
  28. _options = options.Value;
  29. }
  30. public Task<AuthTokenSetResponse> ExchangeAuthorizationCodeAsync(
  31. string code,
  32. string redirectUri,
  33. CancellationToken cancellationToken)
  34. {
  35. return RequestTokenSetAsync(
  36. new Dictionary<string, string>
  37. {
  38. ["grant_type"] = "authorization_code",
  39. ["client_id"] = _options.ClientId,
  40. ["client_secret"] = GetClientSecret(),
  41. ["redirect_uri"] = redirectUri,
  42. ["code"] = code,
  43. },
  44. cancellationToken);
  45. }
  46. public Task<AuthTokenSetResponse> RefreshAccessTokenAsync(
  47. string refreshToken,
  48. CancellationToken cancellationToken)
  49. {
  50. return RequestTokenSetAsync(
  51. new Dictionary<string, string>
  52. {
  53. ["grant_type"] = "refresh_token",
  54. ["client_id"] = _options.ClientId,
  55. ["client_secret"] = GetClientSecret(),
  56. ["refresh_token"] = refreshToken,
  57. },
  58. cancellationToken);
  59. }
  60. public async Task EndSessionAsync(string idTokenHint, CancellationToken cancellationToken)
  61. {
  62. using var content = new FormUrlEncodedContent(new Dictionary<string, string>
  63. {
  64. ["id_token_hint"] = idTokenHint,
  65. ["client_id"] = _options.ClientId,
  66. ["client_secret"] = GetClientSecret(),
  67. });
  68. using var response = await _httpClient.PostAsync(EndSessionEndpoint, content, cancellationToken);
  69. if (!response.IsSuccessStatusCode)
  70. {
  71. var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
  72. _logger.LogWarning(
  73. "Keycloak end-session request failed with {StatusCode}: {ErrorBody}",
  74. (int)response.StatusCode,
  75. errorBody);
  76. throw new KeycloakTokenRequestException(response.StatusCode, errorBody);
  77. }
  78. }
  79. private async Task<AuthTokenSetResponse> RequestTokenSetAsync(
  80. Dictionary<string, string> formValues,
  81. CancellationToken cancellationToken)
  82. {
  83. using var content = new FormUrlEncodedContent(formValues);
  84. using var response = await _httpClient.PostAsync(TokenEndpoint, content, cancellationToken);
  85. if (!response.IsSuccessStatusCode)
  86. {
  87. var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
  88. _logger.LogWarning(
  89. "Keycloak token request failed with {StatusCode}: {ErrorBody}",
  90. (int)response.StatusCode,
  91. errorBody);
  92. throw new KeycloakTokenRequestException(response.StatusCode, errorBody);
  93. }
  94. var payload = await response.Content
  95. .ReadFromJsonAsync<KeycloakTokenResponse>(cancellationToken)
  96. ?? throw new InvalidOperationException("Keycloak token response was empty.");
  97. return new AuthTokenSetResponse(
  98. payload.AccessToken,
  99. payload.RefreshToken,
  100. DateTimeOffset.UtcNow.AddSeconds(payload.ExpiresIn).ToUnixTimeSeconds(),
  101. string.IsNullOrEmpty(payload.IdToken) ? null : payload.IdToken);
  102. }
  103. private string TokenEndpoint =>
  104. $"{_options.TokenEndpointAuthority.TrimEnd('/')}/protocol/openid-connect/token";
  105. private string EndSessionEndpoint =>
  106. $"{_options.TokenEndpointAuthority.TrimEnd('/')}/protocol/openid-connect/logout";
  107. private string GetClientSecret()
  108. {
  109. if (string.IsNullOrWhiteSpace(_options.ClientSecret))
  110. {
  111. throw new InvalidOperationException("Keycloak client secret is not configured.");
  112. }
  113. return _options.ClientSecret;
  114. }
  115. }
  116. public sealed class KeycloakTokenRequestException : Exception
  117. {
  118. public KeycloakTokenRequestException(HttpStatusCode statusCode, string responseBody)
  119. : base($"Keycloak token request failed with status {(int)statusCode}.")
  120. {
  121. StatusCode = statusCode;
  122. ResponseBody = responseBody;
  123. }
  124. public HttpStatusCode StatusCode { get; }
  125. public string ResponseBody { get; }
  126. }
  127. public sealed record AuthTokenSetResponse(
  128. string AccessToken,
  129. string RefreshToken,
  130. long ExpiresAtEpochSeconds,
  131. string? IdToken = null);
  132. internal sealed class KeycloakTokenResponse
  133. {
  134. [JsonPropertyName("access_token")]
  135. public string AccessToken { get; init; } = string.Empty;
  136. [JsonPropertyName("refresh_token")]
  137. public string RefreshToken { get; init; } = string.Empty;
  138. [JsonPropertyName("expires_in")]
  139. public int ExpiresIn { get; init; }
  140. [JsonPropertyName("id_token")]
  141. public string IdToken { get; init; } = string.Empty;
  142. }

Powered by TurnKey Linux.