I'm trying to validate a token that was generated in Azure Active Directory.
But the result of the conversion is an error. I've tried changing several settings, but to no avail.
I'm implementing the test in a Console Application.
What could be wrong?
Error
IDX10511: Signature validation failed. Keys tried: 'Microsoft.IdentityModel.Tokens.X509SecurityKey, KeyId: '2ZQpJ3UpbjAYXYGaXEJl8lV0TOI', InternalId: '2ZQpJ3UpbjAYXYGaXEJl8lV0TOI'. , KeyId: 2ZQpJ3UpbjAYXYGaXEJl8lV0TOI
'.
Number of keys in TokenValidationParameters: '14'.
Number of keys in Configuration: '0'.
Matched key was in 'TokenValidationParameters'.
kid: '2ZQpJ3UpbjAYXYGaXEJl8lV0TOI'.
Exceptions caught:
''.
token: '{"typ":"JWT","nonce":"Dl7E6r5PvOq2hGS909Qdgz6KiLjXBsLUS_XhofbRH1k","alg":"RS256","x5t":"2ZQpJ3UpbjAYXYGaXEJl8lV0TOI","kid":"2ZQpJ3UpbjAYXYGaXEJl8lV0TOI"}.{"aud":"https://graph.microsoft.com","iss":"https://sts.windows.net/f3211d0e-125b-42c3-86db-322b19a65a22/","iat":1664377202,"nbf":1664377202,"exp":1664381102,"aio":"E2ZgYLD1ms4yn3nR69oIsXXzJ/DoAwA=","app_displayname":"66943_Portal_Claro_NDI_PRD_Staging_ESOPortal","appid":"8bd88acb-22ff-4698-9e35-eb877d48e837","appidacr":"1","idp":"https://sts.windows.net/f3211d0e-125b-42c3-86db-322b19a65a22/","idtyp":"app","oid":"d7ff5f9f-86f3-4e6d-9203-33b820b90e73","rh":"0.ASYADh0h81sSw0KG2zIrGaZaIgMAAAAAAAAAwAAAAAAAAAAmAAA.","sub":"d7ff5f9f-86f3-4e6d-9203-33b820b90e73","tenant_region_scope":"NA","tid":"f3211d0e-125b-42c3-86db-322b19a65a22","uti":"3bt0MgzfykqBvr1x789_AA","ver":"1.0","wids":["0997a1d0-0d1d-4acb-b408-d5ca73121e90"],"xms_tcdt":1403205942}'.
Token JWT
eyJ0eXAiOiJKV1QiLCJub25jZSI6IkRsN0U2cjVQdk9xMmhHUzkwOVFkZ3o2S2lMalhCc0xVU19YaG9mYlJIMWsiLCJhbGciOiJSUzI1NiIsIng1dCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGxWMFRPSSIsImtpZCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGxWMFRPSSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9mMzIxMWQwZS0xMjViLTQyYzMtODZkYi0zMjJiMTlhNjVhMjIvIiwiaWF0IjoxNjY0Mzc3MjAyLCJuYmYiOjE2NjQzNzcyMDIsImV4cCI6MTY2NDM4MTEwMiwiYWlvIjoiRTJaZ1lMRDFtczR5bjNuUjY5b0lzWFh6Si9Eb0F3QT0iLCJhcHBfZGlzcGxheW5hbWUiOiI2Njk0M19Qb3J0YWxfQ2xhcm9fTkRJX1BSRF9TdGFnaW5nX0VTT1BvcnRhbCIsImFwcGlkIjoiOGJkODhhY2ItMjJmZi00Njk4LTllMzUtZWI4NzdkNDhlODM3IiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvZjMyMTFkMGUtMTI1Yi00MmMzLTg2ZGItMzIyYjE5YTY1YTIyLyIsImlkdHlwIjoiYXBwIiwib2lkIjoiZDdmZjVmOWYtODZmMy00ZTZkLTkyMDMtMzNiODIwYjkwZTczIiwicmgiOiIwLkFTWUFEaDBoODFzU3cwS0cyeklyR2FaYUlnTUFBQUFBQUFBQXdBQUFBQUFBQUFBbUFBQS4iLCJzdWIiOiJkN2ZmNWY5Zi04NmYzLTRlNmQtOTIwMy0zM2I4MjBiOTBlNzMiLCJ0ZW5hbnRfcmVnaW9uX3Njb3BlIjoiTkEiLCJ0aWQiOiJmMzIxMWQwZS0xMjViLTQyYzMtODZkYi0zMjJiMTlhNjVhMjIiLCJ1dGkiOiIzYnQwTWd6ZnlrcUJ2cjF4Nzg5X0FBIiwidmVyIjoiMS4wIiwid2lkcyI6WyIwOTk3YTFkMC0wZDFkLTRhY2ItYjQwOC1kNWNhNzMxMjFlOTAiXSwieG1zX3RjZHQiOjE0MDMyMDU5NDJ9.uIBkYSdbR_GcnI-QCM0FI-TJfu5Q5HotlWDB8ee-UqAL-7j8Sg2AqFflwfsF9Qsu4qlsZU7ymISY_SEzWxrsTpAxVYvCdNfuki7tm1WTyhiN_fDZWKM9VGTUrXxFNd5FQL7cAMkq6JfjO13fRs4R7ZOTlWtWS0DCOrZ2Cy506he1Ip0AXAGJFwLKD3aZ8-6ZIV5hHGoluUwE78OSWHCMVXzaliYLhfiYPmGSFqP6OK3AaQysFjEbN_54zh9jy9GrEvGQHCYXN0sueDao2n77qkagRrC67W0pkJHvFLCqTypy1FJfhhpZyKtzLbqC1tmOLPZv_a0_sgJY1Z245l-pug
My Code
private static JwtSecurityToken ValidateAzureToken(string token)
{
string _tenantId = ConfigurationManager.AppSettings["tenantId"];
string _clientId = ConfigurationManager.AppSettings["clientId"];
string _authority = ConfigurationManager.AppSettings["authority"];
string _audience = ConfigurationManager.AppSettings["audience"];
string authority = string.Format(CultureInfo.InvariantCulture, _authority, _tenantId);
ConfigurationManager<OpenIdConnectConfiguration> _configManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{authority}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration config = _configManager.GetConfigurationAsync().Result;
IList<string> validissuers = new List<string>()
{
$"https://login.microsoftonline.com/{_tenantId}/",
$"https://login.microsoftonline.com/{_tenantId}/v2.0",
$"https://login.windows.net/{_tenantId}/",
$"https://login.microsoft.com/{_tenantId}/",
$"https://sts.windows.net/{_tenantId}/"
};
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidAudiences = new[] { _audience, _clientId },
ValidIssuers = validissuers,
IssuerSigningKeys = config.SigningKeys
};
IdentityModelEventSource.ShowPII = true;
SecurityToken securityToken;
try
{
var claims = new JwtSecurityTokenHandler().ValidateToken(token, validationParameters, out securityToken);
}
catch (Exception e)
{
return null;
}
return securityToken as JwtSecurityToken;
}
Related
I am generating a valid JWT in my API, and returning it with an expiry embedded. See code and example below:
public static string GenerateToken(string securityKey,
string claimName, string issuer, RedisManagerPool redisClient)
{
var claims = new[]
{
new Claim(ClaimTypes.Name,
claimName)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expiry = DateTime.Now.AddMinutes(UtilityCommand.Cache.GetCacheValue<int>(Functions.ParameterPath
+ Functions.Integration
+ Functions.JWT
+ "/expiry_minutes", redisClient));
var token = new JwtSecurityToken(
issuer: issuer,
audience: issuer,
claims: claims,
expires: expiry,
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Example:
{
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "scpi",
"exp": 1598960076,
"iss": "https://lcsapi",
"aud": "https://lcsapi"
}
But no matter how long I use the token for, it never expires? Where am I going wrong here? Even if I set the expiry to something like 1 minute. Here is my auth check:
public APIGatewayCustomAuthorizerResponse GetAuthentication(APIGatewayCustomAuthorizerRequest authorizerRequest, ILambdaContext context)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = Issuer,
ValidateAudience = true,
ValidateLifetime = UtilityCommand.Cache.GetCacheValue<bool>(ParameterPath + Integration + JWT + "/jwtexpires", _redisClient), // testing
ValidAudience = Issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey)),
ClockSkew = TimeSpan.FromMinutes(5), // Required to account for potential drift times between systems.
ValidateIssuerSigningKey = true
};
SecurityToken validatedToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
bool authorized = false;
if (!string.IsNullOrWhiteSpace(authorizerRequest.AuthorizationToken))
{
try
{
var jwt = authorizerRequest.AuthorizationToken.Replace("Bearer ", string.Empty);
var user = handler.ValidateToken(jwt, tokenValidationParameters, out validatedToken);
var claim = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name);
if (claim != null)
authorized = claim.Value == ClaimName; // Ensure that the claim value matches the assertion
}
catch (Exception ex)
{
context.Logger.LogLine($"Error occurred validating token: {ex.Message}");
}
}
else
{
context.Logger.LogLine($"Error occurred validating token: No token provided.");
}
return GenerateAuthorizerResponse(authorized, authorizerRequest, context);
}
Glad you've resolved it. Worth making sure you are checking expiry against DateTime.UtcNow, since the expiry claim is a UTC value.
Answer, I needed to add the following to my TokenValidationParameters.
LifetimeValidator = LifetimeValidator,
Which accepts a delegate function that checks the expiry (I didn't realise this wasn't handled automatically). It's just a bool return on whether the expiry is passed:
private bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken token, TokenValidationParameters #params)
{
if (expires != null)
{
return expires > DateTime.Now;
}
return false;
}
I'm trying to use JWT tokens. I managed to generate a valid JWTTokenString and validated it on the JWT debugger but I'm having an impossible time validating the token in .Net. Here's the code I have so far:
class Program {
static string key = "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429090fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1";
static void Main(string[] args) {
var stringToken = GenerateToken();
ValidateToken(stringToken);
}
private static string GenerateToken() {
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var header = new JwtHeader(credentials);
var payload = new JwtPayload {
{ "some ", "hello "},
{ "scope", "world"},
};
var secToken = new JwtSecurityToken(header, payload);
var handler = new JwtSecurityTokenHandler();
return handler.WriteToken(secToken);
}
private static bool ValidateToken(string authToken) {
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = GetValidationParameters();
SecurityToken validatedToken;
IPrincipal principal = tokenHandler.ValidateToken(authToken, validationParameters, out validatedToken);
Thread.CurrentPrincipal = principal;
return true;
}
private static TokenValidationParameters GetValidationParameters() {
return new TokenValidationParameters() {
//NOT A CLUE WHAT TO PLACE HERE
};
}
}
All I want is a function that receives a token and returns true or false based on its validity. From research I've seen people use IssuerSigningToken to assign the validation key. But when I try to use it, it doesn't seem to exist. Could anyone give me a hand on validating the token?
You must use the same key to validate the token as the one you use to generate it. Also you need to disable some validations such as expiration, issuer and audiance, because the token you generate doesn't have these information (or you can add these information). Here's a working example:
class Program
{
static string key = "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429090fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1";
static void Main(string[] args)
{
var stringToken = GenerateToken();
ValidateToken(stringToken);
}
private static string GenerateToken()
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var secToken = new JwtSecurityToken(
signingCredentials: credentials,
issuer: "Sample",
audience: "Sample",
claims: new[]
{
new Claim(JwtRegisteredClaimNames.Sub, "meziantou")
},
expires: DateTime.UtcNow.AddDays(1));
var handler = new JwtSecurityTokenHandler();
return handler.WriteToken(secToken);
}
private static bool ValidateToken(string authToken)
{
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = GetValidationParameters();
SecurityToken validatedToken;
IPrincipal principal = tokenHandler.ValidateToken(authToken, validationParameters, out validatedToken);
return true;
}
private static TokenValidationParameters GetValidationParameters()
{
return new TokenValidationParameters()
{
ValidateLifetime = false, // Because there is no expiration in the generated token
ValidateAudience = false, // Because there is no audiance in the generated token
ValidateIssuer = false, // Because there is no issuer in the generated token
ValidIssuer = "Sample",
ValidAudience = "Sample",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)) // The same key as the one that generate the token
};
}
}
In my case I only want to validate if the signature is correct. Most likely you will probably want to use #meziantou answer. But if you only want to verify that the message has not been tampered with here is an example. Lastly since I am the only person generating this tokens and I know I will be generating them using HmacSha256 I am using this approach.
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
class Program
{
static readonly byte[] key = Encoding.UTF8.GetBytes("f645b33ef0d04cbe859777ac6f46226d");
// use this algorithm for example to work
static readonly string securityAlgorithm = SecurityAlgorithms.HmacSha256;
static void Main()
{
var token = GenerateToken();
var isTokenValid = IsJwtTokenValid(token);
if (isTokenValid)
Console.WriteLine(true);
}
/// <summary>
/// This method assumes token has been hashed using HMACSHA256 algorithm!
/// </summary>
private static bool IsJwtTokenValid(string token)
{
// example of token:
// header payload signature
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb29AZ21haWwuY29tIiwiZXhwIjoxNjQ1NzM1MDU2fQ.Gtrm2G_35ynyNd1-CjZ1HsvvFFItEsXPvwhaOsN81HQ
// from JWT spec
static string Base64UrlEncode(byte[] input)
{
var output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // Remove any trailing '='s
output = output.Replace('+', '-'); // 62nd char of encoding
output = output.Replace('/', '_'); // 63rd char of encoding
return output;
}
try
{
// position of second period in order to split header+payload and signature
int index = token.IndexOf('.', token.IndexOf('.') + 1);
// Example: Gtrm2G_35ynyNd1-CjZ1HsvvFFItEsXPvwhaOsN81HQ
string signature = token[(index + 1)..];
// Bytes of header + payload
// In other words bytes of: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb29AZ21haWwuY29tIiwiZXhwIjoxNjQ1NzM1MDU2fQ
byte[] bytesToSign = Encoding.UTF8.GetBytes(token[..index]);
// compute hash
var hash = new HMACSHA256(key).ComputeHash(bytesToSign);
var computedSignature = Base64UrlEncode(hash);
// make sure that signatures match
return computedSignature.Length == signature.Length
&& computedSignature.SequenceEqual(signature);
}
catch
{
return false;
}
}
private static string GenerateToken()
{
var securityKey = new SymmetricSecurityKey(key);
var credentials = new SigningCredentials(securityKey, securityAlgorithm);
var secToken = new JwtSecurityToken(
signingCredentials: credentials,
claims: new[]
{
new Claim(JwtRegisteredClaimNames.Sub, "foo#gmail.com")
},
expires: DateTime.UtcNow.AddDays(1));
var handler = new JwtSecurityTokenHandler();
return handler.WriteToken(secToken);
}
}
Validate Token in Jwt Middleware Class that Invoke Method fire in every request for Authorization
JwtMiddleware
{
private readonly RequestDelegate _next;
private readonly TokenValidationParameters _tokenValidationParams;
public JwtMiddleware(RequestDelegate next, TokenValidationParameters
tokenValidationParams)
{
_next = next;
_tokenValidationParams = tokenValidationParams;
}
public async Task Invoke(HttpContext context)
{
try{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
var jwtTokenHandler = new JwtSecurityTokenHandler();
// Validation 1 - Validation JWT token format
var tokenInVerification = jwtTokenHandler.ValidateToken(token, _tokenValidationParams, out var validatedToken);
if (validatedToken is JwtSecurityToken jwtSecurityToken)
{
var result = jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase);
if (result == false)
{
Error Invalid = new Error()
{
Success = false,
Errors = "Token is Invalid"
};
context.Items["Error"] = Invalid;
}
}
}
catch (Exception ex)
{
Error Invalid = new Error()
{
Success = false,
Errors = "Token does not match or may expired."
};
context.Items["Error"] = Invalid ; // userService.GetById(userId);
}
await _next(context);
}
}
I have JwtToken created like this:
X509Certificate2 cert = certificateStore.Certificate;
var now = DateTime.UtcNow;
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor()
{
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtClaimTypes.Subject, upn),
new Claim(REQUEST_TYPE_NAME, requestType),
new Claim(DOMAIN_NAME, domain),
}),
Lifetime = new Lifetime(now, now.AddMinutes(60)),
SigningCredentials = new X509SigningCredentials(cert),
TokenIssuerName = ISSUER
};
SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);
This is correct. Token is created and its first claim is named "sub" which is content of JwtTokenTypes.Subject. I checked it through jwt web.
Problem is, I have this method for resolving claims:
if (string.IsNullOrWhiteSpace(token)) throw new MissingTokenException("Token should not be null.");
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = new X509SecurityToken(new X509Certificate2(new X509RawDataKeyIdentifierClause(certificateStore.Certificate).GetX509RawData()));
var validationParameters = new TokenValidationParameters()
{
IssuerSigningToken = securityToken,
ValidateAudience = false,
ValidateActor = false,
ValidIssuer = ISSUER
};
SecurityToken securedToken = new JwtSecurityToken();
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out securedToken);
Claim claim = claimsPrincipal.FindFirst(m => string.Equals(m.Type, REQUEST_TYPE_NAME, StringComparison.OrdinalIgnoreCase));
if (claim != null && !string.Equals(claim.Value, requestType, StringComparison.OrdinalIgnoreCase))
{
throw new MismatchedTokenException("Token is not of the proper type.");
}
upn = claimsPrincipal.Claims.FirstOrDefault(m => m.Type.Equals(JwtClaimTypes.Subject) || m.Type.Equals(ClaimTypes.NameIdentifier))?.Value;
domain = claimsPrincipal.Claims.FirstOrDefault(m => m.Type.Equals(DOMAIN_NAME))?.Value;
In the end of the method you can see I check claim name for JwtClaimTypes.Subject, which it should be, and for ClaimTypes.NameIdentifiew, which it actually is.
Do you have any idea why this transformations occur or how to prevent them?
You need to add before:
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
that clears the mapping of claims for jwt.
The mapping is done to adapt the jwt claims to .net types of claims.
If you avoid the mapping you need to set the claim types for name and role if you want to use that from the Identity (IPrincipal.IsInRole or Identity.Name).
You can do this when creating the ClaimsIdentity:
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtClaimTypes.Subject, upn),
new Claim(REQUEST_TYPE_NAME, requestType),
new Claim(DOMAIN_NAME, domain),
}, "<auth type>", "name", "role"),
you'll have to change the authType to something like cookie, and the name and role claim name most probably would be like that.
The other way is to set it on the token validation parameter:
var validationParameters = new TokenValidationParameters()
{
IssuerSigningToken = securityToken,
ValidateAudience = false,
ValidateActor = false,
ValidIssuer = ISSUER,
NameClaimType = "name",
RoleClaimType = "role"
};
I have a bit of a Frankenstien service in that it has endpoints for both SOAP and REST hosted on the same URL by the same code base. I'm using the client credentials grant flow to successfully secure the REST endpoints, but would like to use the same process to secure the SOAP calls. The startup.cs initializes the Identity server bearer token authentication like so:
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["IdentityServerUrl"],
RequiredScopes = new[] { ConfigurationManager.AppSettings["IdentityServerScopes"] }
});
And for the REST endpoints I add an
[Authorize]
code decoration and everything works. For the SOAP side I repurposed the password field and have sent the token through that and can decode it like so:
string sPassword = request.Authentication.Password;
if (sPassword.Contains("."))
{
"\nAccess Token (decoded):".ConsoleGreen();
var parts = sPassword.Split('.');
var header = parts[0];
var claims = parts[1];
Console.WriteLine(JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(header))));
Console.WriteLine(JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(claims))));
}
I can see the claims but this isn't validating the token. From here I've pieced together a ValidateToken method that throws exceptions about the Signature validation failed. Unable to resolve SecurityKeyIdentifier. I'm fairly certain that everything has been signed by the IdentityServer3 cert, but I'm stuck trying to create a cert. I don't have any certs in my KeyStore and would like a solution that doesn't require that I insert the cert in the KeyStore. Here is the attempt:
public static bool VerifyToken(string token)
{
const string thumbPrint = "6bf8e136eb36d4a56ea05c7ae4b9a45b63bf975d"; // correct thumbprint of certificate
var cert = X509CertificateHelper.FindByThumbprint(StoreName.My, StoreLocation.LocalMachine, thumbPrint).First();
var validationParameters = new TokenValidationParameters()
{
//IssuerSigningToken = new BinarySecretSecurityToken(_key),
IssuerSigningToken = new X509SecurityToken(cert),
ValidAudience = "https://securityeli.twcable.com/core/resources",
ValidIssuer = "https://securityeli.twcable.com/core",
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true
//ValidateIssuerSigningKey = true
};
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken validatedToken = null;
try
{
tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
//... manual validations return false if anything untoward is discovered
return validatedToken != null;
}
public class X509CertificateHelper
{
public static IEnumerable<X509Certificate2> FindByThumbprint(StoreName storeName, StoreLocation storeLocation, string thumbprint)
{
var store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
foreach (var certificate in certificates)
{
yield return certificate;
}
store.Close();
}
}
The current process doesn't work because I have no keys in my keystore. The BinarySecretSecurityToken failed because I don't know the key length?
I'm also going to come back to the REST side of the house, it validates the bearer token using the Authorize tag, so I should have access to the cert but have no idea how to get it out of the application. I can see in Startup it get passed IAPPBuilder app that I haven't been able to access.
Two questions are how to I create a cert to validate a token created in IdentityServer3 in C#? And can I retrieve that cert somehow?
After trying multiple paths I finally found something that works, I'm going to try and capture the relevant parts in case someone else is ever trying to do the same thing.
First I split the incoming token into it's parts:
var parts = sPassword.Split('.');
var header = parts[0];
var claims = parts[1];
var token = new JwtSecurityToken(sPassword);
I then setup some variables and called a custom VerifyToken method:
CustomResponse customResponse = null;
SecurityToken validatedToken = null;
ClaimsPrincipal claimsPrincipal = null;
if (VerifyToken(sPassword, ref customResponse, ref validatedToken, ref claimsPrincipal))
{
// Process SOAP request after authentication
}
else
return customResponse; // token wasn't authenticated, and not authorized message was set in the VerifyToken method
The VerifyToken method looks like this:
public static bool VerifyToken(string token, ref CustomResponse customResponse, ref SecurityToken validatedToken, ref ClaimsPrincipal claimsPrincipal)
{
// This was the biggest challenge in finding the cert that is used to validate the token
var certString = "Found in the CallbackController.cs in the IdentityServer3.Samples repository"
var cert = new X509Certificate2(Convert.FromBase64String(certString));
// Setting what you'd like the authorization to validate.
var validationParameters = new TokenValidationParameters()
{
IssuerSigningToken = new X509SecurityToken(cert),
ValidAudience = ConfigurationManager.AppSettings["IdentityServerUrl"] + "/resources",
ValidIssuer = ConfigurationManager.AppSettings["IdentityServerUrl"],
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true
};
var tokenHandler = new JwtSecurityTokenHandler();
try
{
claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
}
catch (SecurityTokenValidationException e)
{
//HttpContext.Current.Response.StatusCode = 401;
//statusCode = HttpStatusCode.Unauthorized;
customResponse = new CustomResponse();
customResponse.ServiceReturnStatus = new ServiceReturnStatus();
customResponse.ServiceReturnStatus.ReturnCode = -401;
customResponse.ServiceReturnStatus.ReturnMessage = "Unauthorized";
}
catch (Exception e)
{
//HttpContext.Current.Response.StatusCode = 403;
//statusCode = HttpStatusCode.InternalServerError;
customResponse = new CustomResponse();
customResponse.ServiceReturnStatus = new ServiceReturnStatus();
customResponse.ServiceReturnStatus.ReturnCode = -403;
customResponse.ServiceReturnStatus.ReturnMessage = "Internal Server Error";
}
//... manual validations return false if anything untoward is discovered
return validatedToken != null;
}
private string GetClaimFromPrincipal(ClaimsPrincipal principal, string claimType)
{
var uidClaim = principal != null && principal.Claims != null ? principal.Claims.FirstOrDefault(s => s.Type == claimType) : null;
return uidClaim != null ? uidClaim.Value : null;
}
I also added a GetClaimFromPrincipal that you can use to get claims out of the principal.
That's it, it doesn't look all that complicated, but it sure took me a lot of trial and error to get it to work. I'd still like an option that uses the Owin Startup information to validate/authorize the token because all I did was basically load all the information that I loaded in the Startup.cs like so:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["IdentityServerUrl"],
RequiredScopes = new[] { ConfigurationManager.AppSettings["IdentityServerScopes"] }
});
I am using this code to authenticate users
TokenValidationParameters tokenParams = new TokenValidationParameters();
tokenParams.ValidAudiences = new List<string> { AUTH_VALID_AUDIENCE };
tokenParams.ValidateIssuer = true;
tokenParams.IssuerValidator = new IssuerValidator(IssuerValidator);
var options = new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = tokenParams,
Tenant = AUTH_OPTIONS_TENANT,
MetadataAddress = string.Format(METADATA_ADDRESS_FORMAT, "login.windows.net")
};
app.UseWindowsAzureActiveDirectoryBearerAuthentication(options);
Is there a way to log the failure reason(eg: the exception message)?
For example, if i use expired token, i get 401(unauthorized) but i could not find the reason(in that case: expired token) for the unauthorized response.