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.

138 wiersze
4.3KB

  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. }
  15. public sealed class KeycloakTokenClient : IKeycloakTokenClient
  16. {
  17. private readonly HttpClient _httpClient;
  18. private readonly ILogger<KeycloakTokenClient> _logger;
  19. private readonly KeycloakOptions _options;
  20. public KeycloakTokenClient(
  21. HttpClient httpClient,
  22. IOptions<KeycloakOptions> options,
  23. ILogger<KeycloakTokenClient> logger)
  24. {
  25. _httpClient = httpClient;
  26. _logger = logger;
  27. _options = options.Value;
  28. }
  29. public Task<AuthTokenSetResponse> ExchangeAuthorizationCodeAsync(
  30. string code,
  31. string redirectUri,
  32. CancellationToken cancellationToken)
  33. {
  34. return RequestTokenSetAsync(
  35. new Dictionary<string, string>
  36. {
  37. ["grant_type"] = "authorization_code",
  38. ["client_id"] = _options.ClientId,
  39. ["client_secret"] = GetClientSecret(),
  40. ["redirect_uri"] = redirectUri,
  41. ["code"] = code,
  42. },
  43. cancellationToken);
  44. }
  45. public Task<AuthTokenSetResponse> RefreshAccessTokenAsync(
  46. string refreshToken,
  47. CancellationToken cancellationToken)
  48. {
  49. return RequestTokenSetAsync(
  50. new Dictionary<string, string>
  51. {
  52. ["grant_type"] = "refresh_token",
  53. ["client_id"] = _options.ClientId,
  54. ["client_secret"] = GetClientSecret(),
  55. ["refresh_token"] = refreshToken,
  56. },
  57. cancellationToken);
  58. }
  59. private async Task<AuthTokenSetResponse> RequestTokenSetAsync(
  60. Dictionary<string, string> formValues,
  61. CancellationToken cancellationToken)
  62. {
  63. using var content = new FormUrlEncodedContent(formValues);
  64. using var response = await _httpClient.PostAsync(TokenEndpoint, content, cancellationToken);
  65. if (!response.IsSuccessStatusCode)
  66. {
  67. var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
  68. _logger.LogWarning(
  69. "Keycloak token request failed with {StatusCode}: {ErrorBody}",
  70. (int)response.StatusCode,
  71. errorBody);
  72. throw new KeycloakTokenRequestException(response.StatusCode, errorBody);
  73. }
  74. var payload = await response.Content
  75. .ReadFromJsonAsync<KeycloakTokenResponse>(cancellationToken)
  76. ?? throw new InvalidOperationException("Keycloak token response was empty.");
  77. return new AuthTokenSetResponse(
  78. payload.AccessToken,
  79. payload.RefreshToken,
  80. DateTimeOffset.UtcNow.AddSeconds(payload.ExpiresIn).ToUnixTimeSeconds());
  81. }
  82. private string TokenEndpoint =>
  83. $"{_options.TokenEndpointAuthority.TrimEnd('/')}/protocol/openid-connect/token";
  84. private string GetClientSecret()
  85. {
  86. if (string.IsNullOrWhiteSpace(_options.ClientSecret))
  87. {
  88. throw new InvalidOperationException("Keycloak client secret is not configured.");
  89. }
  90. return _options.ClientSecret;
  91. }
  92. }
  93. public sealed class KeycloakTokenRequestException : Exception
  94. {
  95. public KeycloakTokenRequestException(HttpStatusCode statusCode, string responseBody)
  96. : base($"Keycloak token request failed with status {(int)statusCode}.")
  97. {
  98. StatusCode = statusCode;
  99. ResponseBody = responseBody;
  100. }
  101. public HttpStatusCode StatusCode { get; }
  102. public string ResponseBody { get; }
  103. }
  104. public sealed record AuthTokenSetResponse(
  105. string AccessToken,
  106. string RefreshToken,
  107. long ExpiresAtEpochSeconds);
  108. internal sealed class KeycloakTokenResponse
  109. {
  110. [JsonPropertyName("access_token")]
  111. public string AccessToken { get; init; } = string.Empty;
  112. [JsonPropertyName("refresh_token")]
  113. public string RefreshToken { get; init; } = string.Empty;
  114. [JsonPropertyName("expires_in")]
  115. public int ExpiresIn { get; init; }
  116. }

Powered by TurnKey Linux.