I'm trying to upgrade my MVC website to use the new OpenID Connect standard. The OWIN middleware seems to be pretty robust, but unfortunately only supports the
"form_post" response type. This means that Google isn't compatible, as it returns all the tokens in a the url after a "#", so they never reach the server and never trigger the middleware.
I've tried to trigger the response handlers in the middleware myself, but that doesn't seem to work at all, so I've got a simply javascript file that parses out the returned claims and POSTs them to a controller action for processing.
Problem is, even when I get them on the server side I can't parse them correctly. The error I get looks like this:
IDX10500: Signature validation failed. Unable to resolve
SecurityKeyIdentifier: 'SecurityKeyIdentifier
(
IsReadOnly = False,
Count = 1,
Clause[0] = System.IdentityModel.Tokens.NamedKeySecurityKeyIdentifierClause
),
token: '{
"alg":"RS256",
"kid":"073a3204ec09d050f5fd26460d7ddaf4b4ec7561"
}.
{
"iss":"accounts.google.com",
"sub":"100330116539301590598",
"azp":"1061880999501-b47blhmmeprkvhcsnqmhfc7t20gvlgfl.apps.googleusercontent.com",
"nonce":"7c8c3656118e4273a397c7d58e108eb1",
"email_verified":true,
"aud":"1061880999501-b47blhmmeprkvhcsnqmhfc7t20gvlgfl.apps.googleusercontent.com",
"iat":1429556543,"exp\":1429560143
}'."
}
My token verification code follows the example outlined by the good people developing IdentityServer
private async Task<IEnumerable<Claim>> ValidateIdentityTokenAsync(string idToken, string state)
{
// New Stuff
var token = new JwtSecurityToken(idToken);
var jwtHandler = new JwtSecurityTokenHandler();
byte[][] certBytes = getGoogleCertBytes();
for (int i = 0; i < certBytes.Length; i++)
{
var certificate = new X509Certificate2(certBytes[i]);
var certToken = new X509SecurityToken(certificate);
// Set up token validation
var tokenValidationParameters = new TokenValidationParameters();
tokenValidationParameters.ValidAudience = googleClientId;
tokenValidationParameters.IssuerSigningToken = certToken;
tokenValidationParameters.ValidIssuer = "accounts.google.com";
try
{
// Validate
SecurityToken jwt;
var claimsPrincipal = jwtHandler.ValidateToken(idToken, tokenValidationParameters, out jwt);
if (claimsPrincipal != null)
{
// Valid
idTokenStatus = "Valid";
}
}
catch (Exception e)
{
if (idTokenStatus != "Valid")
{
// Invalid?
}
}
}
return token.Claims;
}
private byte[][] getGoogleCertBytes()
{
// The request will be made to the authentication server.
WebRequest request = WebRequest.Create(
"https://www.googleapis.com/oauth2/v1/certs"
);
StreamReader reader = new StreamReader(request.GetResponse().GetResponseStream());
string responseFromServer = reader.ReadToEnd();
String[] split = responseFromServer.Split(':');
// There are two certificates returned from Google
byte[][] certBytes = new byte[2][];
int index = 0;
UTF8Encoding utf8 = new UTF8Encoding();
for (int i = 0; i < split.Length; i++)
{
if (split[i].IndexOf(beginCert) > 0)
{
int startSub = split[i].IndexOf(beginCert);
int endSub = split[i].IndexOf(endCert) + endCert.Length;
certBytes[index] = utf8.GetBytes(split[i].Substring(startSub, endSub).Replace("\\n", "\n"));
index++;
}
}
return certBytes;
}
I know that Signature validation isn't completely necessary for JWTs but I haven't the slightest idea how to turn it off. Any ideas?
I thought I'd post my slightly improved version which uses JSON.Net to parse Googles' X509 Certificates and matches the key to use based on the "kid" (key-id). This is a bit more efficient than trying each certificate, since asymmetric crypto is usually quite expensive.
Also removed out-dated WebClient and manual string parsing code:
static Lazy<Dictionary<string, X509Certificate2>> Certificates = new Lazy<Dictionary<string, X509Certificate2>>( FetchGoogleCertificates );
static Dictionary<string, X509Certificate2> FetchGoogleCertificates()
{
using (var http = new HttpClient())
{
var json = http.GetStringAsync( "https://www.googleapis.com/oauth2/v1/certs" ).Result;
var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>( json );
return dictionary.ToDictionary( x => x.Key, x => new X509Certificate2( Encoding.UTF8.GetBytes( x.Value ) ) );
}
}
JwtSecurityToken ValidateIdentityToken( string idToken )
{
var token = new JwtSecurityToken( idToken );
var jwtHandler = new JwtSecurityTokenHandler();
var certificates = Certificates.Value;
try
{
// Set up token validation
var tokenValidationParameters = new TokenValidationParameters();
tokenValidationParameters.ValidAudience = _clientId;
tokenValidationParameters.ValidIssuer = "accounts.google.com";
tokenValidationParameters.IssuerSigningTokens = certificates.Values.Select( x => new X509SecurityToken( x ) );
tokenValidationParameters.IssuerSigningKeys = certificates.Values.Select( x => new X509SecurityKey( x ) );
tokenValidationParameters.IssuerSigningKeyResolver = ( s, securityToken, identifier, parameters ) =>
{
return identifier.Select( x =>
{
if (!certificates.ContainsKey( x.Id ))
return null;
return new X509SecurityKey( certificates[ x.Id ] );
} ).First( x => x != null );
};
SecurityToken jwt;
var claimsPrincipal = jwtHandler.ValidateToken( idToken, tokenValidationParameters, out jwt );
return (JwtSecurityToken)jwt;
}
catch (Exception ex)
{
_trace.Error( typeof( GoogleOAuth2OpenIdHybridClient ).Name, ex );
return null;
}
}
The problem is the kid in the JWT whose value is the key identifier of the key was used to sign the JWT. Since you construct an array of certificates manually from the JWKs URI, you lose the key identifier information. The validation procedure however requires it.
You'll need to set tokenValidationParameters.IssuerSigningKeyResolver to a function that will return the same key that you set above in tokenValidationParameters.IssuerSigningToken. The purpose of this delegate is to instruct the runtime to ignore any 'matching' semantics and just try the key.
See this article for more information: JwtSecurityTokenHandler 4.0.0 Breaking Changes?
Edit: the code:
tokenValidationParameters.IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => { return new X509SecurityKey(certificate); };
The folks at Microsoft posted code sample for Azure V2 B2C Preview endpoint that support OpenId Connect. See here, with the helper class OpenIdConnectionCachingSecurityTokenProvider the code is simplified as follows:
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AccessTokenFormat = new JwtFormat(new TokenValidationParameters
{
ValidAudiences = new[] { googleClientId },
}, new OpenIdConnectCachingSecurityTokenProvider("https://accounts.google.com/.well-known/openid-configuration"))});
This class is necessary because the OAuthBearer Middleware does not leverage. The OpenID Connect metadata endpoint exposed by the STS by default.
public class OpenIdConnectCachingSecurityTokenProvider : IIssuerSecurityTokenProvider
{
public ConfigurationManager<OpenIdConnectConfiguration> _configManager;
private string _issuer;
private IEnumerable<SecurityToken> _tokens;
private readonly string _metadataEndpoint;
private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim();
public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint)
{
_metadataEndpoint = metadataEndpoint;
_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint);
RetrieveMetadata();
}
/// <summary>
/// Gets the issuer the credentials are for.
/// </summary>
/// <value>
/// The issuer the credentials are for.
/// </value>
public string Issuer
{
get
{
RetrieveMetadata();
_synclock.EnterReadLock();
try
{
return _issuer;
}
finally
{
_synclock.ExitReadLock();
}
}
}
/// <summary>
/// Gets all known security tokens.
/// </summary>
/// <value>
/// All known security tokens.
/// </value>
public IEnumerable<SecurityToken> SecurityTokens
{
get
{
RetrieveMetadata();
_synclock.EnterReadLock();
try
{
return _tokens;
}
finally
{
_synclock.ExitReadLock();
}
}
}
private void RetrieveMetadata()
{
_synclock.EnterWriteLock();
try
{
OpenIdConnectConfiguration config = _configManager.GetConfigurationAsync().Result;
_issuer = config.Issuer;
_tokens = config.SigningTokens;
}
finally
{
_synclock.ExitWriteLock();
}
}
}
Based on the answer from Johannes Rudolph I post my solution.
There is a compiler error in IssuerSigningKeyResolver Delegate which I had to solve.
This is my working code now:
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace QuapiNet.Service
{
public class JwtTokenValidation
{
public async Task<Dictionary<string, X509Certificate2>> FetchGoogleCertificates()
{
using (var http = new HttpClient())
{
var response = await http.GetAsync("https://www.googleapis.com/oauth2/v1/certs");
var dictionary = await response.Content.ReadAsAsync<Dictionary<string, string>>();
return dictionary.ToDictionary(x => x.Key, x => new X509Certificate2(Encoding.UTF8.GetBytes(x.Value)));
}
}
private string CLIENT_ID = "xxxxx.apps.googleusercontent.com";
public async Task<ClaimsPrincipal> ValidateToken(string idToken)
{
var certificates = await this.FetchGoogleCertificates();
TokenValidationParameters tvp = new TokenValidationParameters()
{
ValidateActor = false, // check the profile ID
ValidateAudience = true, // check the client ID
ValidAudience = CLIENT_ID,
ValidateIssuer = true, // check token came from Google
ValidIssuers = new List<string> { "accounts.google.com", "https://accounts.google.com" },
ValidateIssuerSigningKey = true,
RequireSignedTokens = true,
IssuerSigningKeys = certificates.Values.Select(x => new X509SecurityKey(x)),
IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) =>
{
return certificates
.Where(x => x.Key.ToUpper() == kid.ToUpper())
.Select(x => new X509SecurityKey(x.Value));
},
ValidateLifetime = true,
RequireExpirationTime = true,
ClockSkew = TimeSpan.FromHours(13)
};
JwtSecurityTokenHandler jsth = new JwtSecurityTokenHandler();
SecurityToken validatedToken;
ClaimsPrincipal cp = jsth.ValidateToken(idToken, tvp, out validatedToken);
return cp;
}
}
}
Related
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'm trying to run my app but it get stuck with the following error:
System.NotSupportedException HResult=0x80131515 Message=IDX10634:
Unable to create the SignatureProvider. Algorithm: '[PII is hidden by
default. Set the 'ShowPII' flag in IdentityModelEventSource.cs to true
to reveal it.]', SecurityKey: '[PII is hidden by default. Set the
'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]'
is not supported.
Where
Algorithm is RS256
It stucks on executing this instruction: var sectoken = tokenHandler.CreateToken(tokenDescriptor);
What does it mean? What went wrong in my code? How can I solve this?
Here's my code:
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
//...
public class TokenManager
{
private string unencoded_key = "CaptainDeadpool";
private string encoded_key = "CaptainDeadpool";
//...
public TokenManager()
{
var plainTextBytes = Encoding.UTF8.GetBytes(unencoded_key);
encoded_key = Convert.ToBase64String(plainTextBytes);
}
public string CreateFromUsername(string usr, int? timer)
{
if (timer == null) { timer = 30; }
double timeadd = Convert.ToDouble(timer);
var secret = Convert.FromBase64String(encoded_key);
var tokenHandler = new JwtSecurityTokenHandler();
var actual = DateTime.UtcNow;
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, usr) }),
Expires = actual.AddMinutes(timeadd),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(secret), SecurityAlgorithms.RsaSha256Signature)
};
var sectoken = tokenHandler.CreateToken(tokenDescriptor);
var stringtoken = tokenHandler.WriteToken(sectoken);
return stringtoken;
}
//...
Here's my tokenDescriptor's content while issuing the error:
No idea what that error message means, but it doesn't matter I think, because your code is logically wrong. RSA is assymetric algorithm, but you are trying to use SymmetricSecurityKey with it.
So either use another (symmetric) signature algorithm (and ensure that your key size is valid for this algorithm), for example:
// adjust key size
private string unencoded_key = "CaptainDeadpool!";
private string encoded_key = "CaptainDeadpool!";
// ...
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(secret),
SecurityAlgorithms.HmacSha256Signature)
Or provide valid key, for example:
private readonly RSA _rsa;
public TokenManager() {
// import instead of creating new, if necessary
_rsa = new RSACryptoServiceProvider(2048);
}
// ...
SigningCredentials = new SigningCredentials(
new RsaSecurityKey(_rsa),
SecurityAlgorithms.RsaSha256Signature)
I had the same problem when using hmacSha256. if your security key is too short, you may receive that error. I increased the size of the secret secureKey and that resolved my problem.
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("You_Need_To_Provide_A_Longer_Secret_Key_Here"));
I am following this tutorial:
https://jonhilton.net/2017/10/11/secure-your-asp.net-core-2.0-api-part-1---issuing-a-jwt/
Here is the main code:
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["SecurityKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "yourdomain.com",
audience: "yourdomain.com",
claims: user.Claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
The part I don't understand is where it looks for a key in the configuration file, but it gives no indicator what this key is/should be?
One option is to keep a repository of symmetric signing keys that are associated with the "kid" claim in the JWT header. For example, keep a file of keys in an encrypted AWS S3 bucket. The .NET Core 2 web service pulls the file of keys from the S3 bucket every X minutes.
When a request arrives, the "kid" claim value is used to look up the associated symmetric key from a collection that was built from the pulled file.
EXAMPLE:
Configure an IssuerSigningKeyResolver function.
public static IServiceCollection AddJwtValidation(this IServiceCollection services)
{
IServiceProvider sp = services.BuildServiceProvider();
ConfigRoot = sp.GetRequiredService<IConfigurationRoot>();
tokenAudience = ConfigRoot["JwtToken:Audience"];
tokenIssuer = ConfigRoot["JwtToken:Issuer"];
SecurityKeyManager.Start();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Audience = tokenAudience;
options.ClaimsIssuer = tokenIssuer;
options.TokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
RequireSignedTokens = true,
IssuerSigningKeyResolver = MyIssuerSigningKeyResolver,
....
The IssuerSigningKeyResolver is defined as:
public static List<SecurityKey> MyIssuerSigningKeyResolver(string token, SecurityToken jwtToken, string kid, TokenValidationParameters validationParameters)
{
List<SecurityKey> keys = new List<SecurityKey>();
if (validationParameters == null)
{
throw new ArgumentNullException("validationParameters");
}
if (jwtToken == null)
{
throw new ArgumentNullException("securityToken");
}
if (!string.IsNullOrEmpty(kid))
{
SymmetricSecurityKey key = SecurityKeyManager.GetSecurityKey(kid);
keys.Add(key);
}
return keys;
}
The SecurityKeyManager periodically pulls the key file from AWS S3 and stores the keys in a collection.
// Example approved-clients.txt file in the approved-clients S3 bucket (us-east-1).
// //kid,key,active
// customer1,AAAAAAAAAAAAAAAA,true
// customer2,BBBBBBBBBBBBBBBB,true
// customer3,CCCCCCCCCCCCCCCC,true
// customer4,DDDDDDDDDDDDDDDD,true
namespace My.CoreServices.Security.Jwt
{
public class SecurityKeyManager
{
private const int RELOAD_TIMER_DELAY_SECONDS = 3 * 1000;
private const int RELOAD_TIMER_PERIOD_MINUTES = 60 * 60 * 1000;
[DebuggerDisplay("{Kid} {SymmetricKey} {Active}")]
internal class ApprovedClient
{
public string Kid { get; set; }
public bool Active { get; set; }
public string SymmetricKey { get; set; }
};
private static List<SymmetricSecurityKey> securityKeys = new List<SymmetricSecurityKey>();
private static Timer reloadTimer = null;
private static object keySync = new object();
public static void Start()
{
// Start a new timer to reload all the security keys every RELOAD_TIMER_PERIOD_MINUTES.
if (reloadTimer == null)
{
reloadTimer = new Timer(async (t) =>
{
try
{
List<ApprovedClient> approvedClients = new List<ApprovedClient>();
Log.Debug("Pulling latest approved client symmetric keys for JWT signature validation");
string awsAccessKeyId = JwtConfigure.ConfigRoot["AWS:KeyManagement:AccessKeyId"];
string awsSecretAccessKey = fromBase64(JwtConfigure.ConfigRoot["AWS:KeyManagement:SecretAccessKey"]);
string awsRegion = JwtConfigure.ConfigRoot["AWS:KeyManagement:Region"];
using (var client = new AmazonS3Client(awsAccessKeyId, awsSecretAccessKey, RegionEndpoint.GetBySystemName(awsRegion)))
{
var request = new GetObjectRequest();
request.BucketName = JwtConfigure.ConfigRoot["AWS:KeyManagement:Bucket"];
request.Key = JwtConfigure.ConfigRoot["AWS:KeyManagement:Key"];
var response = await client.GetObjectAsync(request);
using (StreamReader sr = new StreamReader(response.ResponseStream))
{
while (sr.Peek() > 0)
{
string line = await sr.ReadLineAsync();
// Ignore comment lines in the approved-client file
if (!line.StartsWith("//") && !string.IsNullOrEmpty(line))
{
// Each line of the file should only have 3 items:
// kid, key, active
string[] items = line.Split(',');
approvedClients.Add(new ApprovedClient()
{
Kid = items[0],
SymmetricKey = items[1],
Active = Boolean.Parse(items[2])
});
}
}
}
}
lock (keySync)
{
if (approvedClients.Count > 0)
{
// Clear the security key list and repopulate
securityKeys.Clear();
foreach (var approvedClient in approvedClients)
{
if (approvedClient.Active)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(approvedClient.SymmetricKey));
key.KeyId = approvedClient.Kid;
securityKeys.Add(key);
}
}
}
}
Log.Information($"Reloaded security keys");
}
catch (Exception ex)
{
Log.Warning($"Error getting current security keys - {ex.Message}");
}
}, null, RELOAD_TIMER_DELAY_SECONDS, RELOAD_TIMER_PERIOD_MINUTES);
}
}
public static void Stop()
{
if (reloadTimer != null)
{
reloadTimer.Dispose();
reloadTimer = null;
}
}
public static SymmetricSecurityKey GetSecurityKey(string kid)
{
SymmetricSecurityKey securityKey = null;
lock (keySync)
{
byte[] keyData = securityKeys.Where(k => k.KeyId == kid).Select(x => x.Key).FirstOrDefault();
if (keyData != null)
{
securityKey = new SymmetricSecurityKey(keyData);
securityKey.KeyId = kid;
}
}
return securityKey;
}
private static string fromBase64(string encodedValue)
{
byte[] decodedBytes = Convert.FromBase64String(encodedValue);
return Encoding.UTF8.GetString(decodedBytes);
}
}
}
Ensure that when a JWT is created for a specific user/customer/etc, the "kid" claim is set in the JWT header.
{
"alg": "HS256",
"kid": "customer2",
"typ": "JWT"
}
The value of "kid" will be passed as the third parameter to the IssuerSigningKeyResolver method. That kid will then be used to lookup the associated symmetric key that is used to validate the JWT signature.
This key can be any string: it's the secret key used to both encrypt and decrypt your secure payload.
From Wikipedia:
Symmetric-key algorithms are algorithms for cryptography that use the same cryptographic keys for both encryption of plaintext and decryption of ciphertext. The keys, in practice, represent a shared secret between two or more parties that can be used to maintain a private information link.
As for how to generate an efficient key, you can refer to this question on crypto.stackexchange.
In ASP.NET MVC app, I am trying to implement authentication against external OIDC service. For my testing I am using IdentityServer3 (https://identityserver.github.io/Documentation/) and public OIDC demo server: https://mitreid.org/
I cloned this sample from GitHub: https://github.com/IdentityServer/IdentityServer3.Samples/tree/master/source/MVC%20Authentication
Then added the following code to register the public OIDC server as external login provider:
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "<AuthTypeName>",
Authority = "https://mitreid.org/",
Caption = "MIT Test Server",
ClientId = "<Client Id>",
ClientSecret = "<Client Secret>",
RedirectUri = "https://localhost:44319/", //NOT SURE WHAT TO PUT HERE
ResponseType = "code",
Scope = "openid email profile",
SignInAsAuthenticationType = signInAsType
});
}
The code works, i get the option to login via external OIDC server. The browser redirects to the external server login page and when login and password is entered, the consent page is shown. However, after the browser navigates back to https://localhost:44319/ the user is not authenticated - User.Identity.IsAuthenticated is false.
Question: What should be correct value of RedirectUri property? Does OpenIdConnect middleware have capability to parse the authantication info passed in from external server or it must be coded manually? Is there any sample code how to do this?
I was studying the code and debugging quite a few hours (I am new to this) and I learned that:
This problem is related to OpenIdConnect OWIN Middleware implemented by Microsoft (https://github.com/aspnet/AspNetKatana/tree/dev/src/Microsoft.Owin.Security.OpenIdConnect).
The middleware from Microsoft expect that the OIDC server sends the message using HTTP POST, but the MIT server does HTTP GET
The middleware from Microsoft expect that there is id token along with code in the message obtained from OIDC server, but the MIT server sends only the code.
Looks like the RedirectUri can be any path under /identity because the middleware method AuthenticateCoreAsync() is hit on every request and it does compare request path to configured Options.CallbackPath (which is set from RedirectURI)
So I just had to implement the standard authorization code flow - exchange the code for id token, get claims, create authentication ticket and redirect to IdentityServer /identity/callback endpoint. When I've done this, everything started working. IdentityServer is awesome!
I inherited new set of classes from OpenIdConnect middleware and did override some methods. The key method is async Task<AuthenticationTicket> AuthenticateCoreAsync() in OpenIdConnectAuthenticationHandler. I pasted the code below in case it would help to someone.
public class CustomOidcHandler : OpenIdConnectAuthenticationHandler
{
private const string HandledResponse = "HandledResponse";
private readonly ILogger _logger;
private OpenIdConnectConfiguration _configuration;
public CustomOidcHandler(ILogger logger) : base(logger)
{
_logger = logger;
}
/// <summary>
/// Invoked to process incoming authentication messages.
/// </summary>
/// <returns>An <see cref="AuthenticationTicket"/> if successful.</returns>
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
// Allow login to be constrained to a specific path. Need to make this runtime configurable.
if (Options.CallbackPath.HasValue && Options.CallbackPath != (Request.PathBase + Request.Path))
return null;
OpenIdConnectMessage openIdConnectMessage = null;
if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
openIdConnectMessage = new OpenIdConnectMessage(Request.Query);
if (openIdConnectMessage == null)
return null;
ExceptionDispatchInfo authFailedEx = null;
try
{
return await CreateAuthenticationTicket(openIdConnectMessage).ConfigureAwait(false);
}
catch (Exception exception)
{
// We can't await inside a catch block, capture and handle outside.
authFailedEx = ExceptionDispatchInfo.Capture(exception);
}
if (authFailedEx != null)
{
_logger.WriteError("Exception occurred while processing message: ", authFailedEx.SourceException);
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification.
if (Options.RefreshOnIssuerKeyNotFound && authFailedEx.SourceException.GetType() == typeof(SecurityTokenSignatureKeyNotFoundException))
Options.ConfigurationManager.RequestRefresh();
var authenticationFailedNotification = new AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage,
Exception = authFailedEx.SourceException
};
await Options.Notifications.AuthenticationFailed(authenticationFailedNotification).ConfigureAwait(false);
if (authenticationFailedNotification.HandledResponse)
return GetHandledResponseTicket();
if (authenticationFailedNotification.Skipped)
return null;
authFailedEx.Throw();
}
return null;
}
private async Task<AuthenticationTicket> CreateAuthenticationTicket(OpenIdConnectMessage openIdConnectMessage)
{
var messageReceivedNotification =
new MessageReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage
};
await Options.Notifications.MessageReceived(messageReceivedNotification).ConfigureAwait(false);
if (messageReceivedNotification.HandledResponse)
{
return GetHandledResponseTicket();
}
if (messageReceivedNotification.Skipped)
{
return null;
}
// runtime always adds state, if we don't find it OR we failed to 'unprotect' it this is not a message we
// should process.
AuthenticationProperties properties = GetPropertiesFromState(openIdConnectMessage.State);
if (properties == null)
{
_logger.WriteWarning("The state field is missing or invalid.");
return null;
}
// devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users.
if (!string.IsNullOrWhiteSpace(openIdConnectMessage.Error))
{
throw new OpenIdConnectProtocolException(
string.Format(CultureInfo.InvariantCulture,
openIdConnectMessage.Error,
"Exception_OpenIdConnectMessageError", openIdConnectMessage.ErrorDescription ?? string.Empty,
openIdConnectMessage.ErrorUri ?? string.Empty));
}
// tokens.Item1 contains id token
// tokens.Item2 contains access token
Tuple<string, string> tokens = await GetTokens(openIdConnectMessage.Code, Options)
.ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken))
openIdConnectMessage.IdToken = tokens.Item1;
var securityTokenReceivedNotification =
new SecurityTokenReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context,
Options)
{
ProtocolMessage = openIdConnectMessage,
};
await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification).ConfigureAwait(false);
if (securityTokenReceivedNotification.HandledResponse)
return GetHandledResponseTicket();
if (securityTokenReceivedNotification.Skipped)
return null;
if (_configuration == null)
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.Request.CallCancelled)
.ConfigureAwait(false);
// Copy and augment to avoid cross request race conditions for updated configurations.
TokenValidationParameters tvp = Options.TokenValidationParameters.Clone();
IEnumerable<string> issuers = new[] {_configuration.Issuer};
tvp.ValidIssuers = tvp.ValidIssuers?.Concat(issuers) ?? issuers;
tvp.IssuerSigningTokens = tvp.IssuerSigningTokens?.Concat(_configuration.SigningTokens) ?? _configuration.SigningTokens;
SecurityToken validatedToken;
ClaimsPrincipal principal =
Options.SecurityTokenHandlers.ValidateToken(openIdConnectMessage.IdToken, tvp, out validatedToken);
ClaimsIdentity claimsIdentity = principal.Identity as ClaimsIdentity;
var claims = await GetClaims(tokens.Item2).ConfigureAwait(false);
AddClaim(claims, claimsIdentity, "sub", ClaimTypes.NameIdentifier, Options.AuthenticationType);
AddClaim(claims, claimsIdentity, "given_name", ClaimTypes.GivenName);
AddClaim(claims, claimsIdentity, "family_name", ClaimTypes.Surname);
AddClaim(claims, claimsIdentity, "preferred_username", ClaimTypes.Name);
AddClaim(claims, claimsIdentity, "email", ClaimTypes.Email);
// claims principal could have changed claim values, use bits received on wire for validation.
JwtSecurityToken jwt = validatedToken as JwtSecurityToken;
AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, properties);
if (Options.ProtocolValidator.RequireNonce)
{
if (String.IsNullOrWhiteSpace(openIdConnectMessage.Nonce))
openIdConnectMessage.Nonce = jwt.Payload.Nonce;
// deletes the nonce cookie
RetrieveNonce(openIdConnectMessage);
}
// remember 'session_state' and 'check_session_iframe'
if (!string.IsNullOrWhiteSpace(openIdConnectMessage.SessionState))
ticket.Properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = openIdConnectMessage.SessionState;
if (!string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe))
ticket.Properties.Dictionary[OpenIdConnectSessionProperties.CheckSessionIFrame] =
_configuration.CheckSessionIframe;
if (Options.UseTokenLifetime)
{
// Override any session persistence to match the token lifetime.
DateTime issued = jwt.ValidFrom;
if (issued != DateTime.MinValue)
{
ticket.Properties.IssuedUtc = issued.ToUniversalTime();
}
DateTime expires = jwt.ValidTo;
if (expires != DateTime.MinValue)
{
ticket.Properties.ExpiresUtc = expires.ToUniversalTime();
}
ticket.Properties.AllowRefresh = false;
}
var securityTokenValidatedNotification =
new SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context,
Options)
{
AuthenticationTicket = ticket,
ProtocolMessage = openIdConnectMessage,
};
await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification).ConfigureAwait(false);
if (securityTokenValidatedNotification.HandledResponse)
{
return GetHandledResponseTicket();
}
if (securityTokenValidatedNotification.Skipped)
{
return null;
}
// Flow possible changes
ticket = securityTokenValidatedNotification.AuthenticationTicket;
// there is no hash of the code (c_hash) in the jwt obtained from the server
// I don't know how to perform the validation using ProtocolValidator without the hash
// that is why the code below is commented
//var protocolValidationContext = new OpenIdConnectProtocolValidationContext
//{
// AuthorizationCode = openIdConnectMessage.Code,
// Nonce = nonce
//};
//Options.ProtocolValidator.Validate(jwt, protocolValidationContext);
if (openIdConnectMessage.Code != null)
{
var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options)
{
AuthenticationTicket = ticket,
Code = openIdConnectMessage.Code,
JwtSecurityToken = jwt,
ProtocolMessage = openIdConnectMessage,
RedirectUri =
ticket.Properties.Dictionary.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey)
? ticket.Properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey]
: string.Empty,
};
await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification)
.ConfigureAwait(false);
if (authorizationCodeReceivedNotification.HandledResponse)
{
return GetHandledResponseTicket();
}
if (authorizationCodeReceivedNotification.Skipped)
{
return null;
}
// Flow possible changes
ticket = authorizationCodeReceivedNotification.AuthenticationTicket;
}
return ticket;
}
private static void AddClaim(IEnumerable<Tuple<string, string>> claims, ClaimsIdentity claimsIdentity, string key, string claimType, string issuer = null)
{
string subject = claims
.Where(it => it.Item1 == key)
.Select(x => x.Item2).SingleOrDefault();
if (!string.IsNullOrWhiteSpace(subject))
claimsIdentity.AddClaim(
new System.Security.Claims.Claim(claimType, subject, ClaimValueTypes.String, issuer));
}
private async Task<Tuple<string, string>> GetTokens(string authorizationCode, OpenIdConnectAuthenticationOptions options)
{
// exchange authorization code at authorization server for an access and refresh token
Dictionary<string, string> post = null;
post = new Dictionary<string, string>
{
{"client_id", options.ClientId},
{"client_secret", options.ClientSecret},
{"grant_type", "authorization_code"},
{"code", authorizationCode},
{"redirect_uri", options.RedirectUri}
};
string content;
using (var client = new HttpClient())
{
var postContent = new FormUrlEncodedContent(post);
var response = await client.PostAsync(options.Authority.TrimEnd('/') + "/token", postContent)
.ConfigureAwait(false);
content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
// received tokens from authorization server
var json = JObject.Parse(content);
var accessToken = json["access_token"].ToString();
string idToken = null;
if (json["id_token"] != null)
idToken = json["id_token"].ToString();
return new Tuple<string, string>(idToken, accessToken);
}
private async Task<IEnumerable<Tuple<string, string>>> GetClaims(string accessToken)
{
string userInfoEndpoint = Options.Authority.TrimEnd('/') + "/userinfo";
var userInfoClient = new UserInfoClient(new Uri(userInfoEndpoint), accessToken);
var userInfoResponse = await userInfoClient.GetAsync().ConfigureAwait(false);
var claims = userInfoResponse.Claims;
return claims;
}
private static AuthenticationTicket GetHandledResponseTicket()
{
return new AuthenticationTicket(null, new AuthenticationProperties(new Dictionary<string, string>() { { HandledResponse, "true" } }));
}
private AuthenticationProperties GetPropertiesFromState(string state)
{
// assume a well formed query string: <a=b&>OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey=kasjd;fljasldkjflksdj<&c=d>
int startIndex = 0;
if (string.IsNullOrWhiteSpace(state) || (startIndex = state.IndexOf("OpenIdConnect.AuthenticationProperties", StringComparison.Ordinal)) == -1)
{
return null;
}
int authenticationIndex = startIndex + "OpenIdConnect.AuthenticationProperties".Length;
if (authenticationIndex == -1 || authenticationIndex == state.Length || state[authenticationIndex] != '=')
{
return null;
}
// scan rest of string looking for '&'
authenticationIndex++;
int endIndex = state.Substring(authenticationIndex, state.Length - authenticationIndex).IndexOf("&", StringComparison.Ordinal);
// -1 => no other parameters are after the AuthenticationPropertiesKey
if (endIndex == -1)
{
return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex).Replace('+', ' ')));
}
else
{
return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex, endIndex).Replace('+', ' ')));
}
}
}
public static class CustomOidcAuthenticationExtensions
{
/// <summary>
/// Adds the <see cref="OpenIdConnectAuthenticationMiddleware"/> into the OWIN runtime.
/// </summary>
/// <param name="app">The <see cref="IAppBuilder"/> passed to the configuration method</param>
/// <param name="openIdConnectOptions">A <see cref="OpenIdConnectAuthenticationOptions"/> contains settings for obtaining identities using the OpenIdConnect protocol.</param>
/// <returns>The updated <see cref="IAppBuilder"/></returns>
public static IAppBuilder UseCustomOidcAuthentication(this IAppBuilder app, OpenIdConnectAuthenticationOptions openIdConnectOptions)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
if (openIdConnectOptions == null)
throw new ArgumentNullException(nameof(openIdConnectOptions));
return app.Use(typeof(CustomOidcMiddleware), app, openIdConnectOptions);
}
}
and in Startup.cs
public class Startup
{
....
public void Configuration(IAppBuilder app)
{
....
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseCustomOidcAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "<name>",
Authority = "<OIDC server url>",
Caption = "<caption>",
ClientId = "<client id>",
ClientSecret = "<client secret>",
// might be https://localhost:44319/identity/<anything>
RedirectUri = "https://localhost:44319/identity/signin-customoidc",
ResponseType = "code",
Scope = "openid email profile address phone",
SignInAsAuthenticationType = signInAsType
}
);
}
....
}
....
}
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"] }
});