I am trying to verify a json web token obtained by a firebase android client and passed to a server running .net
Following the answer here I created these methods to validate the token and extract the uid:
public static async Task<string> GetUserNameFromTokenIfValid(string jsonWebToken)
{
const string FirebaseProjectId = "testapp-16ecd";
try
{
// 1. Get Google signing keys
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("https://www.googleapis.com/robot/v1/metadata/");
HttpResponseMessage response = await client.GetAsync("x509/securetoken#system.gserviceaccount.com");
if (!response.IsSuccessStatusCode) { return null; }
var x509Data = await response.Content.ReadAsAsync<Dictionary<string, string>>();
SecurityKey[] keys = x509Data.Values.Select(CreateSecurityKeyFromPublicKey).ToArray();
// Use JwtSecurityTokenHandler to validate the JWT token
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
// Set the expected properties of the JWT token in the TokenValidationParameters
TokenValidationParameters validationParameters = new TokenValidationParameters()
{
ValidAudience = FirebaseProjectId,
ValidIssuer = "https://securetoken.google.com/" + FirebaseProjectId,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = keys
};
SecurityToken validatedToken;
ClaimsPrincipal principal = tokenHandler.ValidateToken(jsonWebToken, validationParameters, out validatedToken);
var jwt = (JwtSecurityToken)validatedToken;
return jwt.Subject;
}
catch (Exception e)
{
return null;
}
}
static SecurityKey CreateSecurityKeyFromPublicKey(string data)
{
return new X509SecurityKey(new X509Certificate2(Encoding.UTF8.GetBytes(data)));
}
When I run the code I get the response:
{"IDX10501: Signature validation failed. Unable to match 'kid': 'c2154b0435d58fc96a4480bd7655188fd4370b07', \ntoken: '{"alg":"RS256","typ":"JWT","kid":"c2154b0435d58fc96a4480bd7655188fd4370b07"}......
Calling https://www.googleapis.com/robot/v1/metadata/x509/securetoken#system.gserviceaccount.com does return a certificate with a matching id:
{
"c2154b0435d58fc96a4480bd7655188fd4370b07": "-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIIRZGQCmoKoNQwDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE\nAxMmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMTYx\nMTIxMDA0NTI2WhcNMTYxMTI0MDExNTI2WjAxMS8wLQYDVQQDEyZzZWN1cmV0b2tl\nbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAKHbxqFaNQyrrrv8gocpQjES+HCum8XRQYYLRqstJ12FGtDN\np32qagCbc0x94TaBZF7tCPMgyFU8pBQP7CvCxWxoy+Xdv+52lcR0sG/kskr23E3N\nJmWVHT3YwiMwdgsbWDIpWEbvJdn3DPFaapvD9BJPwNoXuFCO2vA2rhi1LuNWsaHt\nBj5jTicGCnt2PGKUTXJ9q1hOFi90wxTVUVMfFqDa4g9iKqRoaNaLOo0w3VgsFPlr\nMBca1fw1ArZpEGm3XHaDOiCi+EZ2+GRvdF/aPNy1+RdnUPMEEuHErULSxXpYGIdt\n/Mo7QvtFXkIl6ZHvEp5pWkS8mlAJyfPrOs8RzXMCAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJ\nKoZIhvcNAQEFBQADggEBAJYXDQFIOC0W0ZwLO/5afSlqtMZ+lSiFJJnGx/IXI5Mi\n0sBI3QA7QXmiNH4tVyEiK+HsFPKAYovsbh7HDypEnBGsz9UmEU6Wn6Qu9/v38+bo\nLant6Ds9ME7QHhKJKtYkso0F2RVwu220xZQl1yrl4bjq+2ZDncYthILjw5t+8Z4c\nQW5UCr2wlVtkflGtIPR1UrvyU13eiI5SPkwOWPZvG2iTabnLfcRIkhQgIalkznMe\niz8Pzpk9eT8HFeZYiB61GpIWHG4oEb1/Z4Q//os+vWDQ+X0ARTYhTEbwLLQ0dcjW\nfg/tm7J+MGH5NH5MwjO+CI4fA3NoGOuEzF1vb7/hNdU=\n-----END CERTIFICATE-----\n"
I have successfully validated this token using the Java call (made in kotlin)
FirebaseAuth.getInstance().verifyIdToken(idToken).addOnSuccessListener { decodedToken ->
val uid = decodedToken.uid
}
I'm sure by now you have figured out the solution for this, but for future people who come across this question.
Set the KeyId for the X509SecurityKey
x509Data.Select(cert => new X509SecurityKey(new X509Certificate2(Encoding.UTF8.GetBytes(cert.Value)))
{
KeyId = cert.Key
})
.ToArray()
This will allow the TokenValidationParameters to look up which issuerKey to validate against.
Related
I don't understand how this library works. Could you help me please ?
Here is my simple code :
public void TestJwtSecurityTokenHandler()
{
var stream =
"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJJU1MiLCJzY29wZSI6Imh0dHBzOi8vbGFyaW0uZG5zY2UuZG91YW5lL2NpZWxzZXJ2aWNlL3dzIiwiYXVkIjoiaHR0cHM6Ly9kb3VhbmUuZmluYW5jZXMuZ291di5mci9vYXV0aDIvdjEiLCJpYXQiOiJcL0RhdGUoMTQ2ODM2MjU5Mzc4NClcLyJ9";
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadToken(stream);
}
This is the error :
The string needs to be in compact JSON format, which is of the form: Base64UrlEncodedHeader.Base64UrlEndcodedPayload.OPTIONAL,Base64UrlEncodedSignature'.
If you copy the stream in jwt.io website, it works fine :)
I found the solution, I just forgot to Cast the result:
var stream = "[encoded jwt]";
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadToken(stream);
var tokenS = jsonToken as JwtSecurityToken;
Or, without the cast:
var token = "[encoded jwt]";
var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(token);
I can get Claims using:
var jti = tokenS.Claims.First(claim => claim.Type == "jti").Value;
new JwtSecurityTokenHandler().ReadToken("") will return a SecurityToken
new JwtSecurityTokenHandler().ReadJwtToken("") will return a JwtSecurityToken
If you just change the method you are using you can avoid the cast in the above answer
You need the secret string which was used to generate encrypt token.
This code works for me:
protected string GetName(string token)
{
string secret = "this is a string used for encrypt and decrypt token";
var key = Encoding.ASCII.GetBytes(secret);
var handler = new JwtSecurityTokenHandler();
var validations = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
var claims = handler.ValidateToken(token, validations, out var tokenSecure);
return claims.Identity.Name;
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Email, model.UserName),
new Claim(JwtRegisteredClaimNames.NameId, model.Id.ToString()),
};
var token = new JwtSecurityToken(_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
Then extract content
var handler = new JwtSecurityTokenHandler();
string authHeader = Request.Headers["Authorization"];
authHeader = authHeader.Replace("Bearer ", "");
var jsonToken = handler.ReadToken(authHeader);
var tokenS = handler.ReadToken(authHeader) as JwtSecurityToken;
var id = tokenS.Claims.First(claim => claim.Type == "nameid").Value;
Using .net core jwt packages, the Claims are available:
[Route("api/[controller]")]
[ApiController]
[Authorize(Policy = "Bearer")]
public class AbstractController: ControllerBase
{
protected string UserId()
{
var principal = HttpContext.User;
if (principal?.Claims != null)
{
foreach (var claim in principal.Claims)
{
log.Debug($"CLAIM TYPE: {claim.Type}; CLAIM VALUE: {claim.Value}");
}
}
return principal?.Claims?.SingleOrDefault(p => p.Type == "username")?.Value;
}
}
I write this solution and it's work for me
protected Dictionary<string, string> GetTokenInfo(string token)
{
var TokenInfo = new Dictionary<string, string>();
var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(token);
var claims = jwtSecurityToken.Claims.ToList();
foreach (var claim in claims)
{
TokenInfo.Add(claim.Type, claim.Value);
}
return TokenInfo;
}
Extending on cooxkie answer, and dpix answer, when you are reading a jwt token (such as an access_token received from AD FS), you can merge the claims in the jwt token with the claims from "context.AuthenticationTicket.Identity" that might not have the same set of claims as the jwt token.
To Illustrate, in an Authentication Code flow using OpenID Connect,after a user is authenticated, you can handle the event SecurityTokenValidated which provides you with an authentication context, then you can use it to read the access_token as a jwt token, then you can "merge" tokens that are in the access_token with the standard list of claims received as part of the user identity:
private Task OnSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage,OpenIdConnectAuthenticationOptions> context)
{
//get the current user identity
ClaimsIdentity claimsIdentity = (ClaimsIdentity)context.AuthenticationTicket.Identity;
/*read access token from the current context*/
string access_token = context.ProtocolMessage.AccessToken;
JwtSecurityTokenHandler hand = new JwtSecurityTokenHandler();
//read the token as recommended by Coxkie and dpix
var tokenS = hand.ReadJwtToken(access_token);
//here, you read the claims from the access token which might have
//additional claims needed by your application
foreach (var claim in tokenS.Claims)
{
if (!claimsIdentity.HasClaim(claim.Type, claim.Value))
claimsIdentity.AddClaim(claim);
}
return Task.FromResult(0);
}
Use this:
public static string Get_Payload_JWTToken(string token)
{
var handler = new JwtSecurityTokenHandler();
var DecodedJWT = handler.ReadJwtToken(token);
string payload = DecodedJWT.EncodedPayload; // Gives Payload
return Encoding.UTF8.GetString(FromBase64Url(payload));
}
static byte[] FromBase64Url(string base64Url)
{
string padded = base64Url.Length % 4 == 0
? base64Url : base64Url + "====".Substring(base64Url.Length % 4);
string base64 = padded.Replace("_", "/").Replace("-", "+");
return Convert.FromBase64String(base64);
}
Though this answer is not answering the original question but its a really very useful feature for C# developers, so adding it as the answer.
Visual Studio 2022 has added a feature to decode the value of a token at runtime.
You can check the feature in Visual Studio 2022 preview (version 17.5.0 preview 2.0)
Mouse over the variable containing the JWT and then select the string manipulation as JWT Decode, and you can see the token value.
I am a noob, trying to do JWT the simplest possible way on a .NET Core 6 Web API project, but I can't even get it to work.
Requirement: You need to be logged in to call the GetProductList API.
(I am testing this on Swagger that comes with the project)
FYI, my Login controller: (working as intended)
[HttpPost("login")]
public async Task<ActionResult> Login(LoginDto request)
{
var user = GetUserFromRequest(request);
if (user == null)
return BadRequest("Invalid credentials.");
string jwt = CreateJwtToken(user.Id.ToString());
Response.Cookies.Append(COOKIE_JWT, jwt, _cookieOptions);
return Ok();
}
[HttpGet("user")]
public IActionResult GetUser()
{
try
{
var jwt = Request.Cookies[COOKIE_JWT];
var userId = VerifyJwtAndGetUserId(jwt);
return Ok(GetUserById(userId));
}
catch(Exception ex)
{
return Unauthorized();
}
}
public static string CreateJwtToken(string userId)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY));
var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var token = new JwtSecurityToken(
issuer: userId,
expires: DateTime.Now.AddDays(365),
signingCredentials: cred
);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return jwt;
}
public static string VerifyJwtAndGetUserId(string jwt)
{
var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(jwt, new TokenValidationParameters {
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY)),
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false
}, out SecurityToken validatedToken);
string userId = validatedToken.Issuer;
return userId;
}
The question is, how can I make the [Authorize] attribute work?
[HttpGet("list")]
//[Authorize]
public async Task<ActionResult<List<Product>>> GetProductList()
{
return Ok(GetProducts());
}
The above works, but adding [Authorize] attribute gives a 401 with the following header: (while GetUser above is fine)
content-length: 0
date: Mon,13 Jun 2022 23:27:32 GMT
server: Kestrel
www-authenticate: Bearer
This is what's in my Program.cs: (maybe this is wrong?)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters { // similar to the one in controller
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY)),
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false
};
options.Events = new JwtBearerEvents { // https://spin.atomicobject.com/2020/07/25/net-core-jwt-cookie-authentication/
OnMessageReceived = ctx => {
ctx.Token = ctx.Request.Cookies["jwt"];
return Task.CompletedTask;
}
};
});
SOLUTION:
Move app.UseAuthentication(); above app.UserAuthorization();.
Summary
FrontEnd
According to JWT Introduction as below
Whenever the user wants to access a protected route or resource, the
user agent should send the JWT, typically in the Authorization header
using the Bearer schema. The content of the header should look like
the following:
Authorization: Bearer [token]
So you need to add a header Authorization: Bearer <token> in your request on frontEnd.
a simple example at frontEnd. In this example the yourApiUrl should be your list api route.
const token = getCookieValue(`COOKIE_JWT`);
fetch(yourApiUrl, {
headers: {
"Authorization": `Bearer ${token}`,
}
})
getCookieValue function
const getCookieValue(name) =>{
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''
}
then your request will have the authorization header with a prefix [Bearer].
a picture from my browser's dev tool.
and then the [Authorize] attribute should work
BackEnd
Maybe it is not the problem at frontEnd, then you can check the backEnd's code.
Some code example at backEnd .NET 6 WebApi about generate or examine the JWT token
GenerateToken function
Use to generate token when sign-in controller
public string GenerateToken(TokenModel tokenModel, int
expireMinutes = 30)
{
var issuer = this._configuration.GetValue<string> ("JwtSettings:Issuer");
var signKey = this._configuration.GetValue<string>
("JwtSettings:SignKey");
// Configuring "Claims" to your JWT Token
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Iss, issuer),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID
new Claim(JwtRegisteredClaimNames.Sub, tokenModel.EmployeeNo), // User.Identity.Name
new Claim(JwtRegisteredClaimNames.NameId, tokenModel.EmployeeNo),
new Claim(JwtRegisteredClaimNames.Name, tokenModel.EmployeeName),
};
for (int i = 0; i < tokenModel.Roles.Length; i++)
{
claims.Add(new Claim("roles", tokenModel.Roles[i]));
}
var userClaimsIdentity = new ClaimsIdentity(claims);
// Create a SymmetricSecurityKey for JWT Token signatures
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signKey));
// HmacSha256 MUST be larger than 128 bits, so the key can't be too short. At least 16 and more characters.
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
// Create SecurityTokenDescriptor
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = userClaimsIdentity,
NotBefore = DateTime.Now, // Default is DateTime.Now
IssuedAt = DateTime.Now, // Default is DateTime.Now
Expires = DateTime.Now.AddMinutes(expireMinutes),
SigningCredentials = signingCredentials
};
// Generate a JWT securityToken, than get the serialized Token result (string)
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var serializeToken = tokenHandler.WriteToken(securityToken);
return serializeToken;
}
Program.cs
.NET6 WebApi's setting in Program.cs about examine Jwt token.
📌 [NOTICE]: the method builder.Services.AddAuthorization(); MUST below the builder.Services.AddAuthentication method, or
will result error.
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) //check the HTTP Header's Authorization has the JWT Bearer Token
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
// varify Issuer
ValidateIssuer = true,
ValidIssuer = builder.Configuration.GetValue<string>("JwtSettings:Issuer"),
// 📌 Important!!! audience need to be set to false
// Because the default is true.
// So if you didn't set this prop when generate token, then the api will always return a check token error
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration.GetValue<string>("JwtSettings:SignKey")))
};
});
builder.Services.AddAuthorization();
Easy Test by thunder client of VsCode
Use the thumder client VsCode to test the api route.
Use the swagger's login controller to get the JWTToken.
Then paste the token in thunder client, and add the prefix Bearer
a simple image of thunder client
I'm asking for help to verify the GCP token (x-gcp-marketplace-token).
Basically I would like to implement a token verification explained here.
Expected language C#. preferable to be compatible with .Net Core.
UPDATE
This is what I have so far. It is a POC don't worry about fields naming.
Actually validation fails.
private async Task<OrderViewModel> GoogleResolveOrder(string token)
{
OrderViewModel result = new OrderViewModel { Transaction = new TransactionViewModel() };
//Decode token
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
JwtSecurityToken tokenInfo = handler.ReadJwtToken(token);
if (!String.IsNullOrEmpty(tokenInfo.Header.Kid) && !String.IsNullOrEmpty(tokenInfo.Payload.Sub) && !String.IsNullOrEmpty(tokenInfo.Payload.Iss))
{
string keyId = tokenInfo.Header.Kid;
string keysEndpoint = tokenInfo.Payload.Iss;
string procurementId = tokenInfo.Payload.Sub;
//load gcp certificate
var response = await _client.GetAsync(keysEndpoint);
string googleCretificateInfoResponse = await response.Content.ReadAsStringAsync();
Dictionary<string, string> certifateInfo = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(googleCretificateInfoResponse);
//Get public key from certificate
string certificatePublicKeyText = certifateInfo[keyId];
byte[] certicateStream = Encoding.UTF8.GetBytes(certificatePublicKeyText);
X509Certificate certificate = new X509Certificate(certicateStream);
try
{
// Verify token with public key, leave private key as null
SecurityToken tokenValidated = null;
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidateLifetime = true,
ValidateAudience = true,
ValidAudience = tokenInfo.Payload.Aud[0],
ValidateIssuer = true,
ValidIssuer = "https://www.googleapis.com/robot/v1/metadata/x509/cloud-commerce-partner#system.gserviceaccount.com",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(certificate.GetPublicKey())
};
ClaimsPrincipal clainsResult = handler.ValidateToken(token, validationParameters, out tokenValidated);
//TODO- IF not exception Token is valid
result.Transaction.MinimunQuantity = 1;
}
catch (Exception ex)
{
result.Transaction.MinimunQuantity = -1;
result.Transaction.FulfillmentStatus = ex.Message;
}
//TODO- Delete
result.Transaction.OrgId = tokenInfo.Payload.Sub;
result.Transaction.PlanName = tokenInfo.Payload.Iss;
result.Transaction.PlanID = tokenInfo.Header.Kid;
result.Transaction.Customer = String.Join(',', tokenInfo.Payload.Aud);
result.Transaction.CustomerEmailId = tokenInfo.Payload.Exp.Value.ToString();
result.Transaction.SubscriptionId = token;
}
return result;
}
I'm still working on this.
Getting this error:
IDX10501: Signature validation failed. Unable to match key: kid: '[PII is hidden. For more details, see aka.ms/IdentityModel/PII.]'. Exceptions caught: '[PII is hidden. For more details, see aka.ms/IdentityModel/PII.]'. token: '[PII is hidden. For more details, see aka.ms/IdentityModel/PII.]'.
I am writing C# code that runs against an Azure cloud. My application is an ASP.NET Core web service that exposes methods but no UI.
Most (not all) of my methods require a JSON Web Token (JWT) for authorization. I have this in my Startup.cs:
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration["AppSettings:IdentityAuthEndpoint"];
options.RequireHttpsMetadata = bool.Parse(Configuration["AppSettings:RequireHttps"]);
options.ApiName = "data";
options.EnableCaching = true;
});
This throws an error if the JWT is missing or completely bad. But if the JWT is just expired, the above lets the user through.
According to this Stack Overflow answer, I can check that context.User.Claims is empty: https://stackoverflow.com/a/62289801/4290962
That seems to be true. But is there a standard best-practice way to detect this and throw a standardized error? Or do I need to write custom code to check the claims? I can do that, of course. I'm just thinking there ought to be a prettier solution.
Thanks in advance!
I would recommend you to write a middleware for this, This will give you better control on what you want to achieve. Like you said it's good to look for an existing solution, but here's one that I'm using in one of my enterprise application.
Intercept Token - Using this middleware on every request
public class JwtMiddleware
{
private readonly RequestDelegate _next;
public JwtMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, IUserService userService)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (token != null) AttachUserToContext(context, userService, token);
await _next(context);
}
private void AttachUserToContext(HttpContext context, IUserService userService, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(AppConfigBuilder.Build().Security.JwtSecret);
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
context.Items["User"] = userService.GetUserById(userId);
}
catch
{
}
}
}
Register it on StartUp
app.UseMiddleware<JwtMiddleware>();
Then create an Authentication filter
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var user = (User)context.HttpContext.Items["User"];
if (user == null)
{
context.Result = new JsonResult(new Response {
IsSuccess = false,
ResponseStatus = ResponseStatus.ERROR,
Message="Request terminated. Unauthorized access to protected resource.",
Info=new List<string>() {
"Verify the auth token sending through this request",
"Verify if your token is invalid or expired",
"Request for a new token by logging in again" }
})
{ StatusCode = StatusCodes.Status401Unauthorized };
}
}
}
That's all need to do. Now for any endpoint to be secured, Just use [Authorize] attribute like this. This will validate your token and emit appropriate standard response.
[HttpGet]
[Authorize]
public IActionResult Get()
{
//Boilerplate code
var response = userService.GetAllUsers();
return (response.IsSuccess) ? Ok(response) : BadRequest(response);
}
Just for info: You can define your own standard Response class on Authorize filter. The one I'm using is from ExpressGlobalExceptionHandler library.
This is the JWT token generation logic I've created
private string GenerateJwtToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(appConfig.Security.JwtSecret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
I ended up doing the below, based on this answer: https://stackoverflow.com/a/34423434/4290962
Note that _rejectExpiredJwt is a configurable boolean.
public JwtSecurityToken ValidateToken(string tokenStr)
{
var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(
new RSAParameters
{
Modulus = FromBase64Url(_modulus),
Exponent = FromBase64Url(_exponent)
});
var validationParameters = new TokenValidationParameters
{
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidateAudience = false,
ValidateIssuer = true,
ValidIssuer = _issuer,
ValidateLifetime = _rejectExpiredJwt,
IssuerSigningKey = new RsaSecurityKey(rsa)
};
var handler = new JwtSecurityTokenHandler();
handler.ValidateToken(tokenStr, validationParameters, out SecurityToken validatedSecurityToken);
var validatedJwt = validatedSecurityToken as JwtSecurityToken;
return validatedJwt ?? throw new UnauthorizedException(
$"Token is not a {nameof(JwtSecurityToken)} but a {validatedSecurityToken.GetType().Name}.");
static byte[] FromBase64Url(string base64Url)
{
string padded = base64Url.Length % 4 == 0
? base64Url
: base64Url + "====".Substring(base64Url.Length % 4);
string base64 = padded.Replace("_", "/")
.Replace("-", "+");
return Convert.FromBase64String(base64);
}
}
I am having some trouble manually validating a JWT token issued by Identity Server 4. Using the
ClientId: "CLIENT1"
ClientSecret: "123456"
The exception I keep getting is: IDX10501: Signature validation failed. Unable to match keys: '[PII is hidden by default. Set the 'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]'
Is anyone able to advise me where I am going wrong.
private static void ValidateJwt(string jwt, DiscoveryResponse disco)
{
var parameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidIssuer = disco.Issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("123456")),
ValidAudience = "CLIENT1",
//IssuerSigningKeys = keys,
// ValidateAudience = true,
// ValidateLifetime = true,
};
SecurityToken validatedToken;
var handler = new JwtSecurityTokenHandler();
handler.InboundClaimTypeMap.Clear();
try
{
var user = handler.ValidateToken(jwt, parameters, out validatedToken);
}
catch(Exception ex)
{
var error = ex.Message;
}
}
Check out ValidateJwt() in this sample:
https://github.com/IdentityServer/IdentityServer4/blob/master/samples/Clients/old/MvcManual/Controllers/HomeController.cs
The bit you're missing is loading the public key from the discovery document.
Try changing the length of your private key. Your private key is too small to be encoded I suppose.
For manual verification, you could just use
static byte[] FromBase64Url(string base64Url)
{
string padded = base64Url.Length % 4 == 0
? base64Url : base64Url + "====".Substring(base64Url.Length % 4);
string base64 = padded.Replace("_", "/")
.Replace("-", "+");
return Convert.FromBase64String(base64);
}
This also answers #henk-holterman 's question
Encoding.UTF8.GetBytes( can't be the right way to do this.
Although realistically a better way to do this is via the OIDC discovery-endpoint
Auth0 has a good article about this using standard NuGet packages. Basically, you load everything needed from the discovery end-point.
IConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{auth0Domain}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
TokenValidationParameters validationParameters =
new TokenValidationParameters
{
ValidIssuer = auth0Domain,
ValidAudiences = new[] { auth0Audience },
IssuerSigningKeys = openIdConfig.SigningKeys
};
SecurityToken validatedToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
var user = handler.ValidateToken("eyJhbGciOi.....", validationParameters, out validatedToken);
You can read more about it here Or their GitHub sample page about this here
In my case, I did not have a discovery endpoint. Just a JWKS endpoint.
So I opted to do this.
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using RestSharp;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Threading.Tasks;
public class ExpectedJwksResponse
{
[JsonProperty(PropertyName = "keys")]
public List<JsonWebKey> Keys { get; set; }
}
private static async Task<List<SecurityKey>> GetSecurityKeysAsync()
{
// Feel free to use HttpClient or whatever you want to call the endpoint.
var client = new RestClient("<https://sample-jwks-endpoint.url>");
var request = new RestRequest(Method.GET);
var result = await client.ExecuteTaskAsync<ExpectedJwksResponse>(request);
if (result.StatusCode != System.Net.HttpStatusCode.OK)
{
throw new Exception("Wasnt 200 status code");
}
if (result.Data == null || result.Data.Keys == null || result.Data.Keys.Count == 0 )
{
throw new Exception("Couldnt parse any keys");
}
var keys = new List<SecurityKey>();
foreach ( var key in result.Data.Keys )
{
keys.Add(key);
}
return keys;
}
private async Task<bool> ValidateToken(token){
TokenValidationParameters validationParameters = new TokenValidationParameters
{
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidateLifetime = true,
ValidIssuer = "https://sample-issuer.com",
ValidAudiences = new[] { "https://sample-audience/resource" },
IssuerSigningKeys = await GetSecurityKeysAsync()
};
var user = null as System.Security.Claims.ClaimsPrincipal;
SecurityToken validatedToken;
try
{
user = handler.ValidateToken(token, validationParameters, out validatedToken);
}
catch ( Exception e )
{
Console.Write($"ErrorMessage: {e.Message}");
return false;
}
var readToken = handler.ReadJwtToken(token);
var claims = readToken.Claims;
return true;
}
You have specified:
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secret"))
but the JwtSecurityTokenHandler could not match it with the key which can be part of jwt header itself. Basically it means that your configuration has mismatch[es] with configuration of the real issuer. The error suggests that this relates to the signature keys.
Please, check the configuration of that issuer (if you can), find out missed parts, and try again.
You can use jwt.io to debug your jwt online.
IdentityServer signs the JWT using RS256. This means you need to use a public key to verify the JWT (you can get this from the discovery document).
The client id & client secret are client credentials used for requesting tokens. They have no part in validating them.
You are trying to use SymmetricKey for JWT validation. Try looking your token in JWT.io and if algorithm is"RS256" then SymmetricKey won't work.
Please check when you create JWT token make sure that you added SigningCredentials.
var token = new JwtSecurityToken(
Constants.Audiance,Constants.Issuer,claims
notBefore:DateTime.Now,
expires:DateTime.Now.AddHours(1),
**signinCredential**
);