Skip to content

Commit 2f1f42b

Browse files
croemmichsscarduzio
authored andcommitted
(JWT) Allow validating RSA, DSA, and other KeyFactory supported algorithms (sscarduzio#287)
1 parent 910e194 commit 2f1f42b

File tree

3 files changed

+131
-5
lines changed

3 files changed

+131
-5
lines changed

core/src/main/java/tech/beshu/ror/acl/blocks/rules/impl/JwtAuthSyncRule.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.jsonwebtoken.Claims;
2121
import io.jsonwebtoken.ExpiredJwtException;
2222
import io.jsonwebtoken.Jws;
23+
import io.jsonwebtoken.JwtParser;
2324
import io.jsonwebtoken.Jwts;
2425
import io.jsonwebtoken.MalformedJwtException;
2526
import io.jsonwebtoken.SignatureException;
@@ -34,17 +35,23 @@
3435
import tech.beshu.ror.settings.rules.JwtAuthRuleSettings;
3536

3637
import java.security.AccessController;
38+
import java.security.Key;
39+
import java.security.KeyFactory;
3740
import java.security.PrivilegedAction;
41+
import java.security.spec.X509EncodedKeySpec;
42+
import java.util.Base64;
3843
import java.util.Optional;
3944

4045
public class JwtAuthSyncRule extends UserRule implements Authentication {
4146

4247
private final LoggerShim logger;
4348
private final JwtAuthRuleSettings settings;
49+
private final Optional<Key> signingKeyForAlgo;
4450

4551
public JwtAuthSyncRule(JwtAuthRuleSettings settings, ESContext context) {
4652
this.logger = context.logger(getClass());
4753
this.settings = settings;
54+
this.signingKeyForAlgo = getSigningKeyForAlgo();
4855
}
4956

5057
private static Optional<String> extractToken(String authHeader) {
@@ -57,6 +64,20 @@ private static Optional<String> extractToken(String authHeader) {
5764

5865
}
5966

67+
private Optional<Key> getSigningKeyForAlgo() {
68+
if (settings.getAlgo().isPresent()) {
69+
try {
70+
byte[] decoded = Base64.getDecoder().decode(settings.getKey());
71+
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(decoded);
72+
KeyFactory kf = KeyFactory.getInstance(settings.getAlgo().get());
73+
return Optional.of(kf.generatePublic(X509publicKey));
74+
} catch (Exception e) {
75+
logger.error(e.getMessage());
76+
}
77+
}
78+
return Optional.empty();
79+
}
80+
6081
@Override
6182
public RuleExitResult match(RequestContext rc) {
6283
Optional<String> token = Optional.of(rc.getHeaders())
@@ -70,10 +91,16 @@ public RuleExitResult match(RequestContext rc) {
7091

7192
try {
7293
Jws<Claims> jws = AccessController.doPrivileged(
73-
(PrivilegedAction<Jws<Claims>>) () ->
74-
Jwts.parser()
75-
.setSigningKey(settings.getKey())
76-
.parseClaimsJws(token.get()));
94+
(PrivilegedAction<Jws<Claims>>) () -> {
95+
JwtParser parser = Jwts.parser();
96+
if (signingKeyForAlgo.isPresent()) {
97+
parser.setSigningKey(signingKeyForAlgo.get());
98+
} else {
99+
parser.setSigningKey(settings.getKey());
100+
}
101+
return parser.parseClaimsJws(token.get());
102+
}
103+
);
77104

78105
Optional<String> user = settings.getUserClaim().map(claim -> jws.getBody().get(claim, String.class));
79106
if (settings.getUserClaim().isPresent())

core/src/main/java/tech/beshu/ror/settings/rules/JwtAuthRuleSettings.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,31 @@
2727
public class JwtAuthRuleSettings implements RuleSettings, AuthKeyProviderSettings {
2828

2929
public static final String ATTRIBUTE_NAME = "jwt_auth";
30+
public static final String SIGNATURE_ALGO = "signature_algo";
3031
public static final String SIGNATURE_KEY = "signature_key";
3132
public static final String USER_CLAIM = "user_claim";
3233
public static final String HEADER_NAME = "header_name";
3334
public static final String DEFAULT_HEADER_NAME = "Authorization";
3435

3536
private final byte[] key;
3637
private final Optional<String> userClaim;
38+
private final Optional<String> algo;
3739
private final String headerName;
3840

39-
private JwtAuthRuleSettings(String key, Optional<String> userClaim, Optional<String> headerName) {
41+
private JwtAuthRuleSettings(String key, Optional<String> algo, Optional<String> userClaim, Optional<String> headerName) {
4042
if (Strings.isNullOrEmpty(key))
4143
throw new SettingsMalformedException(
4244
"Attribute '" + SIGNATURE_KEY + "' shall not evaluate to an empty string");
4345
this.key = key.getBytes();
46+
this.algo = algo;
4447
this.userClaim = userClaim;
4548
this.headerName = headerName.orElse(DEFAULT_HEADER_NAME);
4649
}
4750

4851
public static JwtAuthRuleSettings from(RawSettings settings) {
4952
return new JwtAuthRuleSettings(
5053
evalPrefixedSignatureKey(ensureString(settings, SIGNATURE_KEY)),
54+
settings.stringOpt(SIGNATURE_ALGO),
5155
settings.stringOpt(USER_CLAIM),
5256
settings.stringOpt(HEADER_NAME)
5357
);
@@ -72,6 +76,10 @@ public byte[] getKey() {
7276
return key;
7377
}
7478

79+
public Optional<String> getAlgo() {
80+
return algo;
81+
}
82+
7583
public Optional<String> getUserClaim() {
7684
return userClaim;
7785
}

core/src/test/java/tech/beshu/ror/acl/blocks/rules/impl/JwtAuthRuleTests.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
import tech.beshu.ror.requestcontext.RequestContext;
3333
import tech.beshu.ror.settings.rules.JwtAuthRuleSettings;
3434

35+
import java.security.KeyException;
36+
import java.security.KeyFactory;
37+
import java.security.PrivateKey;
38+
import java.security.spec.PKCS8EncodedKeySpec;
39+
import java.util.Base64;
3540
import java.util.Map.Entry;
3641
import java.util.Optional;
3742

@@ -46,6 +51,7 @@
4651
public class JwtAuthRuleTests {
4752

4853
private static final String SETTINGS_SIGNATURE_KEY = JwtAuthRuleSettings.SIGNATURE_KEY;
54+
private static final String SETTINGS_SIGNATURE_ALGO = JwtAuthRuleSettings.SIGNATURE_ALGO;
4955
private static final String SETTINGS_USER_CLAIM = JwtAuthRuleSettings.USER_CLAIM;
5056
private static final String ALGO = "HS256";
5157
private static final String SECRET = "123456";
@@ -73,6 +79,26 @@ public void shouldAcceptTokenWithValidSignature() {
7379
assertTrue(res.get().isMatch());
7480
}
7581

82+
@Test
83+
public void shouldAcceptTokenWithValidRSASignature() throws KeyException {
84+
String token = Jwts.builder()
85+
.setSubject(SUBJECT)
86+
.signWith(SignatureAlgorithm.valueOf("RS256"), getRsaPrivateKey())
87+
.compact();
88+
89+
RawSettings settings = makeSettings(SETTINGS_SIGNATURE_KEY, getRsaPublicKey(), SETTINGS_SIGNATURE_ALGO, "RSA");
90+
91+
RequestContext rc = getMock(token);
92+
93+
Optional<SyncRule> rule = makeRule(settings);
94+
Optional<RuleExitResult> res = rule.map(r -> r.match(rc));
95+
rc.commit();
96+
97+
assertTrue(rule.isPresent());
98+
assertTrue(res.isPresent());
99+
assertTrue(res.get().isMatch());
100+
}
101+
76102
@Test
77103
public void shouldRejectTokenWithInvalidSignature() {
78104
String token = Jwts.builder()
@@ -91,6 +117,26 @@ public void shouldRejectTokenWithInvalidSignature() {
91117
assertFalse(res.get().isMatch());
92118
}
93119

120+
@Test
121+
public void shouldRejectRSATokenWithInvalidSignature() throws KeyException {
122+
String token = Jwts.builder()
123+
.setSubject(SUBJECT)
124+
.signWith(SignatureAlgorithm.valueOf("RS256"), getRsaPrivateKey())
125+
.compact();
126+
127+
RawSettings settings = makeSettings(SETTINGS_SIGNATURE_KEY, getInvalidPublicKey(), SETTINGS_SIGNATURE_ALGO, "RSA");
128+
129+
RequestContext rc = getMock(token);
130+
131+
Optional<SyncRule> rule = makeRule(settings);
132+
Optional<RuleExitResult> res = rule.map(r -> r.match(rc));
133+
rc.commit();
134+
135+
assertTrue(rule.isPresent());
136+
assertTrue(res.isPresent());
137+
assertFalse(res.get().isMatch());
138+
}
139+
94140
@Test
95141
public void shouldAcceptAUserClaimSetting() {
96142
RawSettings settings = makeSettings(SETTINGS_SIGNATURE_KEY, SECRET,
@@ -239,4 +285,49 @@ private Optional<SyncRule> makeRule(RawSettings settings) {
239285
return Optional.empty();
240286
}
241287
}
288+
289+
private PrivateKey getRsaPrivateKey() throws KeyException {
290+
try {
291+
byte[] decoded = Base64.getMimeDecoder().decode("MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCzBElX1jA8I8K7\n" +
292+
"TXvdKV+nkvu+/qJOab50asTpDT/WlRVsL+wZLgi1+R6t5Qu4thWI3SmqEY3E0A9l\n" +
293+
"puM4vlICUiqrmPTm+UY41oQFMz4XwoP4cQh/E/g5nBykL3YPqkzYUoJhRknH+lna\n" +
294+
"wzEUafupH0N0Kc8eruG+9pM0BkLDweUFrHzXzY3C423LQSm5mYeglMYJlFcmJ9vo\n" +
295+
"MnCUmDPY4qTNlFy8U4ksBFBA1+q/ppFqeeOasAlHh7lnLAtR78I/rGLhVDBqAgO0\n" +
296+
"W2sOMDMLP584ll0zryYrulA7OEsQGYqQepSmUS9pm0243dl0gwsuGYbc0m5LP24B\n" +
297+
"F/RLQ2pJAgMBAAECggEAAPS+54cvTsLqIVHynWXBKwXv7j8x4rVR3RFM5+m4M48s\n" +
298+
"RB2lZyUFyuL/tPIKM/xU9RwpQs1BMpHh4ysW/5CUo4qIy83PUQR3yYnrvpNde4cA\n" +
299+
"aW1BHFyg8L3SsVXHjaHdMzKNm7NiZX0CydZNBsziGS8fjxlCD+njLr/mXVrDNIRs\n" +
300+
"SVQ+rZjnNIjflX7KnIYmLtN6a64mC/UPDobtmmadvyAf8Hc/o7JX1Iqy4wtIuEFb\n" +
301+
"qf82+xXPcEJqST0fFfWcMp3WEU0cyWNfFZWlmmqzMrJPqCJaRJMMFwawxHI4GQMW\n" +
302+
"W/3OyYT4ySdD/Lt/+rQRkR4BbI8J5h9CfNSrhYryeQKBgQDWlsXVQdgsVsC4pXay\n" +
303+
"LxjMf5zbcFxg+Jdp3koHpJS5my8cWTRFcRxyTFf8KDesKb/fEhYVV40CurZv4vKU\n" +
304+
"jHJYf+72QjAVWN6Wyjmxa9Ctc6n1OdZ4gHwBdYNnJJHXhihAbzT4kzF8uccFg6Oj\n" +
305+
"Es8csXdPnJ4huNN38FWhnfdpQwKBgQDVkCh6WkmjqYSh3F+Zr/sYCc+B42hvhIbt\n" +
306+
"OLr3U1PTqgv9DRtCfPcR1oJS0kilUo2Fd+4P3xV6EJTpOJbZdIYTRkxIrl6ORDkF\n" +
307+
"0Lp01Vnzv3DVjhpL4oMdWAVTC7BLJCN8inmz+Pf6RndJrBgLz2HQXMN3NCm5b+21\n" +
308+
"ojK0iGHvgwKBgFrdl0H5UrdbuNm3Pu6uoLqfYuVMy+FIAp2SwhhAabW6b5V6dHbf\n" +
309+
"MaN4jl05DnH5b8TenLlGzHAWbgAswnmCizzMV3yxhDjV29NQKGPneoKoEpTDe/yk\n" +
310+
"s13Oy+iWBKeVqF+4d162vWLKK+s61cTMxySoRRRSBmfTIsCL5Ua9ZDGPAoGAcn8X\n" +
311+
"NIGzeUspEJ5Vos/2jqyz069YDnG+5O/FTVQfXRuN0d10//B/hdC7jiuvRvM7bJMf\n" +
312+
"zuKLYSYCsAbm2S7fsvW9cDoL97ob2EJPtNOtpkC8/cFx171ZDiJiuGNL4P0/CUY0\n" +
313+
"eYjBaizdR2I8ghhtGIijQwV0WTbo+rg69w8ncoECgYBmf4xoW03WYtzGkinhN6FQ\n" +
314+
"SZt3/ATmJR0iLFzcvMncP+4xGq1J1oL7v0ArUX1mWGfJRS27zgH7k/qJprABnJnI\n" +
315+
"0TXjhBObmkicvOm11rYK2he2g+eW5RbZpr7FfrNuiZjMOmJn8dWHuwtboNcuEF3A\n" +
316+
"6Mzj9h2krlUiyKMi0IKLHw==");
317+
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
318+
KeyFactory kf = KeyFactory.getInstance("RSA");
319+
return kf.generatePrivate(spec);
320+
} catch (Exception e) {
321+
throw new KeyException(e);
322+
}
323+
}
324+
325+
private String getRsaPublicKey() {
326+
return "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAswRJV9YwPCPCu0173Slfp5L7vv6iTmm+dGrE6Q0/1pUVbC/sGS4ItfkereULuLYViN0pqhGNxNAPZabjOL5SAlIqq5j05vlGONaEBTM+F8KD+HEIfxP4OZwcpC92D6pM2FKCYUZJx/pZ2sMxFGn7qR9DdCnPHq7hvvaTNAZCw8HlBax8182NwuNty0EpuZmHoJTGCZRXJifb6DJwlJgz2OKkzZRcvFOJLARQQNfqv6aRannjmrAJR4e5ZywLUe/CP6xi4VQwagIDtFtrDjAzCz+fOJZdM68mK7pQOzhLEBmKkHqUplEvaZtNuN3ZdIMLLhmG3NJuSz9uARf0S0NqSQIDAQAB";
327+
}
328+
329+
private String getInvalidPublicKey() {
330+
return getRsaPublicKey().replace("QAB", "QAC");
331+
}
332+
242333
}

0 commit comments

Comments
 (0)