Skip to content

Commit

Permalink
FEAT: Validating role and refactoring validations
Browse files Browse the repository at this point in the history
  • Loading branch information
Felipe Mattioli dos Santos committed Jul 11, 2024
1 parent 973b238 commit bac24c9
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,140 +7,213 @@
using Microsoft.AspNetCore.Http;
using Feijuca.Keycloak.MultiTenancy.Services;
using Feijuca.Keycloak.MultiTenancy.Services.Models;
using Newtonsoft.Json.Linq;

namespace Feijuca.Keycloak.MultiTenancy.Extensions
{
public static class AuthExtensions
{
private static readonly HttpClient _httpClient = new();
private static readonly JwtSecurityTokenHandler _tokenHandler = new();

public static IServiceCollection AddKeyCloakAuth(this IServiceCollection services, AuthSettings authSettings)
{
var httpClient = new HttpClient();
var tokenHandler = new JwtSecurityTokenHandler();

services.AddSingleton<JwtSecurityTokenHandler>()
.AddSingleton(authSettings)
.AddScoped<IAuthService, AuthService>()
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddKeycloakWebApi(
options =>
{
options.Resource = authSettings.ClientId;
options.AuthServerUrl = authSettings.AuthServerUrl;
options.VerifyTokenAudience = true;
},
options =>
services
.AddSingleton(_tokenHandler)
.AddSingleton(authSettings)
.AddScoped<IAuthService, AuthService>()
.AddSingleton(authSettings)
.AddScoped<IAuthService, AuthService>()
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddKeycloakWebApi(
options =>
{
options.Resource = authSettings.ClientId;
options.AuthServerUrl = authSettings.AuthServerUrl;
options.VerifyTokenAudience = true;
},
options =>
{
options.Events = new JwtBearerEvents
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = async context =>
{
try
{
var tokenJwt = context.Request.Headers.Authorization.FirstOrDefault();
if (string.IsNullOrEmpty(tokenJwt))
{
context.HttpContext.Items["AuthError"] = "Invalid JWT token provided! Please check. ";
context.HttpContext.Items["AuthStatusCode"] = 401;
return;
}
var bearerToken = tokenJwt.Replace("Bearer ", "");
var tokenInfos = tokenHandler.ReadJwtToken(tokenJwt.Replace("Bearer ", ""));
var tenantNumber = tokenInfos.Claims.FirstOrDefault(c => c.Type == "tenant")?.Value;
var tenantRealm = authSettings.Realms.FirstOrDefault(realm => realm.Name == tenantNumber);
if (tenantRealm is null)
{
context.HttpContext.Items["AuthError"] = "This token don't belongs to valid tenant. Please check!";
context.HttpContext.Items["AuthStatusCode"] = 401;
context.NoResult();
return;
}
var audience = tokenInfos.Claims.FirstOrDefault(c => c.Type == "aud")?.Value;
if (string.IsNullOrEmpty(audience))
{
context.HttpContext.Items["AuthError"] = "Invalid scope provided! Please, check the scopes provided!";
context.HttpContext.Items["AuthStatusCode"] = 403;
context.NoResult();
return;
}
var jwksUrl = $"{tenantRealm.Issuer}/protocol/openid-connect/certs";
var jwks = await httpClient.GetStringAsync(jwksUrl);
var jsonWebKeySet = new JsonWebKeySet(jwks);
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = tenantRealm.Issuer,
ValidateAudience = true,
ValidAudience = tenantRealm.Audience,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = jsonWebKeySet.Keys
};
var claims = tokenHandler.ValidateToken(bearerToken, tokenValidationParameters, out var validatedToken);
context.Principal = claims;
context.Success();
}
catch (Exception e)
{
context.Response.StatusCode = 500;
context.HttpContext.Items["AuthError"] = "The following error occurs during the authentication process: " + e.Message;
context.Fail("");
}
},
OnAuthenticationFailed = async context =>
{
var errorDescription = context.Exception.Message;
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(errorDescription);
},
OnChallenge = async context =>
{
if (!context.Response.HasStarted)
{
var errorMessage = context.HttpContext.Items["AuthError"] as string ?? "Authentication failed!";
var statusCode = context.HttpContext.Items["AuthStatusCode"] as int? ?? 401;
var responseMessage = new { Message = errorMessage };
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(responseMessage);
}
context.HandleResponse();
}
};
}
);
OnMessageReceived = OnMessageReceived(authSettings),
OnAuthenticationFailed = OnAuthenticationFailed,
OnChallenge = OnChallenge
};
});

services
.AddAuthorization()
.AddKeycloakAuthorization();

if (!string.IsNullOrEmpty(authSettings?.PolicyName))
ConfigureAuthorization(services, authSettings);

return services;
}

