Last active
July 17, 2017 07:31
-
-
Save tobiaszuercher/bd38241975dcbe0adda2ccc2ba547d04 to your computer and use it in GitHub Desktop.
Src copy paste of JwtAuthProviderReader with some minor changes to support IdentityServer v4. Changes marked with comment "changed"
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.Generic; | |
using System.IO; | |
using System.Net; | |
using System.Security.Cryptography; | |
using ServiceStack; | |
using ServiceStack.Auth; | |
using ServiceStack.Configuration; | |
using ServiceStack.Host; | |
using ServiceStack.Host.Handlers; | |
using ServiceStack.Text; | |
using ServiceStack.Web; | |
namespace My.Project.Auth | |
{ | |
/// <summary> | |
/// Enable access to protected Services using JWT Tokens | |
/// </summary> | |
public class JwtAuthProviderReaderCustom : AuthProvider, IAuthWithRequest, IAuthPlugin | |
{ | |
public static RsaKeyLengths UseRsaKeyLength = RsaKeyLengths.Bit2048; | |
public const string Name = AuthenticateService.JwtProvider; | |
public const string Realm = "/auth/" + AuthenticateService.JwtProvider; | |
public static HashSet<string> IgnoreForOperationTypes = new HashSet<string> | |
{ | |
typeof(StaticFileHandler).Name, | |
}; | |
/// <summary> | |
/// Different HMAC Algorithms supported | |
/// </summary> | |
public static readonly Dictionary<string, Func<byte[], byte[], byte[]>> HmacAlgorithms = new Dictionary<string, Func<byte[], byte[], byte[]>> | |
{ | |
{ "HS256", (key, value) => { using (var sha = new HMACSHA256(key)) { return sha.ComputeHash(value); } } }, | |
{ "HS384", (key, value) => { using (var sha = new HMACSHA384(key)) { return sha.ComputeHash(value); } } }, | |
{ "HS512", (key, value) => { using (var sha = new HMACSHA512(key)) { return sha.ComputeHash(value); } } } | |
}; | |
/// <summary> | |
/// Different RSA Signing Algorithms supported | |
/// </summary> | |
public static readonly Dictionary<string, Func<RSAParameters, byte[], byte[]>> RsaSignAlgorithms = new Dictionary<string, Func<RSAParameters, byte[], byte[]>> | |
{ | |
{ "RS256", (key, value) => RsaUtils.Authenticate(value, key, "SHA256", UseRsaKeyLength) }, | |
{ "RS384", (key, value) => RsaUtils.Authenticate(value, key, "SHA384", UseRsaKeyLength) }, | |
{ "RS512", (key, value) => RsaUtils.Authenticate(value, key, "SHA512", UseRsaKeyLength) }, | |
}; | |
public static readonly Dictionary<string, Func<RSAParameters, byte[], byte[], bool>> RsaVerifyAlgorithms = new Dictionary<string, Func<RSAParameters, byte[], byte[], bool>> | |
{ | |
{ "RS256", (key, value, sig) => RsaUtils.Verify(value, sig, key, "SHA256", UseRsaKeyLength) }, | |
{ "RS384", (key, value, sig) => RsaUtils.Verify(value, sig, key, "SHA384", UseRsaKeyLength) }, | |
{ "RS512", (key, value, sig) => RsaUtils.Verify(value, sig, key, "SHA512", UseRsaKeyLength) }, | |
}; | |
/// <summary> | |
/// Whether to only allow access via API Key from a secure connection. (default true) | |
/// </summary> | |
public bool RequireSecureConnection { get; set; } | |
/// <summary> | |
/// Run custom filter after JWT Header is created | |
/// </summary> | |
public Action<JsonObject, IAuthSession> CreateHeaderFilter { get; set; } | |
/// <summary> | |
/// Run custom filter after JWT Payload is created | |
/// </summary> | |
public Action<JsonObject, IAuthSession> CreatePayloadFilter { get; set; } | |
/// <summary> | |
/// Run custom filter after session is restored from a JWT Token | |
/// </summary> | |
public Action<IAuthSession, JsonObject, IRequest> PopulateSessionFilter { get; set; } | |
/// <summary> | |
/// Whether to encrypt JWE Payload (default false). | |
/// Uses RSA-OAEP for Key Encryption and AES/128/CBC HMAC SHA256 for Conent Encryption | |
/// </summary> | |
public bool EncryptPayload { get; set; } | |
/// <summary> | |
/// Which Hash Algorithm should be used to sign the JWT Token. (default HS256) | |
/// </summary> | |
public string HashAlgorithm { get; set; } | |
/// <summary> | |
/// Whether to only allow processing of JWT Tokens using the configured HashAlgorithm. (default true) | |
/// </summary> | |
public bool RequireHashAlgorithm { get; set; } | |
/// <summary> | |
/// The Issuer to embed in the token. (default ssjwt) | |
/// </summary> | |
public string Issuer { get; set; } | |
/// <summary> | |
/// The Audience to embed in the token. (default null) | |
/// </summary> | |
public string Audience { get; set; } | |
/// <summary> | |
/// What Id to use to identify the Key used to sign the token. (default First 3 chars of Base64 Key) | |
/// </summary> | |
public string KeyId { get; set; } | |
/// <summary> | |
/// The AuthKey used to sign the JWT Token | |
/// </summary> | |
public byte[] AuthKey { get; set; } | |
public string AuthKeyBase64 | |
{ | |
set { AuthKey = Convert.FromBase64String(value); } | |
} | |
/// <summary> | |
/// Allow verification using multiple Auth keys | |
/// </summary> | |
public List<byte[]> FallbackAuthKeys { get; set; } | |
/// <summary> | |
/// The RSA Private Key used to Sign the JWT Token when RSA is used | |
/// </summary> | |
public RSAParameters? privateKey; | |
public RSAParameters? PrivateKey | |
{ | |
get { return privateKey; } | |
set | |
{ | |
privateKey = value; | |
if (privateKey != null) | |
PublicKey = privateKey.Value.ToPublicRsaParameters(); | |
} | |
} | |
/// <summary> | |
/// Convenient overload to intialize the Private Key via exported XML | |
/// </summary> | |
public string PrivateKeyXml | |
{ | |
get { return PrivateKey?.FromPrivateRSAParameters(); } | |
set { PrivateKey = value?.ToPrivateRSAParameters(); } | |
} | |
/// <summary> | |
/// The RSA Public Key used to Verify the JWT Token when RSA is used | |
/// </summary> | |
public RSAParameters? PublicKey { get; set; } | |
/// <summary> | |
/// Convenient overload to intialize the Public Key via exported XML | |
/// </summary> | |
public string PublicKeyXml | |
{ | |
get { return PublicKey?.FromPublicRSAParameters(); } | |
set { PublicKey = value?.ToPublicRSAParameters(); } | |
} | |
/// <summary> | |
/// Allow verification using multiple public keys | |
/// </summary> | |
public List<RSAParameters> FallbackPublicKeys { get; set; } | |
/// <summary> | |
/// How long should JWT Tokens be valid for. (default 14 days) | |
/// </summary> | |
public TimeSpan ExpireTokensIn { get; set; } | |
/// <summary> | |
/// How long should JWT Refresh Tokens be valid for. (default 365 days) | |
/// </summary> | |
public TimeSpan ExpireRefreshTokensIn { get; set; } | |
/// <summary> | |
/// Allow custom logic to invalidate JWT Tokens | |
/// </summary> | |
public Func<JsonObject, IRequest, bool> ValidateToken { get; set; } | |
/// <summary> | |
/// Allow custom logic to invalidate Refresh Tokens | |
/// </summary> | |
public Func<JsonObject, IRequest, bool> ValidateRefreshToken { get; set; } | |
/// <summary> | |
/// Convenient overload to initialize ExpireTokensIn with an Integer | |
/// </summary> | |
public int ExpireTokensInDays | |
{ | |
set | |
{ | |
if (value > 0) | |
ExpireTokensIn = TimeSpan.FromDays(value); | |
} | |
} | |
/// <summary> | |
/// Whether to invalidate all JWT Tokens issued before a specified date. | |
/// </summary> | |
public DateTime? InvalidateTokensIssuedBefore { get; set; } | |
/// <summary> | |
/// Modify the registration of ConvertSessionToToken Service | |
/// </summary> | |
public Dictionary<Type, string[]> ServiceRoutes { get; set; } | |
public JwtAuthProviderReaderCustom() | |
: base(null, Realm, Name) | |
{ | |
Init(); | |
} | |
public JwtAuthProviderReaderCustom(IAppSettings appSettings) | |
: base(appSettings, Realm, Name) | |
{ | |
Init(appSettings); | |
} | |
public virtual void Init(IAppSettings appSettings = null) | |
{ | |
RequireSecureConnection = true; | |
EncryptPayload = false; | |
HashAlgorithm = "HS256"; | |
RequireHashAlgorithm = true; | |
Issuer = "ssjwt"; | |
ExpireTokensIn = TimeSpan.FromDays(14); | |
ExpireRefreshTokensIn = TimeSpan.FromDays(365); | |
FallbackAuthKeys = new List<byte[]>(); | |
FallbackPublicKeys = new List<RSAParameters>(); | |
if (appSettings != null) | |
{ | |
RequireSecureConnection = appSettings.Get("jwt.RequireSecureConnection", RequireSecureConnection); | |
RequireHashAlgorithm = appSettings.Get("jwt.RequireHashAlgorithm", RequireHashAlgorithm); | |
EncryptPayload = appSettings.Get("jwt.EncryptPayload", EncryptPayload); | |
Issuer = appSettings.GetString("jwt.Issuer"); | |
Audience = appSettings.GetString("jwt.Audience"); | |
KeyId = appSettings.GetString("jwt.KeyId"); | |
var hashAlg = appSettings.GetString("jwt.HashAlgorithm"); | |
if (!string.IsNullOrEmpty(hashAlg)) | |
HashAlgorithm = hashAlg; | |
var privateKeyXml = appSettings.GetString("jwt.PrivateKeyXml"); | |
if (privateKeyXml != null) | |
PrivateKeyXml = privateKeyXml; | |
var publicKeyXml = appSettings.GetString("jwt.PublicKeyXml"); | |
if (publicKeyXml != null) | |
PublicKeyXml = publicKeyXml; | |
var base64 = appSettings.GetString("jwt.AuthKeyBase64"); | |
if (base64 != null) | |
AuthKeyBase64 = base64; | |
var dateStr = appSettings.GetString("jwt.InvalidateTokensIssuedBefore"); | |
if (!string.IsNullOrEmpty(dateStr)) | |
InvalidateTokensIssuedBefore = dateStr.FromJsv<DateTime>(); | |
ExpireTokensIn = appSettings.Get("jwt.ExpireTokensIn", ExpireTokensIn); | |
ExpireRefreshTokensIn = appSettings.Get("jwt.ExpireRefreshTokensIn", ExpireRefreshTokensIn); | |
var intStr = appSettings.GetString("jwt.ExpireTokensInDays"); | |
if (intStr != null) | |
ExpireTokensInDays = int.Parse(intStr); | |
string base64Key; | |
var i = 1; | |
while ((base64Key = appSettings.GetString("jwt.PrivateKeyXml." + i++)) != null) | |
{ | |
var publicKey = base64Key.ToPublicRSAParameters(); | |
FallbackPublicKeys.Add(publicKey); | |
} | |
i = 1; | |
while ((base64Key = appSettings.GetString("jwt.PublicKeyXml." + i++)) != null) | |
{ | |
var publicKey = base64Key.ToPublicRSAParameters(); | |
FallbackPublicKeys.Add(publicKey); | |
} | |
i = 1; | |
while ((base64Key = appSettings.GetString("jwt.AuthKeyBase64." + i++)) != null) | |
{ | |
var authKey = Convert.FromBase64String(base64Key); | |
FallbackAuthKeys.Add(authKey); | |
} | |
} | |
} | |
public virtual string GetKeyId() | |
{ | |
if (KeyId != null) | |
return KeyId; | |
if (HmacAlgorithms.ContainsKey(HashAlgorithm) && AuthKey != null) | |
return Convert.ToBase64String(AuthKey).Substring(0, 3); | |
if (RsaSignAlgorithms.ContainsKey(HashAlgorithm) && PublicKey != null) | |
return Convert.ToBase64String(PublicKey.Value.Modulus).Substring(0, 3); | |
return null; | |
} | |
public override bool IsAuthorized(IAuthSession session, IAuthTokens tokens, Authenticate request = null) | |
{ | |
return session.FromToken && session.IsAuthenticated; | |
} | |
public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request) | |
{ | |
throw new NotImplementedException("JWT Authenticate() should not be called directly"); | |
} | |
public void PreAuthenticate(IRequest req, IResponse res) | |
{ | |
if (req.OperationName != null && IgnoreForOperationTypes.Contains(req.OperationName)) | |
return; | |
if (req.GetSession().IsAuthenticated) | |
return; | |
var bearerToken = req.GetBearerToken() | |
?? req.GetCookieValue(Keywords.TokenCookie); | |
if (bearerToken != null) | |
{ | |
var parts = bearerToken.Split('.'); | |
if (parts.Length == 3) | |
{ | |
if (RequireSecureConnection && !req.IsSecureConnection) | |
throw HttpError.Forbidden(ErrorMessages.JwtRequiresSecureConnection); | |
var jwtPayload = GetVerifiedJwtPayload(parts); | |
if (jwtPayload == null) //not verified | |
return; | |
if (ValidateToken != null) | |
{ | |
if (!ValidateToken(jwtPayload, req)) | |
throw HttpError.Forbidden(ErrorMessages.TokenInvalid); | |
} | |
var session = CreateSessionFromPayload(req, jwtPayload); | |
req.Items[Keywords.Session] = session; | |
} | |
else if (parts.Length == 5) //Encrypted JWE Token | |
{ | |
if (RequireSecureConnection && !req.IsSecureConnection) | |
throw HttpError.Forbidden(ErrorMessages.JwtRequiresSecureConnection); | |
if (PrivateKey == null || PublicKey == null) | |
throw new NotSupportedException("PrivateKey is required to DecryptPayload"); | |
var jweHeaderBase64Url = parts[0]; | |
var jweEncKeyBase64Url = parts[1]; | |
var ivBase64Url = parts[2]; | |
var cipherTextBase64Url = parts[3]; | |
var tagBase64Url = parts[4]; | |
var sentTag = tagBase64Url.FromBase64UrlSafe(); | |
var aadBytes = (jweHeaderBase64Url + "." + jweEncKeyBase64Url).ToUtf8Bytes(); | |
var iv = ivBase64Url.FromBase64UrlSafe(); | |
var cipherText = cipherTextBase64Url.FromBase64UrlSafe(); | |
var jweEncKey = jweEncKeyBase64Url.FromBase64UrlSafe(); | |
var cryptAuthKeys256 = RsaUtils.Decrypt(jweEncKey, PrivateKey.Value, UseRsaKeyLength); | |
var authKey = new byte[128 / 8]; | |
var cryptKey = new byte[128 / 8]; | |
Buffer.BlockCopy(cryptAuthKeys256, 0, authKey, 0, authKey.Length); | |
Buffer.BlockCopy(cryptAuthKeys256, authKey.Length, cryptKey, 0, cryptKey.Length); | |
using (var hmac = new HMACSHA256(authKey)) | |
using (var encryptedStream = new MemoryStream()) | |
{ | |
using (var writer = new BinaryWriter(encryptedStream)) | |
{ | |
writer.Write(aadBytes); | |
writer.Write(iv); | |
writer.Write(cipherText); | |
writer.Flush(); | |
var calcTag = hmac.ComputeHash(encryptedStream.ToArray()); | |
if (!calcTag.EquivalentTo(sentTag)) | |
return; | |
} | |
} | |
JsonObject jwtPayload; | |
var aes = Aes.Create(); | |
aes.KeySize = 128; | |
aes.BlockSize = 128; | |
aes.Mode = CipherMode.CBC; | |
aes.Padding = PaddingMode.PKCS7; | |
using (aes) | |
using (var decryptor = aes.CreateDecryptor(cryptKey, iv)) | |
using (var ms = MemoryStreamFactory.GetStream(cipherText)) | |
using (var cryptStream = new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) | |
{ | |
var jwtPayloadBytes = cryptStream.ReadFully(); | |
jwtPayload = JsonObject.Parse(jwtPayloadBytes.FromUtf8Bytes()); | |
} | |
if (ValidateToken != null) | |
{ | |
if (!ValidateToken(jwtPayload, req)) | |
throw HttpError.Forbidden(ErrorMessages.TokenInvalid); | |
} | |
var session = CreateSessionFromPayload(req, jwtPayload); | |
req.Items[Keywords.Session] = session; | |
} | |
} | |
} | |
public JsonObject GetVerifiedJwtPayload(string[] jwtParts) | |
{ | |
if (jwtParts.Length != 3) | |
throw new ArgumentException(ErrorMessages.TokenInvalid); | |
var header = jwtParts[0]; | |
var payload = jwtParts[1]; | |
var signatureBytes = jwtParts[2].FromBase64UrlSafe(); | |
var headerJson = header.FromBase64UrlSafe().FromUtf8Bytes(); | |
var payloadBytes = payload.FromBase64UrlSafe(); | |
var headerData = headerJson.FromJson<Dictionary<string, string>>(); | |
var bytesToSign = string.Concat(header, ".", payload).ToUtf8Bytes(); | |
var algorithm = headerData["alg"]; | |
//Potential Security Risk for relying on user-specified algorithm: https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ | |
if (RequireHashAlgorithm && algorithm != HashAlgorithm) | |
throw new NotSupportedException($"Invalid algoritm '{algorithm}', expected '{HashAlgorithm}'"); | |
if (!VerifyPayload(algorithm, bytesToSign, signatureBytes)) | |
return null; | |
var payloadJson = payloadBytes.FromUtf8Bytes(); | |
var jwtPayload = JsonObject.Parse(payloadJson); | |
return jwtPayload; | |
} | |
private IAuthSession CreateSessionFromPayload(IRequest req, JsonObject jwtPayload) | |
{ | |
AssertJwtPayloadIsValid(jwtPayload); | |
var sessionId = jwtPayload.GetValue("jid", SessionExtensions.CreateRandomSessionId); | |
var session = SessionFeature.CreateNewSession(req, sessionId); | |
session.AuthProvider = Name; | |
session.PopulateFromMap(jwtPayload); | |
session.PopulateFromIdentityServerMap(jwtPayload); | |
PopulateSessionFilter?.Invoke(session, jwtPayload, req); | |
HostContext.AppHost.OnSessionFilter(session, sessionId); | |
return session; | |
} | |
public void AssertJwtPayloadIsValid(JsonObject jwtPayload) | |
{ | |
var expiresAt = GetUnixTime(jwtPayload, "exp"); | |
var secondsSinceEpoch = DateTime.UtcNow.ToUnixTime(); | |
if (secondsSinceEpoch >= expiresAt) | |
throw new TokenException(ErrorMessages.TokenExpired); | |
if (InvalidateTokensIssuedBefore != null) | |
{ | |
var issuedAt = GetUnixTime(jwtPayload, "iat"); | |
if (issuedAt == null || issuedAt < InvalidateTokensIssuedBefore.Value.ToUnixTime()) | |
throw new TokenException(ErrorMessages.TokenInvalidated); | |
} | |
string audience; | |
if (jwtPayload.TryGetValue("aud", out audience)) | |
{ | |
if (!audience.Contains(Audience)) // Changed: contains | |
throw new TokenException("Invalid Audience: " + audience); | |
} | |
} | |
public bool VerifyPayload(string algorithm, byte[] bytesToSign, byte[] sentSignatureBytes) | |
{ | |
var isHmac = HmacAlgorithms.ContainsKey(algorithm); | |
var isRsa = RsaSignAlgorithms.ContainsKey(algorithm); | |
if (!isHmac && !isRsa) | |
throw new NotSupportedException("Invalid algoritm: " + algorithm); | |
if (isHmac) | |
{ | |
if (AuthKey == null) | |
throw new NotSupportedException("AuthKey required to use: " + HashAlgorithm); | |
var authKeys = new List<byte[]> { AuthKey }; | |
authKeys.AddRange(FallbackAuthKeys); | |
foreach (var authKey in authKeys) | |
{ | |
var calcSignatureBytes = HmacAlgorithms[algorithm](authKey, bytesToSign); | |
if (calcSignatureBytes.EquivalentTo(sentSignatureBytes)) | |
return true; | |
} | |
} | |
else | |
{ | |
if (PublicKey == null) | |
throw new NotSupportedException("PublicKey required to use: " + HashAlgorithm); | |
var publicKeys = new List<RSAParameters> { PublicKey.Value }; | |
publicKeys.AddRange(FallbackPublicKeys); | |
foreach (var publicKey in publicKeys) | |
{ | |
var verified = RsaVerifyAlgorithms[algorithm](publicKey, bytesToSign, sentSignatureBytes); | |
if (verified) | |
return true; | |
} | |
} | |
return false; | |
} | |
static int? GetUnixTime(Dictionary<string, string> jwtPayload, string key) | |
{ | |
string value; | |
if (jwtPayload.TryGetValue(key, out value) && !string.IsNullOrEmpty(value)) | |
{ | |
try | |
{ | |
return int.Parse(value); | |
} | |
catch (Exception) | |
{ | |
throw new TokenException($"Claim '{key}' must be a Unix Timestamp"); | |
} | |
} | |
return null; | |
} | |
public void Register(IAppHost appHost, AuthFeature feature) | |
{ | |
var isHmac = HmacAlgorithms.ContainsKey(HashAlgorithm); | |
var isRsa = RsaSignAlgorithms.ContainsKey(HashAlgorithm); | |
if (!isHmac && !isRsa) | |
throw new NotSupportedException("Invalid algoritm: " + HashAlgorithm); | |
if (isHmac && AuthKey == null) | |
throw new ArgumentNullException("AuthKey", "An AuthKey is Required to use JWT, e.g: new JwtAuthProvider { AuthKey = AesUtils.CreateKey() }"); | |
else if (isRsa && PrivateKey == null && PublicKey == null) | |
throw new ArgumentNullException("PrivateKey", "PrivateKey is Required to use JWT with " + HashAlgorithm); | |
if (KeyId == null) | |
KeyId = GetKeyId(); | |
foreach (var registerService in ServiceRoutes) | |
{ | |
appHost.RegisterService(registerService.Key, registerService.Value); | |
} | |
feature.AuthResponseDecorator = AuthenticateResponseDecorator; | |
} | |
public object AuthenticateResponseDecorator(AuthFilterContext authCtx) | |
{ | |
if (authCtx.AuthResponse.BearerToken == null || authCtx.AuthRequest.UseTokenCookie != true) | |
return authCtx.AuthResponse; | |
authCtx.AuthService.Request.RemoveSession(authCtx.AuthService.GetSessionId()); | |
return new HttpResult(authCtx.AuthResponse) | |
{ | |
Cookies = { | |
new Cookie(Keywords.TokenCookie, authCtx.AuthResponse.BearerToken, Cookies.RootPath) { | |
HttpOnly = true, | |
Secure = authCtx.AuthService.Request.IsSecureConnection, | |
Expires = DateTime.UtcNow.Add(ExpireTokensIn), | |
} | |
} | |
}; | |
} | |
} | |
// Changed: added this extension method (& the call) | |
public static class CustomSessionExtensions | |
{ | |
/// <summary> | |
/// ServiceStack uses different keys in the jwt dictionary as IdentityServer | |
/// https://demo.identityserver.io/.well-known/openid-configuration -> claims_supported. | |
/// </summary> | |
public static void PopulateFromIdentityServerMap(this IAuthSession session, Dictionary<string, string> map) | |
{ | |
var authSession = session as AuthUserSession ?? new AuthUserSession(); //Null Object Pattern | |
foreach (var entry in map) | |
{ | |
switch (entry.Key) | |
{ | |
case "auth_time": | |
session.CreatedAt = long.Parse(entry.Value).FromUnixTime(); | |
break; | |
case "nickname": | |
session.UserAuthName = entry.Value; | |
authSession.Nickname = entry.Value; | |
session.UserName = entry.Value; | |
break; | |
case "given_name": | |
session.FirstName = entry.Value; | |
break; | |
case "family_name": | |
session.LastName = entry.Value; | |
break; | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment