Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: remove spring security jwt and use nimbus jose #2624

Merged
merged 25 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9eb9f61
Refactor: remove spring security jwt and use nimbus jose
strehle Nov 28, 2023
f76d255
fixes from non OIDC standard tests
strehle Nov 28, 2023
20d6831
Merge branch 'fix/tokenNonStandard' into eol/replaceSpringJwt
strehle Nov 29, 2023
a105e63
refactoring token to claim parser
strehle Nov 29, 2023
affc0cc
Merge remote-tracking branch 'origin/develop' into eol/replaceSpringJwt
strehle Nov 29, 2023
5d7c941
test fix
strehle Nov 29, 2023
96b1cc1
test fixes
strehle Nov 29, 2023
4897c98
sonar fixes
strehle Nov 30, 2023
6cdf61f
Merge branch 'develop' of github.com:cloudfoundry/uaa into eol/replac…
strehle Nov 30, 2023
f6bed5b
sonar refactorings
strehle Nov 30, 2023
8685f62
Merge branch 'develop' of github.com:cloudfoundry/uaa into eol/replac…
strehle Nov 30, 2023
ebd2433
renaming to have less differences in library replacement
strehle Dec 1, 2023
a6a79f6
Merge remote-tracking branch 'origin/develop' into eol/replaceSpringJwt
strehle Dec 16, 2023
d66603b
fixed sonar smells
strehle Dec 19, 2023
56e1bc7
Merge remote-tracking branch 'origin/develop' into eol/replaceSpringJwt
strehle Dec 19, 2023
b7e4c01
Merge remote-tracking branch 'origin/develop' into eol/replaceSpringJwt
strehle Dec 19, 2023
5b45227
Merge remote-tracking branch 'origin/develop' into eol/replaceSpringJwt
strehle Dec 21, 2023
aadb0d8
review
strehle Dec 21, 2023
39b2d85
use interface Verifier
strehle Dec 21, 2023
e22e9c3
review
strehle Dec 21, 2023
1ed82f8
generated value
strehle Dec 21, 2023
662bfed
Merge branch 'develop' of github.com:cloudfoundry/uaa into eol/replac…
strehle Jan 7, 2024
a50fc60
fix rebase from develop
strehle Jan 7, 2024
2b664fa
Merge remote-tracking branch 'origin/develop' into eol/replaceSpringJwt
strehle Jan 9, 2024
6bc3228
introduce constant DEFAULT_UAA_URL
strehle Jan 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ versions.hamcrestVersion = "2.2"
versions.springBootVersion = "2.7.18"
versions.springFrameworkVersion = "5.3.31"
versions.springSecurityVersion = "5.8.9"
versions.springSecurityJwtVersion = "1.1.1.RELEASE"
versions.springSecurityOAuthVersion = "2.5.2.RELEASE"
versions.springSecuritySamlVersion = "1.0.10.RELEASE"
versions.tomcatCargoVersion = "9.0.84"
Expand Down Expand Up @@ -101,7 +100,6 @@ libraries.springRestdocs = "org.springframework.restdocs:spring-restdocs-mockmvc
libraries.springRetry = "org.springframework.retry:spring-retry"
libraries.springSecurityConfig = "org.springframework.security:spring-security-config:${versions.springSecurityVersion}"
libraries.springSecurityCore = "org.springframework.security:spring-security-core:${versions.springSecurityVersion}"
libraries.springSecurityJwt = "org.springframework.security:spring-security-jwt:${versions.springSecurityJwtVersion}"
libraries.springSecurityLdap = "org.springframework.security:spring-security-ldap:${versions.springSecurityVersion}"
libraries.springSecurityOauth = "org.springframework.security.oauth:spring-security-oauth2:${versions.springSecurityOAuthVersion}"
libraries.springSecuritySaml = "org.springframework.security.extensions:spring-security-saml2-core:${versions.springSecuritySamlVersion}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,8 @@ public static int countNonNull( Object... objects ) {
}
return count;
}