private static Func<MessageReceivedContext, Task> OnMessageReceived(AuthSettings authSettings)
{
return async context =>
{
services.AddAuthorizationBuilder()
.AddPolicy(
authSettings.PolicyName,
policy =>
{
policy.RequireResourceRolesForClient(
authSettings.ClientId,
authSettings.Roles!.ToArray());
}
);
try
{
var tokenJwt = context.Request.Headers.Authorization.FirstOrDefault()!;
if (!ValidateToken(context, tokenJwt))
{
return;
}
var token = tokenJwt.Replace("Bearer ", "");
var tokenInfos = _tokenHandler.ReadJwtToken(token);
if (!ValidateAudience(context, tokenInfos))
{
return;
}
var tenantNumber = tokenInfos.Claims.FirstOrDefault(c => c.Type == "tenant")?.Value;
var tenantRealm = authSettings.Realms.FirstOrDefault(realm => realm.Name == tenantNumber)!;
if (!ValidateRealm(context, tenantRealm))
{
return;
}
if (!ValidateRoles(authSettings, context, tokenInfos))
{
return;
}
var tokenValidationParameters = await GetTokenValidationParameters(tenantRealm);
var claims = _tokenHandler.ValidateToken(token, tokenValidationParameters, out var validatedToken);
context.Principal = claims;
context.Success();
}
catch (Exception e)
{
context.Response.StatusCode = 500;
context.HttpContext.Items["AuthError"] = $"Authentication error: {e.Message}";
context.Fail("");
}
};
}

private static Task OnAuthenticationFailed(AuthenticationFailedContext context)
{
var errorMessage = context.HttpContext.Items["AuthError"] as string ?? "Authentication failed!";
var statusCode = context.HttpContext.Items["AuthStatusCode"] as int? ?? 401;
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
return context.Response.WriteAsJsonAsync(new { error = errorMessage });
}

private static async Task OnChallenge(JwtBearerChallengeContext context)
{
if (!context.Response.HasStarted)
{
var errorMessage = context.HttpContext.Items["AuthError"] as string ?? "Authentication failed!";
var statusCode = context.HttpContext.Items["AuthStatusCode"] as int? ?? 401;
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new { Message = errorMessage });
}
else

context.HandleResponse();
}

private static bool ValidateToken(MessageReceivedContext context, string tokenJwt)
{
if (string.IsNullOrEmpty(tokenJwt))
{
services.AddAuthorizationBuilder();
context.HttpContext.Items["AuthError"] = "Invalid JWT token!";
context.HttpContext.Items["AuthStatusCode"] = 401;
return false;
}
return true;
}

return services;
private static bool ValidateRealm(MessageReceivedContext context, Realm tenantRealm)
{
if (tenantRealm == null)
{
context.HttpContext.Items["AuthError"] = "Invalid tenant!";
context.HttpContext.Items["AuthStatusCode"] = 401;
return false;
}
return true;
}

private static bool ValidateRoles(AuthSettings authSettings, MessageReceivedContext context, JwtSecurityToken tokenInfos)
{
if (!authSettings.Roles!.Any())
{
return true;
}

var resourceAccessClaim = tokenInfos.Claims.FirstOrDefault(c => c.Type == "resource_access");
if (resourceAccessClaim == null)
{
context.HttpContext.Items["AuthError"] = "Token missing resource access claim!";
context.HttpContext.Items["AuthStatusCode"] = 403;
return false;
}

var resourceAccess = JObject.Parse(resourceAccessClaim.Value);
var roles = resourceAccess[authSettings.ClientId]?["roles"]?.ToObject<List<string>>();
if (roles == null || !roles.Exists(authSettings.Roles!.Contains))
{
context.HttpContext.Items["AuthError"] = "Token does not contain required roles!";
context.HttpContext.Items["AuthStatusCode"] = 403;
return false;
}

return true;
}

private static bool ValidateAudience(MessageReceivedContext context, JwtSecurityToken tokenInfos)
{
var audience = tokenInfos.Claims.FirstOrDefault(c => c.Type == "aud")?.Value;
if (string.IsNullOrEmpty(audience))
{
context.HttpContext.Items["AuthError"] = "Invalid audience!";
context.HttpContext.Items["AuthStatusCode"] = 403;
return false;
}
return true;
}

private static async Task<TokenValidationParameters> GetTokenValidationParameters(Realm tenantRealm)
{
var jwksUrl = $"{tenantRealm.Issuer}/protocol/openid-connect/certs";
var jwks = await _httpClient.GetStringAsync(jwksUrl);
var jsonWebKeySet = new JsonWebKeySet(jwks);

return new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = tenantRealm.Issuer,
ValidateAudience = true,
ValidAudience = tenantRealm.Audience,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = jsonWebKeySet.Keys
};
}

private static void ConfigureAuthorization(IServiceCollection services, AuthSettings authSettings)
{
services
.AddAuthorization()
.AddKeycloakAuthorization();

if (!string.IsNullOrEmpty(authSettings?.PolicyName))
{
services
.AddAuthorizationBuilder()
.AddPolicy(authSettings.PolicyName, policy =>
{
policy.RequireResourceRolesForClient(
authSettings.ClientId,
authSettings.Roles!.ToArray());
});
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.2" />
<PackageReference Include="Keycloak.AuthServices.Authentication" Version="2.5.2" />
<PackageReference Include="Keycloak.AuthServices.Authorization" Version="2.5.2" />
Expand Down

0 comments on commit bac24c9

Please sign in to comment.