Skip to content

Instantly share code, notes, and snippets.

@svantreeck
Created April 16, 2015 13:41
Show Gist options
  • Save svantreeck/436f6ddddda38c735c62 to your computer and use it in GitHub Desktop.
Save svantreeck/436f6ddddda38c735c62 to your computer and use it in GitHub Desktop.
ServiceStack JWT Token validation for Auth0
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)
{
}
}
}
}
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
});
}
}
}
@svantreeck
Copy link
Author

Enable using:

        Plugins.Add(new AuthFeature(() => new AuthUserSession(),
            new IAuthProvider[] { 
                new JsonWebTokenAuthProvider("Auth0 Client Secret", "Auth0 Client ID"), 
            }));

@tearf001
Copy link

tearf001 commented Apr 5, 2017

truly saves my days,thanks!

@bjarki
Copy link

bjarki commented Nov 17, 2020

I can't find JWT.JsonWebToken.Decode Where is that coming from?

@svantreeck
Copy link
Author

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.

@bjarki
Copy link

bjarki commented Nov 17, 2020

Thanks for the reply.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment