Created
March 1, 2023 07:39
-
-
Save bangonkali/f3bf3641653bc41e24dd7560bd2e6c74 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.IdentityModel.Tokens.Jwt; | |
using System.Security.Claims; | |
using Microsoft.AspNetCore.Authentication; | |
using Newtonsoft.Json; | |
namespace Netzon.Dash.Middlewares; | |
public static class TokenExchangeMiddlewareExtension | |
{ | |
public static IApplicationBuilder UseTokenExchange(this IApplicationBuilder builder) | |
{ | |
return builder.UseMiddleware<TokenExchangeMiddleware>(); | |
} | |
} | |
public class TokenExchangeMiddleware | |
{ | |
private readonly RequestDelegate _next; | |
private readonly IHttpClientFactory _clientFactory; | |
private readonly ILogger<TokenExchangeMiddleware> _logger; | |
private readonly IConfiguration _configuration; | |
public TokenExchangeMiddleware( | |
RequestDelegate next, | |
IHttpClientFactory clientFactory, | |
ILogger<TokenExchangeMiddleware> logger, | |
IConfiguration configuration | |
) | |
{ | |
_next = next; | |
_clientFactory = clientFactory; | |
_logger = logger; | |
_configuration = configuration; | |
} | |
public async Task InvokeAsync(HttpContext context) | |
{ | |
// if not authenticated then just execute to next middleware and return | |
if (context.User.Identity?.IsAuthenticated != true) | |
{ | |
await _next(context); | |
return; | |
} | |
// get username, for keycloak you need to regex this to get the clean username | |
var currentUserName = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; | |
// logs an error so it's easier to find - thanks debug. | |
if (currentUserName != null) _logger.LogInformation($"Current user: {currentUserName}"); | |
//Debug this line of code if you want to validate the content jwt.io | |
var accessToken = await context.GetTokenAsync("access_token"); | |
var idToken = await context.GetTokenAsync("id_token"); | |
var refreshToken = await context.GetTokenAsync("refresh_token"); | |
// check if accessToken is expired | |
if (IsTokenExpired(accessToken)) | |
{ | |
var newAccessToken = await GetRefreshTokenAsync(refreshToken); | |
var serviceAccessToken = await GetTokenExchangeAsync(newAccessToken); | |
_logger.LogInformation("Access token is expired, refreshing token"); | |
} | |
// Call the next delegate/middleware in the pipeline. | |
await _next(context); | |
} | |
// function that checks if string jwt is expired | |
private static bool IsTokenExpired(string token) | |
{ | |
var handler = new JwtSecurityTokenHandler(); // TODO: optimize | |
var jsonToken = handler.ReadToken(token); | |
var tokenS = jsonToken as JwtSecurityToken; | |
var exp = tokenS.Claims.First(claim => claim.Type == "exp").Value; | |
var unixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); | |
return unixTimestamp > long.Parse(exp); | |
} | |
private async Task<string> GetRefreshTokenAsync(string refreshToken) | |
{ | |
/* | |
* Get refresh token | |
* Uses the settings injected from startup to read the configuration | |
*/ | |
try | |
{ | |
string url = _configuration["Keycloak:TokenExchange"]; | |
//Important the grant type fro refresh token, must be set to this! | |
string grant_type = "refresh_token"; | |
string client_id = _configuration["Keycloak:ClientId"]; | |
string client_secret = _configuration["Keycloak:ClientSecret"]; | |
string token = refreshToken; | |
var form = new Dictionary<string, string> | |
{ | |
{"grant_type", grant_type}, | |
{"client_id", client_id}, | |
{"client_secret", client_secret}, | |
{"refresh_token", token } | |
}; | |
var client = _clientFactory.CreateClient(); // TODO: optimize | |
HttpResponseMessage tokenResponse = await client.PostAsync(url, new FormUrlEncodedContent(form)); | |
var jsonContent = await tokenResponse.Content.ReadAsStringAsync(); | |
Token tok = JsonConvert.DeserializeObject<Token>(jsonContent); | |
return tok.AccessToken; | |
} | |
catch (Exception ex) | |
{ | |
return ex.Message; | |
} | |
} | |
private async Task<string> GetTokenExchangeAsync(string accessToken) | |
{ | |
/* | |
* Get exchange token | |
* ses the settings injected from startup to read the configuration | |
*/ | |
try | |
{ | |
string url = _configuration["Keycloak:TokenExchange"]; | |
//Important, the grant types for token exchange, must be set to this! | |
string grant_type = "urn:ietf:params:oauth:grant-type:token-exchange"; | |
string client_id = _configuration["Keycloak:ClientId"]; | |
string client_secret = _configuration["Keycloak:ClientSecret"]; | |
string audience = _configuration["Keycloak:Audience"]; | |
string token = accessToken; | |
var form = new Dictionary<string, string> | |
{ | |
{"grant_type", grant_type}, | |
{"client_id", client_id}, | |
{"client_secret", client_secret}, | |
{"audience", audience}, | |
{"subject_token", token } | |
}; | |
var client = _clientFactory.CreateClient(); // TODO: optimize | |
HttpResponseMessage tokenResponse = await client.PostAsync(url, new FormUrlEncodedContent(form)); | |
var jsonContent = await tokenResponse.Content.ReadAsStringAsync(); | |
Token tok = JsonConvert.DeserializeObject<Token>(jsonContent); | |
return tok.AccessToken; | |
} | |
catch (Exception ex) | |
{ | |
return ex.Message; | |
} | |
} | |
internal class Token | |
{ | |
[JsonProperty("access_token")] public string AccessToken { get; set; } | |
[JsonProperty("token_type")] public string TokenType { get; set; } | |
[JsonProperty("expires_in")] public int ExpiresIn { get; set; } | |
[JsonProperty("refresh_token")] public string RefreshToken { get; set; } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment