Skip to content

Commit

Permalink
Add support for multiple Audiences in JWT
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed Mar 21, 2018
1 parent 99ee3a6 commit fd5cd7b
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 14 deletions.
23 changes: 17 additions & 6 deletions src/ServiceStack/Auth/JwtAuthProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public void Execute(AuthFilterContext authContext)

public string CreateJwtBearerToken(IRequest req, IAuthSession session, IEnumerable<string> roles = null, IEnumerable<string> perms = null)
{
var jwtPayload = CreateJwtPayload(session, Issuer, ExpireTokensIn, Audience, roles, perms);
var jwtPayload = CreateJwtPayload(session, Issuer, ExpireTokensIn, Audiences, roles, perms);
CreatePayloadFilter?.Invoke(jwtPayload, session);

if (EncryptPayload)
Expand Down Expand Up @@ -141,8 +141,7 @@ public string CreateJwtRefreshToken(IRequest req, string userId, TimeSpan expire
{"exp", now.Add(expireRefreshTokenIn).ToUnixTime().ToString()},
};

if (Audience != null)
jwtPayload["aud"] = Audience;
jwtPayload.SetAudience(Audiences);

var hashAlgoritm = GetHashAlgorithm(req);
var refreshToken = CreateJwt(jwtHeader, jwtPayload, hashAlgoritm);
Expand Down Expand Up @@ -277,7 +276,7 @@ public static JsonObject CreateJwtHeader(string algorithm, string keyId = null)

public static JsonObject CreateJwtPayload(
IAuthSession session, string issuer, TimeSpan expireIn,
string audience=null,
IEnumerable<string> audiences=null,
IEnumerable<string> roles=null,
IEnumerable<string> permissions =null)
{
Expand All @@ -290,8 +289,7 @@ public static JsonObject CreateJwtHeader(string algorithm, string keyId = null)
{"exp", now.Add(expireIn).ToUnixTime().ToString()},
};

if (audience != null)
jwtPayload["aud"] = audience;
jwtPayload.SetAudience(audiences?.ToList());

if (!string.IsNullOrEmpty(session.Email))
jwtPayload["email"] = session.Email;
Expand Down Expand Up @@ -444,4 +442,17 @@ public object Any(GetAccessToken request)
};
}
}

internal static class JwtAuthProviderUtils
{
internal static void SetAudience(this JsonObject jwtPayload, List<string> audiences)
{
if (audiences?.Count > 0)
{
jwtPayload["aud"] = audiences.Count == 1
? audiences[0]
: audiences.ToJson();
}
}
}
}
43 changes: 36 additions & 7 deletions src/ServiceStack/Auth/JwtAuthProviderReader.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using ServiceStack.Configuration;
Expand Down Expand Up @@ -97,7 +98,24 @@ public class JwtAuthProviderReader : AuthProvider, IAuthWithRequest, IAuthPlugin
/// <summary>
/// The Audience to embed in the token. (default null)
/// </summary>
public string Audience { get; set; }
public string Audience
{
get => Audiences.Join(",");
set
{
Audiences.Clear();
if (!string.IsNullOrEmpty(value))
{
Audiences.Add(value);
}
}
}

/// <summary>
/// Embed Multiple Audiences in the token. (default none)
/// A JWT is valid if it contains ANY audience in this List
/// </summary>
public List<string> Audiences { get; set; }

/// <summary>
/// What Id to use to identify the Key used to sign the token. (default First 3 chars of Base64 Key)
Expand Down Expand Up @@ -253,6 +271,7 @@ public virtual void Init(IAppSettings appSettings = null)
RequireHashAlgorithm = true;
RemoveInvalidTokenCookie = true;
Issuer = "ssjwt";
Audiences = new List<string>();
ExpireTokensIn = TimeSpan.FromDays(14);
ExpireRefreshTokensIn = TimeSpan.FromDays(365);
FallbackAuthKeys = new List<byte[]>();
Expand All @@ -268,8 +287,13 @@ public virtual void Init(IAppSettings appSettings = null)
IncludeJwtInConvertSessionToTokenResponse = appSettings.Get("jwt.IncludeJwtInConvertSessionToTokenResponse", IncludeJwtInConvertSessionToTokenResponse);