public static boolean isNotEmpty(Object object) {
return !org.springframework.util.ObjectUtils.isEmpty(object);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.net.MalformedURLException;
Expand Down Expand Up @@ -48,6 +49,8 @@ public final class UaaStringUtils {

public static final String EMPTY_STRING = "";

public static final String DEFAULT_UAA_URL = "http://localhost:8080/uaa";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, that is a step in the right direction. Following this PR, the port should be defaulted to 8080 with the capability to override.


private UaaStringUtils() {
}

Expand Down Expand Up @@ -317,4 +320,12 @@ public static String getSafeParameterValue(String[] value) {
}
return StringUtils.hasText(value[0]) ? value[0] : EMPTY_STRING;
}

public static Set<String> getValuesOrDefaultValue(Set<String> values, String defaultValue) {
if (ObjectUtils.isEmpty(values)) {
return Set.of(defaultValue);
} else {
return values;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;

import java.util.ArrayList;
import java.util.Arrays;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

class ObjectUtilsTest {

Expand All @@ -33,4 +38,11 @@ void getDocumentBuilder() throws ParserConfigurationException {
assertEquals(true, builder.isNamespaceAware());
assertEquals(false, builder.isXIncludeAware());
}

@Test
void isNotExmpty() {
assertTrue(ObjectUtils.isNotEmpty(Arrays.asList("1")));
assertFalse(ObjectUtils.isNotEmpty(new ArrayList<>()));
assertFalse(ObjectUtils.isNotEmpty(null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,13 @@ void getSafeParameterValue() {
assertEquals("", UaaStringUtils.getSafeParameterValue(null));
}

@Test
void getArrayDefaultValue() {
assertEquals(Set.of("1", "2"), UaaStringUtils.getValuesOrDefaultValue(Set.of("1", "2"), "1"));
assertEquals(Set.of("1"), UaaStringUtils.getValuesOrDefaultValue(Set.of(), "1"));
assertEquals(Set.of("1"), UaaStringUtils.getValuesOrDefaultValue(null, "1"));
}

private static void replaceZoneVariables(IdentityZone zone) {
String s = "https://{zone.subdomain}.domain.com/z/{zone.id}?id={zone.id}&domain={zone.subdomain}";
String expect = String.format("https://%s.domain.com/z/%s?id=%s&domain=%s", zone.getSubdomain(), zone.getId(), zone.getId(), zone.getSubdomain());
Expand Down
1 change: 0 additions & 1 deletion server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ dependencies {
implementation(libraries.springJdbc)
implementation(libraries.springWeb)
implementation(libraries.springSecurityCore)
implementation(libraries.springSecurityJwt)
implementation(libraries.springSecurityWeb)
implementation(libraries.springBootStarterMail)
implementation(libraries.spingSamlEsapiDependencyVersion) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
import org.cloudfoundry.identity.uaa.audit.AuditEventType;
import org.cloudfoundry.identity.uaa.audit.UaaAuditService;
import org.cloudfoundry.identity.uaa.oauth.UaaOauth2Authentication;
import org.cloudfoundry.identity.uaa.oauth.jwt.Jwt;
import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper;
import org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.springframework.context.ApplicationEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
import com.fasterxml.jackson.core.type.TypeReference;
import org.cloudfoundry.identity.uaa.audit.AuditEvent;
import org.cloudfoundry.identity.uaa.audit.AuditEventType;
import org.cloudfoundry.identity.uaa.oauth.jwt.Jwt;
import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.oauth2.common.OAuth2AccessToken;

import java.util.Map;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import org.cloudfoundry.identity.uaa.oauth.KeyInfo;
import org.cloudfoundry.identity.uaa.oauth.KeyInfoService;
import org.cloudfoundry.identity.uaa.oauth.jwt.ChainedSignatureVerifier;
import org.cloudfoundry.identity.uaa.oauth.jwt.SignatureVerifier;
import org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants;
import org.cloudfoundry.identity.uaa.util.JwtTokenSignedByThisUAA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.cloudfoundry.identity.uaa.zone.MultitenantClientServices;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.NoSuchClientException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import org.cloudfoundry.identity.uaa.error.ParameterParsingException;
import org.cloudfoundry.identity.uaa.error.UaaException;
import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper;
import org.cloudfoundry.identity.uaa.oauth.token.Claims;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.cloudfoundry.identity.uaa.util.TimeService;
import org.cloudfoundry.identity.uaa.util.UaaTokenUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
Expand All @@ -14,7 +13,6 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidScopeException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
Expand Down Expand Up @@ -116,7 +114,7 @@ public Claims checkToken(@RequestParam(name = "token", required = false, defaul
throw new InvalidTokenException((x.getMessage()));
}

Claims response = getClaimsForToken(token.getValue());
Claims response = UaaTokenUtils.getClaimsFromTokenString(token.getValue());

List<String> claimScopes = response.getScope().stream().map(String::toLowerCase).collect(Collectors.toList());

Expand Down Expand Up @@ -156,24 +154,6 @@ public Claims checkToken(HttpServletRequest request) throws HttpRequestMethodNot
}
}

private Claims getClaimsForToken(String token) {
Jwt tokenJwt;
try {
tokenJwt = JwtHelper.decode(token);
} catch (Throwable t) {
throw new InvalidTokenException("Invalid token (could not decode): " + token);
}

Claims claims;
try {
claims = JsonUtils.readValue(tokenJwt.getClaims(), Claims.class);
} catch (JsonUtils.JsonUtilException e) {
throw new InvalidTokenException("Cannot read token claims", e);
}

return claims;
}

@ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package org.cloudfoundry.identity.uaa.oauth;

import org.cloudfoundry.identity.uaa.util.UaaTokenUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper;
import org.cloudfoundry.identity.uaa.oauth.token.IntrospectionClaims;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
Expand Down Expand Up @@ -42,7 +41,7 @@ public IntrospectionClaims introspect(@RequestParam("token") String token) {
return introspectionClaims;
}
resourceServerTokenServices.loadAuthentication(token);
introspectionClaims = getClaimsForToken(oAuth2AccessToken.getValue());
introspectionClaims = UaaTokenUtils.getClaims(oAuth2AccessToken.getValue(), IntrospectionClaims.class);
introspectionClaims.setActive(true);
} catch (InvalidTokenException e) {
introspectionClaims.setActive(false);
Expand All @@ -57,21 +56,4 @@ public IntrospectionClaims introspect(@RequestParam("token") String token) {
public IntrospectionClaims methodNotSupported(HttpServletRequest request) throws HttpRequestMethodNotSupportedException {
throw new HttpRequestMethodNotSupportedException(request.getMethod());
}


private IntrospectionClaims getClaimsForToken(String token) {
org.springframework.security.jwt.Jwt tokenJwt;
tokenJwt = JwtHelper.decode(token);

IntrospectionClaims claims;
try {
// we assume token.getClaims is never null due to previously parsing token when verifying the token
claims = JsonUtils.readValue(tokenJwt.getClaims(), IntrospectionClaims.class);
} catch (JsonUtils.JsonUtilException e) {
logger.error("Can't parse introspection claims in token. Is it a valid JSON?");
throw new InvalidTokenException("Cannot read token claims", e);
}

return claims;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.cloudfoundry.identity.uaa.oauth;

public class InvalidSignatureException extends RuntimeException {

private static final long serialVersionUID = 5458857726945157613L;

private InvalidSignatureException() {
}

public InvalidSignatureException(String message) {
super(message);
}

public InvalidSignatureException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,30 @@

import com.nimbusds.jose.HeaderParameterNames;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKParameterNames;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.util.Base64;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jose.util.X509CertUtils;
import org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey;
import org.cloudfoundry.identity.uaa.oauth.jwt.JwtAlgorithms;
import org.cloudfoundry.identity.uaa.oauth.jwt.SignatureVerifier;
import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper;
import org.cloudfoundry.identity.uaa.oauth.jwt.UaaMacSigner;
import org.cloudfoundry.identity.uaa.util.UaaUrlUtils;
import org.springframework.security.jwt.crypto.sign.EllipticCurveVerifier;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.jwt.crypto.sign.Signer;
import org.springframework.web.util.UriComponentsBuilder;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Collections;
import java.util.HashMap;
Expand All @@ -41,14 +38,15 @@

public class KeyInfo {
private final boolean isAsymmetric;
private Signer signer;
private JWSSigner signer;
private SignatureVerifier verifier;
private final String keyId;
private final String keyUrl;
private final String verifierKey;
private final Optional<X509Certificate> verifierCertificate;
private final JsonWebKey.KeyType type;
private final JWK jwk;
private final String algorithm;

public KeyInfo(String keyId, String signingKey, String keyUrl) {
this(keyId, signingKey, keyUrl, null, null);
Expand All @@ -57,25 +55,23 @@ public KeyInfo(String keyId, String signingKey, String keyUrl, String sigAlg, St
this.keyId = keyId;
this.keyUrl = validateAndConstructTokenKeyUrl(keyUrl);
this.isAsymmetric = isAsymmetric(signingKey);
String algorithm;
if (this.isAsymmetric) {
String jwtAlg;
KeyPair keyPair;
try {
jwk = JWK.parseFromPEMEncodedObjects(signingKey);
jwtAlg = jwk.getKeyType().getValue();
if (jwtAlg.startsWith("RSA")) {
algorithm = Optional.ofNullable(sigAlg).map(JwtAlgorithms::sigAlgJava).orElse(JwtAlgorithms.DEFAULT_RSA);
algorithm = Optional.ofNullable(sigAlg).orElse(JWSAlgorithm.RS256.getName());
keyPair = jwk.toRSAKey().toKeyPair();
PublicKey rsaPublicKey = keyPair.getPublic();
this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate(), algorithm);
this.verifier = new RsaVerifier((RSAPublicKey) rsaPublicKey, algorithm);
this.signer = new RSASSASigner(keyPair.getPrivate(), true);
this.verifier = new SignatureVerifier(keyId, algorithm, jwk);
this.type = RSA;
} else if (jwtAlg.startsWith("EC")) {
algorithm = Optional.ofNullable(sigAlg).map(JwtAlgorithms::sigAlgJava).orElse(JwtAlgorithms.DEFAULT_EC);
algorithm = Optional.ofNullable(sigAlg).orElse(JWSAlgorithm.ES256.getName());
keyPair = jwk.toECKey().toKeyPair();
this.signer = null;
this.verifier = new EllipticCurveVerifier((ECPublicKey) keyPair.getPublic(), algorithm);
this.signer = new ECDSASigner((ECPrivateKey) keyPair.getPrivate());
this.verifier = new SignatureVerifier(keyId, algorithm, jwk);
this.type = EC;
} else {
throw new IllegalArgumentException("Invalid JWK");
Expand All @@ -87,10 +83,10 @@ public KeyInfo(String keyId, String signingKey, String keyUrl, String sigAlg, St
this.verifierKey = JsonWebKey.pemEncodePublicKey(keyPair.getPublic()).orElse(null);
} else {
jwk = new OctetSequenceKey.Builder(signingKey.getBytes()).build();
algorithm = Optional.ofNullable(sigAlg).map(JwtAlgorithms::sigAlgJava).orElse(JwtAlgorithms.DEFAULT_HMAC);
algorithm = Optional.ofNullable(sigAlg).orElse(JWSAlgorithm.HS256.getName());
SecretKey hmacKey = new SecretKeySpec(signingKey.getBytes(), algorithm);
this.signer = new MacSigner(algorithm, hmacKey);
this.verifier = new MacSigner(algorithm, hmacKey);
this.signer = new UaaMacSigner(hmacKey);
this.verifier = new SignatureVerifier(keyId, algorithm, jwk);
this.verifierKey = signingKey;
this.verifierCertificate = Optional.empty();
this.type = MAC;
Expand All @@ -101,7 +97,7 @@ public SignatureVerifier getVerifier() {
return this.verifier;
}

public Signer getSigner() {
public JWSSigner getSigner() {
return this.signer;
}

Expand Down Expand Up @@ -166,7 +162,7 @@ public Map<String, Object> getJwkMap() {
}

public String algorithm() {
return JwtAlgorithms.sigAlg(verifier.algorithm());
return algorithm;
}

private static String validateAndConstructTokenKeyUrl(String keyUrl) {
Expand Down
Loading