Skip to content

Commit d8f91e4

Browse files
committed
Fix NPE with exp claim in NimbusJwtDecoderJwkSupport
Fixes gh-5168
1 parent 2bd31c9 commit d8f91e4

File tree

7 files changed

+83
-57
lines changed

7 files changed

+83
-57
lines changed

oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2Token.java

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.security.oauth2.core;
1717

18+
import org.springframework.lang.Nullable;
1819
import org.springframework.security.core.SpringSecurityCoreVersion;
1920
import org.springframework.util.Assert;
2021

@@ -38,14 +39,23 @@ public abstract class AbstractOAuth2Token implements Serializable {
3839
* Sub-class constructor.
3940
*
4041
* @param tokenValue the token value
41-
* @param issuedAt the time at which the token was issued
42-
* @param expiresAt the expiration time on or after which the token MUST NOT be accepted
4342
*/
44-
protected AbstractOAuth2Token(String tokenValue, Instant issuedAt, Instant expiresAt) {
43+
protected AbstractOAuth2Token(String tokenValue) {
44+
this(tokenValue, null, null);
45+
}
46+
47+
/**
48+
* Sub-class constructor.
49+
*
50+
* @param tokenValue the token value
51+
* @param issuedAt the time at which the token was issued, may be null
52+
* @param expiresAt the expiration time on or after which the token MUST NOT be accepted, may be null
53+
*/
54+
protected AbstractOAuth2Token(String tokenValue, @Nullable Instant issuedAt, @Nullable Instant expiresAt) {
4555
Assert.hasText(tokenValue, "tokenValue cannot be empty");
46-
Assert.notNull(issuedAt, "issuedAt cannot be null");
47-
Assert.notNull(expiresAt, "expiresAt cannot be null");
48-
Assert.isTrue(expiresAt.isAfter(issuedAt), "expiresAt must be after issuedAt");
56+
if (issuedAt != null && expiresAt != null) {
57+
Assert.isTrue(expiresAt.isAfter(issuedAt), "expiresAt must be after issuedAt");
58+
}
4959
this.tokenValue = tokenValue;
5060
this.issuedAt = issuedAt;
5161
this.expiresAt = expiresAt;
@@ -63,18 +73,18 @@ public String getTokenValue() {
6373
/**
6474
* Returns the time at which the token was issued.
6575
*
66-
* @return the time the token was issued
76+
* @return the time the token was issued or null
6777
*/
68-
public Instant getIssuedAt() {
78+
public @Nullable Instant getIssuedAt() {
6979
return this.issuedAt;
7080
}
7181

7282
/**
7383
* Returns the expiration time on or after which the token MUST NOT be accepted.
7484
*
75-
* @return the expiration time of the token
85+
* @return the expiration time of the token or null
7686
*/
77-
public Instant getExpiresAt() {
87+
public @Nullable Instant getExpiresAt() {
7888
return this.expiresAt;
7989
}
8090

@@ -92,17 +102,17 @@ public boolean equals(Object obj) {
92102
if (!this.getTokenValue().equals(that.getTokenValue())) {
93103
return false;
94104
}
95-
if (!this.getIssuedAt().equals(that.getIssuedAt())) {
105+
if (this.getIssuedAt() != null ? !this.getIssuedAt().equals(that.getIssuedAt()) : that.getIssuedAt() != null) {
96106
return false;
97107
}
98-
return this.getExpiresAt().equals(that.getExpiresAt());
108+
return this.getExpiresAt() != null ? this.getExpiresAt().equals(that.getExpiresAt()) : that.getExpiresAt() == null;
99109
}
100110

101111
@Override
102112
public int hashCode() {
103113
int result = this.getTokenValue().hashCode();
104-
result = 31 * result + this.getIssuedAt().hashCode();
105-
result = 31 * result + this.getExpiresAt().hashCode();
114+
result = 31 * result + (this.getIssuedAt() != null ? this.getIssuedAt().hashCode() : 0);
115+
result = 31 * result + (this.getExpiresAt() != null ? this.getExpiresAt().hashCode() : 0);
106116
return result;
107117
}
108118
}

oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,6 @@ public void constructorWhenTokenValueIsNullThenThrowIllegalArgumentException() {
5151
new OAuth2AccessToken(TOKEN_TYPE, null, ISSUED_AT, EXPIRES_AT);
5252
}
5353

54-
@Test(expected = IllegalArgumentException.class)
55-
public void constructorWhenIssuedAtIsNullThenThrowIllegalArgumentException() {
56-
new OAuth2AccessToken(TOKEN_TYPE, TOKEN_VALUE, null, EXPIRES_AT);
57-
}
58-
59-
@Test(expected = IllegalArgumentException.class)
60-
public void constructorWhenExpiresAtIsNullThenThrowIllegalArgumentException() {
61-
new OAuth2AccessToken(TOKEN_TYPE, TOKEN_VALUE, ISSUED_AT, null);
62-
}
63-
6454
@Test(expected = IllegalArgumentException.class)
6555
public void constructorWhenIssuedAtAfterExpiresAtThenThrowIllegalArgumentException() {
6656
new OAuth2AccessToken(TOKEN_TYPE, TOKEN_VALUE, Instant.from(EXPIRES_AT).plusSeconds(1), EXPIRES_AT);

oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcIdTokenTests.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,6 @@ public void constructorWhenTokenValueIsNullThenThrowIllegalArgumentException() {
8282
new OidcIdToken(null, Instant.ofEpochMilli(IAT_VALUE), Instant.ofEpochMilli(EXP_VALUE), CLAIMS);
8383
}
8484

85-
@Test(expected = IllegalArgumentException.class)
86-
public void constructorWhenIssuedAtIsNullThenThrowIllegalArgumentException() {
87-
new OidcIdToken(ID_TOKEN_VALUE, null, Instant.ofEpochMilli(EXP_VALUE), CLAIMS);
88-
}
89-
90-
@Test(expected = IllegalArgumentException.class)
91-
public void constructorWhenExpiresAtIsNullThenThrowIllegalArgumentException() {
92-
new OidcIdToken(ID_TOKEN_VALUE, Instant.ofEpochMilli(IAT_VALUE), null, CLAIMS);
93-
}
94-
9585
@Test(expected = IllegalArgumentException.class)
9686
public void constructorWhenClaimsIsEmptyThenThrowIllegalArgumentException() {
9787
new OidcIdToken(ID_TOKEN_VALUE, Instant.ofEpochMilli(IAT_VALUE),

oauth2/oauth2-jose/spring-security-oauth2-jose.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ dependencies {
55
compile project(':spring-security-oauth2-core')
66
compile springCoreDependency
77
compile 'com.nimbusds:nimbus-jose-jwt'
8+
9+
testCompile powerMock2Dependencies
810
}

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,15 @@ public Jwt decode(String token) throws JwtException {
103103
// Verify the signature
104104
JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null);
105105

106-
Instant expiresAt = jwtClaimsSet.getExpirationTime().toInstant();
107-
Instant issuedAt;
106+
Instant expiresAt = null;
107+
if (jwtClaimsSet.getExpirationTime() != null) {
108+
expiresAt = jwtClaimsSet.getExpirationTime().toInstant();
109+
}
110+
Instant issuedAt = null;
108111
if (jwtClaimsSet.getIssueTime() != null) {
109112
issuedAt = jwtClaimsSet.getIssueTime().toInstant();
110-
} else {
111-
// issuedAt is required in AbstractOAuth2Token so let's default to expiresAt - 1 second
113+
} else if (expiresAt != null) {
114+
// Default to expiresAt - 1 second
112115
issuedAt = Instant.from(expiresAt).minusSeconds(1);
113116
}
114117

oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTests.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,6 @@ public void constructorWhenTokenValueIsNullThenThrowIllegalArgumentException() {
7272
new Jwt(null, Instant.ofEpochMilli(IAT_VALUE), Instant.ofEpochMilli(EXP_VALUE), HEADERS, CLAIMS);
7373
}
7474

75-
@Test(expected = IllegalArgumentException.class)
76-
public void constructorWhenIssuedAtIsNullThenThrowIllegalArgumentException() {
77-
new Jwt(JWT_TOKEN_VALUE, null, Instant.ofEpochMilli(EXP_VALUE), HEADERS, CLAIMS);
78-
}
79-
80-
@Test(expected = IllegalArgumentException.class)
81-
public void constructorWhenExpiresAtIsNullThenThrowIllegalArgumentException() {
82-
new Jwt(JWT_TOKEN_VALUE, Instant.ofEpochMilli(IAT_VALUE), null, HEADERS, CLAIMS);
83-
}
84-
8575
@Test(expected = IllegalArgumentException.class)
8676
public void constructorWhenHeadersIsEmptyThenThrowIllegalArgumentException() {
8777
new Jwt(JWT_TOKEN_VALUE, Instant.ofEpochMilli(IAT_VALUE),

oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,77 @@
1515
*/
1616
package org.springframework.security.oauth2.jwt;
1717

18+
import com.nimbusds.jose.JWSAlgorithm;
19+
import com.nimbusds.jose.JWSHeader;
20+
import com.nimbusds.jwt.JWT;
21+
import com.nimbusds.jwt.JWTClaimsSet;
22+
import com.nimbusds.jwt.JWTParser;
23+
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
1824
import org.junit.Test;
25+
import org.junit.runner.RunWith;
26+
import org.powermock.core.classloader.annotations.PrepareForTest;
27+
import org.powermock.modules.junit4.PowerMockRunner;
1928
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
2029

30+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
31+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
32+
import static org.mockito.ArgumentMatchers.*;
33+
import static org.mockito.Mockito.mock;
34+
import static org.powermock.api.mockito.PowerMockito.*;
35+
2136
/**
2237
* Tests for {@link NimbusJwtDecoderJwkSupport}.
2338
*
2439
* @author Joe Grandja
2540
*/
41+
@RunWith(PowerMockRunner.class)
42+
@PrepareForTest({NimbusJwtDecoderJwkSupport.class, JWTParser.class})
2643
public class NimbusJwtDecoderJwkSupportTests {
2744
private static final String JWK_SET_URL = "https://provider.com/oauth2/keys";
2845
private static final String JWS_ALGORITHM = JwsAlgorithms.RS256;
2946

30-
@Test(expected = IllegalArgumentException.class)
47+
@Test
3148
public void constructorWhenJwkSetUrlIsNullThenThrowIllegalArgumentException() {
32-
new NimbusJwtDecoderJwkSupport(null);
49+
assertThatThrownBy(() -> new NimbusJwtDecoderJwkSupport(null))
50+
.isInstanceOf(IllegalArgumentException.class);
3351
}
3452

35-
@Test(expected = IllegalArgumentException.class)
53+
@Test
3654
public void constructorWhenJwkSetUrlInvalidThenThrowIllegalArgumentException() {
37-
new NimbusJwtDecoderJwkSupport("invalid.com");
55+
assertThatThrownBy(() -> new NimbusJwtDecoderJwkSupport("invalid.com"))
56+
.isInstanceOf(IllegalArgumentException.class);
3857
}
3958

40-
@Test(expected = IllegalArgumentException.class)
59+
@Test
4160
public void constructorWhenJwsAlgorithmIsNullThenThrowIllegalArgumentException() {
42-
new NimbusJwtDecoderJwkSupport(JWK_SET_URL, null);
61+
assertThatThrownBy(() -> new NimbusJwtDecoderJwkSupport(JWK_SET_URL, null))
62+
.isInstanceOf(IllegalArgumentException.class);
4363
}
4464

45-
@Test(expected = JwtException.class)
65+
@Test
4666
public void decodeWhenJwtInvalidThenThrowJwtException() {
4767
NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM);
48-
jwtDecoder.decode("invalid");
68+
assertThatThrownBy(() -> jwtDecoder.decode("invalid"))
69+
.isInstanceOf(JwtException.class);
70+
}
71+
72+
// gh-5168
73+
@Test
74+
public void decodeWhenExpClaimNullThenDoesNotThrowException() throws Exception {
75+
JWT jwt = mock(JWT.class);
76+
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(JWS_ALGORITHM)).build();
77+
when(jwt.getHeader()).thenReturn(header);
78+
79+
mockStatic(JWTParser.class);
80+
when(JWTParser.parse(anyString())).thenReturn(jwt);
81+
82+
DefaultJWTProcessor jwtProcessor = mock(DefaultJWTProcessor.class);
83+
whenNew(DefaultJWTProcessor.class).withAnyArguments().thenReturn(jwtProcessor);
84+
85+
JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().audience("resource1").build();
86+
when(jwtProcessor.process(any(JWT.class), eq(null))).thenReturn(jwtClaimsSet);
87+
88+
NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM);
89+
assertThatCode(() -> jwtDecoder.decode("encoded-jwt")).doesNotThrowAnyException();
4990
}
5091
}

0 commit comments

Comments
 (0)