Issuer = appSettings.GetString("jwt.Issuer");
Audience = appSettings.GetString("jwt.Audience");
KeyId = appSettings.GetString("jwt.KeyId");
Audience = appSettings.GetString("jwt.Audience");
var audiences = appSettings.GetList("jwt.Audiences");
if (!audiences.IsEmpty())
{
Audiences = audiences.ToList();
}

var hashAlg = appSettings.GetString("jwt.HashAlgorithm");
if (!string.IsNullOrEmpty(hashAlg))
Expand Down Expand Up @@ -591,8 +615,13 @@ public string GetInvalidJwtPayloadError(JsonObject jwtPayload)

if (jwtPayload.TryGetValue("aud", out var audience))
{
if (audience != Audience)
return "Invalid Audience: " + audience;
var jwtAudiences = audience.FromJson<List<string>>();
if (jwtAudiences?.Count > 0 && Audiences.Count > 0)
{
var containsAnyAudience = jwtAudiences.Any(x => Audiences.Contains(x));
if (!containsAnyAudience)
return "Invalid Audience: " + audience;
}
}

return null;
Expand Down Expand Up @@ -663,9 +692,9 @@ public void Register(IAppHost appHost, AuthFeature feature)
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);
throw new ArgumentNullException(nameof(AuthKey), "An AuthKey is Required to use JWT, e.g: new JwtAuthProvider { AuthKey = AesUtils.CreateKey() }");
if (isRsa && PrivateKey == null && PublicKey == null)
throw new ArgumentNullException(nameof(PrivateKey), "PrivateKey is Required to use JWT with " + HashAlgorithm);

if (KeyId == null)
KeyId = GetKeyId(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public void Can_manually_create_an_authenticated_UserSession_in_Token()
},
issuer: jwtProvider.Issuer,
expireIn: jwtProvider.ExpireTokensIn,
audience: jwtProvider.Audience,
audiences: jwtProvider.Audiences,
roles: new[] {"TheRole"},
permissions: new[] {"ThePermission"});

Expand Down Expand Up @@ -571,5 +571,45 @@ public void Does_not_validate_invalid_token()
Assert.That(jwtProvider.GetValidJwtPayload(expiredJwt), Is.Null);
}

[Test]
public void Does_validate_multiple_audiences()
{
var jwtProvider = (JwtAuthProvider)AuthenticateService.GetAuthProvider(JwtAuthProviderReader.Name);

string CreateJwtWithAudiences(params string[] audiences)
{
var header = JwtAuthProvider.CreateJwtHeader(jwtProvider.HashAlgorithm);
var body = JwtAuthProvider.CreateJwtPayload(new AuthUserSession
{
UserAuthId = "1",
DisplayName = "Test",
Email = "as@if.com",
IsAuthenticated = true,
},
issuer: jwtProvider.Issuer,
expireIn: jwtProvider.ExpireTokensIn,
audiences: audiences);

var jwtToken = JwtAuthProvider.CreateJwt(header, body, jwtProvider.GetHashAlgorithm());
return jwtToken;
}

jwtProvider.Audiences = new List<string> { "foo", "bar" };
var jwtNoAudience = CreateJwtWithAudiences();
Assert.That(jwtProvider.IsJwtValid(jwtNoAudience));

var jwtWrongAudience = CreateJwtWithAudiences("qux");
Assert.That(!jwtProvider.IsJwtValid(jwtWrongAudience));

var jwtPartialAudienceMatch = CreateJwtWithAudiences("bar","qux");
Assert.That(jwtProvider.IsJwtValid(jwtPartialAudienceMatch));

jwtProvider.Audience = "foo";
Assert.That(!jwtProvider.IsJwtValid(jwtPartialAudienceMatch));

jwtProvider.Audience = null;
Assert.That(jwtProvider.IsJwtValid(jwtPartialAudienceMatch));
}

}
}

0 comments on commit fd5cd7b

Please sign in to comment.