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**
);
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'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'm using actionable messages (with Outlook web app) to call an Logic App. Therefore I am getting an Bearer token in the request:
"Action-Authorization": "Bearer eyJ0eXAi..."
Callstack:
Outlook web app -> Logic App -> my endpoint hosted in azure
Now I tried to validate the token with jwt.io but getting an Issue that the Signature is invalid.
So I tried to validate it in c# with the JwtSecurityTokenHandler.
I tried to add https://substrate.office.com/sts/ to the issuer list, but it seems like the validation don't even get there.
I'm using the following code to validate the jwt token issued by office.com:
bool IsAuthorized(HttpActionContext actionContext)
{
var valid = base.IsAuthorized(actionContext);
// Custom handle for Bearer token, when invalid from base-class
if (!valid && actionContext.Request.Headers.Authorization.Scheme == "Bearer")
{
var jwt = actionContext.Request.Headers.Authorization.Parameter;
var th = new JwtSecurityTokenHandler();
var sjwt = th.ReadToken(jwt) as JwtSecurityToken;
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = false,
//IssuerSigningToken = sjwt,
ValidateActor = false,
ValidateAudience = false,
ValidateIssuer = true,
ValidateLifetime = true,
ValidIssuers = new[] { "https://substrate.office.com/sts/" },
ValidAudiences = new[] {"https://XXX.logic.azure.com"}
};
SecurityToken validatedToken;
try
{
th.ValidateToken(jwt, validationParameters, out validatedToken);
}
catch (Exception ex)
{
return false;
}
}
return valid;
}
Here is my JWT token:
I am getting the exception:
IDX10500: Signature validation failed. Unable to resolve SecurityKeyIdentifier: 'SecurityKeyIdentifier
(
IsReadOnly = False,
Count = 2,
Clause[0] = X509ThumbprintKeyIdentifierClause(Hash = 0x818...),
Clause[1] = System.IdentityModel.Tokens.NamedKeySecurityKeyIdentifierClause
)
', ...
Even though I set ValidateIssuerSigningKey = false.
Is there a way to accept https://substrate.office.com/sts/ as a valid issuer?
The exception says that the "Signature validation failed".
To resolve this problem we can't just add the wanted valid issuer to ValidIssuers, we need the to verify that the token is issued from the issuer itself.
Especially for this case with office.com being the issuer I found the expected key (JWK - JSON Web Key) here:
https://substrate.office.com/sts/common/discovery/keys
(also https://substrate.office.com/sts/common/.well-known/openid-configuration)
Here is the working code:
bool IsAuthorized(HttpActionContext actionContext)
{
var valid = base.IsAuthorized(actionContext);
// Custom handle for Bearer token, when invalid from base-class
if (!valid && actionContext.Request.Headers.Authorization.Scheme == "Bearer")
{
var jwt = actionContext.Request.Headers.Authorization.Parameter;
var th = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new JsonWebKey(GetJWK()),
ValidIssuers = new[] { "https://substrate.office.com/sts/" }
};
Microsoft.IdentityModel.Tokens.SecurityToken validatedToken;
try
{
var claims = th.ValidateToken(jwt, validationParameters, out validatedToken);
valid = true;
}
catch (Exception ex)
{
valid = false;
}
}
return valid;
}
// Get the token from configuration
private string GetJWK()
{
return ConfigurationManager.AppSettings["ida:jwks_json"];
}
In the appsettings I put the RSA key from the website for validating the token, it looks like:
{"kty":"RSA","use":"sig","kid":"gY...","x5t":"gY...","n":"2w...","e":"AQAB","x5c":["MII..."]}
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.