Created
April 16, 2015 13:41
-
-
Save svantreeck/436f6ddddda38c735c62 to your computer and use it in GitHub Desktop.
ServiceStack JWT Token validation for Auth0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Security.Claims; | |
using ServiceStack.Text; | |
namespace ServiceStackAPI | |
{ | |
public static class JsonWebToken | |
{ | |
private const string NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"; | |
private const string RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; | |
private const string ActorClaimType = "http://schemas.xmlsoap.org/ws/2009/09/identity/claims/actor"; | |
private const string DefaultIssuer = "LOCAL AUTHORITY"; | |
private const string StringClaimValueType = "http://www.w3.org/2001/XMLSchema#string"; | |
// sort claim types by relevance | |
private static IEnumerable<string> claimTypesForUserName = new[] { "name", "email", "user_id", "sub" }; | |
private static ISet<string> claimsToExclude = new HashSet<string>(new[] { "iss", "sub", "aud", "exp", "iat", "identities" }); | |
private static DateTime unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | |
public static ClaimsPrincipal ValidateToken(string token, string secretKey, string audience = null, bool checkExpiration = false, string issuer = null) | |
{ | |
var payloadJson = JWT.JsonWebToken.Decode(token, Convert.FromBase64String(secretKey), verify: true); | |
var payloadData = JsonObject.Parse(payloadJson); | |
// audience check | |
if (!string.IsNullOrEmpty(audience)) | |
{ | |
var aud = payloadData["aud"]; | |
if (!string.Equals(aud, audience, StringComparison.Ordinal)) | |
{ | |
throw new TokenValidationException(string.Format("Audience mismatch. Expected: '{0}' and got: '{1}'", audience, aud)); | |
} | |
} | |
// expiration check | |
if (checkExpiration) | |
{ | |
var exp = payloadData["exp"]; | |
if (exp != null) | |
{ | |
DateTime validTo = FromUnixTime(long.Parse(exp)); | |
if (DateTime.Compare(validTo, DateTime.UtcNow) <= 0) | |
{ | |
throw new TokenValidationException( | |
string.Format("Token is expired. Expiration: '{0}'. Current: '{1}'", validTo, DateTime.UtcNow)); | |
} | |
} | |
} | |
// issuer check | |
var iss = payloadData["iss"]; | |
if (iss != null) | |
{ | |
if (!string.IsNullOrEmpty(issuer)) | |
{ | |
if (string.Equals(iss, issuer, StringComparison.Ordinal)) | |
{ | |
throw new TokenValidationException(string.Format("Token issuer mismatch. Expected: '{0}' and got: '{1}'", issuer, iss)); | |
} | |
} | |
else | |
{ | |
// if issuer is not specified, set issuer with jwt[iss] | |
issuer = iss; | |
} | |
} | |
return new ClaimsPrincipal(ClaimsIdentityFromJwt(payloadData, issuer)); | |
} | |
private static ICollection<Claim> ClaimsFromJwt(JsonObject jwtData, string issuer) | |
{ | |
issuer = issuer ?? DefaultIssuer; | |
var list = jwtData.Where(p => !claimsToExclude.Contains(p.Key)) // don't include specific claims | |
.SelectMany(p => | |
{ | |
if (p.Value.StartsWith("[")) | |
{ | |
return jwtData.Get<string[]>(p.Key).Select(v => new Claim(p.Key, v, StringClaimValueType, issuer, issuer)); | |
} | |
else if (p.Value.StartsWith("{")) | |
{ | |
var claim = new Claim(p.Key, p.Value, StringClaimValueType, issuer, issuer); | |
var properties = jwtData.Object(p.Key).ToDictionary(); | |
foreach (var prop in properties) | |
{ | |
claim.Properties.Add(prop); | |
} | |
return new[] { claim }; | |
} | |
else | |
{ | |
return new[] { new Claim(p.Key, p.Value, StringClaimValueType, issuer, issuer) }; | |
} | |
}).ToList(); | |
// set claim for user name | |
// use original jwtData because claimsToExclude filter has sub and otherwise it wouldn't be used | |
var userNameClaimType = claimTypesForUserName.FirstOrDefault(ct => jwtData.ContainsKey(ct)); | |
if (userNameClaimType != null) | |
{ | |
list.Add(new Claim(NameClaimType, jwtData[userNameClaimType].ToString(), StringClaimValueType, issuer, issuer)); | |
} | |
// set claims for roles array | |
list.Where(c => c.Type == "roles").ToList().ForEach(r => | |
{ | |
list.Add(new Claim(RoleClaimType, r.Value, StringClaimValueType, issuer, issuer)); | |
}); | |
list.RemoveAll(c => c.Type == "roles"); | |
return list; | |
} | |
private static ClaimsIdentity ClaimsIdentityFromJwt(JsonObject jwtData, string issuer) | |
{ | |
var subject = new ClaimsIdentity("Federation", NameClaimType, RoleClaimType); | |
var claims = ClaimsFromJwt(jwtData, issuer); | |
foreach (Claim claim in claims) | |
{ | |
var type = claim.Type; | |
if (type == ActorClaimType) | |
{ | |
if (subject.Actor != null) | |
{ | |
throw new InvalidOperationException(string.Format( | |
"Jwt10401: Only a single 'Actor' is supported. Found second claim of type: '{0}', value: '{1}'", new object[] { "actor", claim.Value })); | |
} | |
subject.AddClaim(new Claim(type, claim.Value, claim.ValueType, issuer, issuer, subject)); | |
continue; | |
} | |
var newClaim = new Claim(type, claim.Value, claim.ValueType, issuer, issuer, subject); | |
foreach (var prop in claim.Properties) | |
{ | |
newClaim.Properties.Add(prop); | |
} | |
subject.AddClaim(newClaim); | |
} | |
return subject; | |
} | |
private static DateTime FromUnixTime(long unixTime) | |
{ | |
return unixEpoch.AddSeconds(unixTime); | |
} | |
public class TokenValidationException : Exception | |
{ | |
public TokenValidationException(string message) | |
: base(message) | |
{ | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using ServiceStack; | |
using ServiceStack.Auth; | |
using ServiceStack.Web; | |
using ServiceStackAPI; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net; | |
using System.Text; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using System.Web; | |
namespace ServiceStackAPI | |
{ | |
public class JsonWebTokenAuthProvider : AuthProvider, IAuthWithRequest | |
{ | |
public static string Name = "JWT"; | |
public static string Realm = "/auth/JWT"; | |
private const string MissingAuthHeader = "Missing Authorization Header"; | |
private const string InvalidAuthHeader = "Invalid Authorization Header"; | |
public string SymmetricKey { get; private set; } | |
public string Audience { get; private set; } | |
public string Issuer { get; set; } | |
/// <summary> | |
/// Creates a new JsonWebToken Auth Provider | |
/// </summary> | |
/// <param name="symmetricKey">a key used for validating the token</param> | |
/// <param name="audience"></param> | |
/// <param name="issuer"></param> | |
public JsonWebTokenAuthProvider(string symmetricKey, string audience = null, string issuer = null) | |
{ | |
Provider = Name; | |
AuthRealm = Realm; | |
SymmetricKey = symmetricKey; | |
Audience = audience; | |
Issuer = issuer; | |
} | |
public override object Authenticate(ServiceStack.IServiceBase authService, IAuthSession session, ServiceStack.Authenticate request) | |
{ | |
var header = request.oauth_token; | |
// if no auth header, 401 | |
if (header.IsNullOrEmpty()) | |
{ | |
throw HttpError.Unauthorized(MissingAuthHeader); | |
} | |
var headerData = header.Split(' '); | |
// if header is missing bearer portion, 401 | |
if (string.Compare(headerData[0], "BEARER", StringComparison.OrdinalIgnoreCase) != 0) | |
{ | |
throw HttpError.Unauthorized(InvalidAuthHeader); | |
} | |
try | |
{ | |
// swap - and _ with their Base64 string equivalents | |
var secret = this.SymmetricKey.Replace('-', '+').Replace('_', '/'); | |
// set current principal to the validated token principal | |
Thread.CurrentPrincipal = JsonWebToken.ValidateToken(headerData[1], secret, this.Audience, true, this.Issuer); | |
if (HttpContext.Current != null) | |
{ | |
// set the current request's user the the decoded principal | |
HttpContext.Current.User = Thread.CurrentPrincipal; | |
} | |
// set the session's username to the logged in user | |
session.UserName = Thread.CurrentPrincipal.Identity.Name; | |
return OnAuthenticated(authService, session, new AuthTokens(), new Dictionary<string, string>()); | |
} | |
catch (Exception ex) | |
{ | |
throw new HttpError(HttpStatusCode.Unauthorized, ex); | |
} | |
} | |
/// <summary> | |
/// Check that both the session and the identity are authenticated | |
/// </summary> | |
/// <param name="session"></param> | |
/// <param name="tokens"></param> | |
/// <param name="request"></param> | |
/// <returns></returns> | |
public override bool IsAuthorized(IAuthSession session, IAuthTokens tokens, ServiceStack.Authenticate request = null) | |
{ | |
return HttpContext.Current.User.Identity.IsAuthenticated && session.IsAuthenticated && string.Equals(session.UserName, HttpContext.Current.User.Identity.Name, StringComparison.OrdinalIgnoreCase); | |
} | |
public void PreAuthenticate(IRequest request, IResponse response) | |
{ | |
var header = request.Headers["Authorization"]; | |
var authService = request.TryResolve<AuthenticateService>(); | |
authService.Request = request; | |
// pass auth header in as oauth token to authentication | |
authService.Post(new Authenticate | |
{ | |
provider = Name, | |
oauth_token = header | |
}); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey @bjarki, I'm not sure if this code is valid anymore with Service Stack (I think this was for 3.x, but 6 years ago and IDK anymore), but I think there was a nuget package called JWT that provided it. Hopefully this works for you, but I think with the latest versions and .net core, you just end up targeting the normal asp.net core authentication pipeline.