From 84f9b6818687769ade6ab478adb54363604d5798 Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Mon, 14 Mar 2016 11:05:15 -0700 Subject: [PATCH] Copy and alter Spring JWT Helper for "kid" header [#107773584] https://www.pivotaltracker.com/story/show/107773584 Signed-off-by: Jonathan Lo --- .../uaa/audit/event/AbstractUaaEvent.java | 3 +- .../uaa/audit/event/TokenIssuedEvent.java | 2 +- .../uaa/oauth/CheckTokenEndpoint.java | 2 +- .../identity/uaa/oauth/KeyInfo.java | 7 +- .../identity/uaa/oauth/UaaTokenServices.java | 2 +- .../uaa/oauth/jwt/HeaderParameters.java | 25 ++ .../uaa/oauth/jwt/IdentifiedSigner.java | 26 ++ .../identity/uaa/oauth/jwt/Jwt.java | 19 + .../identity/uaa/oauth/jwt/JwtAlgorithms.java | 79 ++++ .../identity/uaa/oauth/jwt/JwtHelper.java | 344 ++++++++++++++++++ .../identity/uaa/oauth/jwt/Signer.java | 5 + ...sitiveOAuth2SecurityExpressionMethods.java | 2 +- .../uaa/oauth/UaaTokenServicesTests.java | 3 +- .../identity/uaa/oauth/jwt/JwtHelperTest.java | 20 + .../AbstractOAuth2AccessTokenMatchers.java | 2 +- shared_versions.gradle | 2 +- ...uthorizationCodeGrantIntegrationTests.java | 2 +- .../uaa/integration/LdapIntegationTests.java | 2 +- ...orizationWithApprovalIntegrationTests.java | 2 +- .../RefreshTokenSupportIntegrationTests.java | 2 +- .../integration/feature/ImplicitGrantIT.java | 2 +- .../feature/OpenIdTokenGrantsIT.java | 2 +- .../uaa/integration/feature/SamlLoginIT.java | 2 +- .../uaa/mock/token/TokenMvcMockTests.java | 2 +- 24 files changed, 538 insertions(+), 21 deletions(-) create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/HeaderParameters.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/IdentifiedSigner.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/Jwt.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtAlgorithms.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtHelper.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/Signer.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtHelperTest.java diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/AbstractUaaEvent.java b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/AbstractUaaEvent.java index 30ba3d7c84f..8a8ec481fda 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/AbstractUaaEvent.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/AbstractUaaEvent.java @@ -12,11 +12,11 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.audit.event; -import com.fasterxml.jackson.annotation.JsonInclude; 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.audit.UaaAuditService; +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.cloudfoundry.identity.uaa.zone.IdentityZone; @@ -26,7 +26,6 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/TokenIssuedEvent.java b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/TokenIssuedEvent.java index bdbd2a8bb30..e8257a3ac0c 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/TokenIssuedEvent.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/TokenIssuedEvent.java @@ -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.JwtHelper; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.springframework.security.core.Authentication; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; import org.springframework.security.oauth2.common.OAuth2AccessToken; import java.util.Map; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpoint.java index 44efaff48a1..7c641802843 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpoint.java @@ -14,13 +14,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +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.springframework.beans.factory.InitializingBean; import org.springframework.http.ResponseEntity; import org.springframework.security.core.AuthenticationException; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java index ea91bfa1d3d..0e1b0e569ea 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java @@ -2,12 +2,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.oauth.jwt.IdentifiedSigner; +import org.cloudfoundry.identity.uaa.oauth.jwt.Signer; import org.springframework.security.jwt.crypto.sign.InvalidSignatureException; 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.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -21,12 +22,12 @@ public class KeyInfo { private String keyId; private String verifierKey = new RandomValueStringGenerator().generate(); private String signingKey = verifierKey; - private Signer signer = new MacSigner(verifierKey); + private org.springframework.security.jwt.crypto.sign.Signer signer = new MacSigner(verifierKey); private SignatureVerifier verifier = new MacSigner(signingKey); private String type = "MAC"; public Signer getSigner() { - return signer; + return new IdentifiedSigner(keyId, signer); } /** diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java index 42a37e365d3..c9c30bc3328 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java @@ -20,6 +20,7 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.cloudfoundry.identity.uaa.oauth.token.CompositeAccessToken; import org.cloudfoundry.identity.uaa.zone.TokenPolicy; import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; @@ -44,7 +45,6 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException; import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken; import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/HeaderParameters.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/HeaderParameters.java new file mode 100644 index 00000000000..992d79eb0db --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/HeaderParameters.java @@ -0,0 +1,25 @@ +package org.cloudfoundry.identity.uaa.oauth.jwt; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public interface HeaderParameters { + String getAlg(); + + String getEnc(); + + String getIv(); + + String getTyp(); + + String getKid(); +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/IdentifiedSigner.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/IdentifiedSigner.java new file mode 100644 index 00000000000..157c482bf2c --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/IdentifiedSigner.java @@ -0,0 +1,26 @@ +package org.cloudfoundry.identity.uaa.oauth.jwt; + +public class IdentifiedSigner implements Signer { + private final String id; + private final org.springframework.security.jwt.crypto.sign.Signer signer; + + public IdentifiedSigner(String id, org.springframework.security.jwt.crypto.sign.Signer signer) { + this.id = id; + this.signer = signer; + } + + @Override + public String keyId() { + return id; + } + + @Override + public byte[] sign(byte[] bytes) { + return signer.sign(bytes); + } + + @Override + public String algorithm() { + return signer.algorithm(); + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/Jwt.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/Jwt.java new file mode 100644 index 00000000000..b7007abf76a --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/Jwt.java @@ -0,0 +1,19 @@ +/* + * Copyright 2006-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.cloudfoundry.identity.uaa.oauth.jwt; + +import org.springframework.security.jwt.crypto.sign.SignatureVerifier; + +public interface Jwt extends org.springframework.security.jwt.Jwt { + HeaderParameters getHeader(); +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtAlgorithms.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtAlgorithms.java new file mode 100644 index 00000000000..c1b68eb5566 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtAlgorithms.java @@ -0,0 +1,79 @@ +/* + * Copyright 2006-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.cloudfoundry.identity.uaa.oauth.jwt; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.security.jwt.crypto.cipher.CipherMetadata; + +/** + * @author Luke Taylor + */ +public class JwtAlgorithms { + private static final Map sigAlgs = new HashMap(); + private static final Map javaToSigAlgs = new HashMap(); + private static final Map keyAlgs = new HashMap(); + private static final Map javaToKeyAlgs = new HashMap(); + + static { + sigAlgs.put("HS256", "HMACSHA256"); + sigAlgs.put("HS384" , "HMACSHA384"); + sigAlgs.put("HS512" , "HMACSHA512"); + sigAlgs.put("RS256" , "SHA256withRSA"); + sigAlgs.put("RS512" , "SHA512withRSA"); + + keyAlgs.put("RSA1_5" , "RSA/ECB/PKCS1Padding"); + + for(Map.Entry e: sigAlgs.entrySet()) { + javaToSigAlgs.put(e.getValue(), e.getKey()); + } + for(Map.Entry e: keyAlgs.entrySet()) { + javaToKeyAlgs.put(e.getValue(), e.getKey()); + } + + } + + static String sigAlg(String javaName){ + String alg = javaToSigAlgs.get(javaName); + + if (alg == null) { + throw new IllegalArgumentException("Invalid or unsupported signature algorithm: " + javaName); + } + + return alg; + } + + static String keyEncryptionAlg(String javaName) { + String alg = javaToKeyAlgs.get(javaName); + + if (alg == null) { + throw new IllegalArgumentException("Invalid or unsupported key encryption algorithm: " + javaName); + } + + return alg; + } + + static String enc(CipherMetadata cipher) { + if (!cipher.algorithm().equalsIgnoreCase("AES/CBC/PKCS5Padding")) { + throw new IllegalArgumentException("Unknown or unsupported algorithm"); + } + if (cipher.keySize() == 128) { + return "A128CBC"; + } else if (cipher.keySize() == 256) { + return "A256CBC"; + } else { + throw new IllegalArgumentException("Unsupported key size"); + } + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtHelper.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtHelper.java new file mode 100644 index 00000000000..fb4e0c9c8f4 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtHelper.java @@ -0,0 +1,344 @@ +/* + * Copyright 2006-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.cloudfoundry.identity.uaa.oauth.jwt; + +import static org.cloudfoundry.identity.uaa.oauth.jwt.JwtAlgorithms.sigAlg; +import static org.springframework.security.jwt.codec.Codecs.b64UrlDecode; +import static org.springframework.security.jwt.codec.Codecs.b64UrlEncode; +import static org.springframework.security.jwt.codec.Codecs.concat; +import static org.springframework.security.jwt.codec.Codecs.utf8Decode; +import static org.springframework.security.jwt.codec.Codecs.utf8Encode; + +import java.nio.CharBuffer; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.security.jwt.BinaryFormat; +import org.springframework.security.jwt.crypto.sign.SignatureVerifier; + +/** + * @author Luke Taylor + * @author Dave Syer + */ +public class JwtHelper { + static byte[] PERIOD = utf8Encode("."); + + /** + * Creates a token from an encoded token string. + * + * @param token the (non-null) encoded token (three Base-64 encoded strings separated + * by "." characters) + */ + public static Jwt decode(String token) { + int firstPeriod = token.indexOf('.'); + int lastPeriod = token.lastIndexOf('.'); + + if (firstPeriod <= 0 || lastPeriod <= firstPeriod) { + throw new IllegalArgumentException("JWT must have 3 tokens"); + } + CharBuffer buffer = CharBuffer.wrap(token, 0, firstPeriod); + // TODO: Use a Reader which supports CharBuffer + JwtHeader header = JwtHeaderHelper.create(buffer.toString()); + + buffer.limit(lastPeriod).position(firstPeriod + 1); + byte[] claims = b64UrlDecode(buffer); + boolean emptyCrypto = lastPeriod == token.length() - 1; + + byte[] crypto; + + if (emptyCrypto) { + if (!"none".equals(header.parameters.alg)) { + throw new IllegalArgumentException( + "Signed or encrypted token must have non-empty crypto segment"); + } + crypto = new byte[0]; + } + else { + buffer.limit(token.length()).position(lastPeriod + 1); + crypto = b64UrlDecode(buffer); + } + return new JwtImpl(header, claims, crypto); + } + + public static Jwt decodeAndVerify(String token, SignatureVerifier verifier) { + Jwt jwt = decode(token); + jwt.verifySignature(verifier); + + return jwt; + } + + public static Jwt encode(CharSequence content, Signer signer) { + JwtHeader header = JwtHeaderHelper.create(signer); + byte[] claims = utf8Encode(content); + byte[] crypto = signer + .sign(concat(b64UrlEncode(header.bytes()), PERIOD, b64UrlEncode(claims))); + return new JwtImpl(header, claims, crypto); + } +} + +/** + * Helper object for JwtHeader. + * + * Handles the JSON parsing and serialization. + */ +class JwtHeaderHelper { + + static JwtHeader create(String header) { + byte[] bytes = b64UrlDecode(header); + return new JwtHeader(bytes, parseParams(bytes)); + } + + static JwtHeader create(Signer signer) { + HeaderParametersImpl p = new HeaderParametersImpl(sigAlg(signer.algorithm()), null, null, signer.keyId()); + return new JwtHeader(serializeParams(p), p); + } + + static HeaderParametersImpl parseParams(byte[] header) { + Map map = parseMap(utf8Decode(header)); + String alg = map.get("alg"), enc = map.get("enc"), iv = map.get("iv"), + typ = map.get("typ"); + if (typ != null && !"JWT".equalsIgnoreCase(typ)) { + throw new IllegalArgumentException("typ is not \"JWT\""); + } + String kid = map.get("kid"); + return new HeaderParametersImpl(alg, enc, iv, kid); + } + + private static Map parseMap(String json) { + if (json != null) { + json = json.trim(); + if (json.startsWith("{")) { + return parseMapInternal(json); + } + else if (json.equals("")) { + return new LinkedHashMap(); + } + } + throw new IllegalArgumentException("Invalid JSON (null)"); + } + + private static Map parseMapInternal(String json) { + Map map = new LinkedHashMap(); + json = trimLeadingCharacter(trimTrailingCharacter(json, '}'), '{'); + for (String pair : json.split(",")) { + String[] values = pair.split(":"); + String key = strip(values[0], '"'); + String value = null; + if (values.length > 0) { + value = strip(values[1], '"'); + } + if (map.containsKey(key)) { + throw new IllegalArgumentException("Duplicate '" + key + "' field"); + } + map.put(key, value); + } + return map; + } + + private static String strip(String string, char c) { + return trimLeadingCharacter(trimTrailingCharacter(string.trim(), c), c); + } + + private static String trimTrailingCharacter(String string, char c) { + if (string.length() >= 0 && string.charAt(string.length() - 1) == c) { + return string.substring(0, string.length() - 1); + } + return string; + } + + private static String trimLeadingCharacter(String string, char c) { + if (string.length() >= 0 && string.charAt(0) == c) { + return string.substring(1); + } + return string; + } + + private static byte[] serializeParams(HeaderParametersImpl params) { + StringBuilder builder = new StringBuilder("{"); + + appendField(builder, "alg", params.alg); + if(params.kid != null) { + appendField(builder, "kid", params.kid); + } + if (params.enc != null) { + appendField(builder, "enc", params.enc); + } + if (params.iv != null) { + appendField(builder, "iv", params.iv); + } + if (params.typ != null) { + appendField(builder, "typ", params.typ); + } + builder.append("}"); + return utf8Encode(builder.toString()); + + } + + private static void appendField(StringBuilder builder, String name, String value) { + if (builder.length() > 1) { + builder.append(","); + } + builder.append("\"").append(name).append("\":\"").append(value).append("\""); + } +} + +/** + * Header part of JWT + * + */ +class JwtHeader implements BinaryFormat { + private final byte[] bytes; + + final HeaderParametersImpl parameters; + + /** + * @param bytes the decoded header + * @param parameters the parameter values contained in the header + */ + JwtHeader(byte[] bytes, HeaderParametersImpl parameters) { + this.bytes = bytes; + this.parameters = parameters; + } + + @Override + public byte[] bytes() { + return bytes; + } + + @Override + public String toString() { + return utf8Decode(bytes); + } +} + +class HeaderParametersImpl implements HeaderParameters { + final String alg; + + final String enc; + + final String kid; + + @Override + public String getAlg() { + return alg; + } + + @Override + public String getEnc() { + return enc; + } + + @Override + public String getIv() { + return iv; + } + + @Override + public String getTyp() { + return typ; + } + + @Override + public String getKid() { + return kid; + } + + final String iv; + + final String typ = "JWT"; + + protected HeaderParametersImpl(String alg, String kid) { + this(alg, null, null, kid); + } + + protected HeaderParametersImpl(String alg, String enc, String iv, String kid) { + if (alg == null) { + throw new IllegalArgumentException("alg is required"); + } + this.alg = alg; + this.enc = enc; + this.iv = iv; + this.kid = kid; + } + +} + +class JwtImpl implements Jwt { + private final JwtHeader header; + + private final byte[] content; + + private final byte[] crypto; + + private String claims; + + /** + * @param header the header, containing the JWS/JWE algorithm information. + * @param content the base64-decoded "claims" segment (may be encrypted, depending on + * header information). + * @param crypto the base64-decoded "crypto" segment. + */ + JwtImpl(JwtHeader header, byte[] content, byte[] crypto) { + this.header = header; + this.content = content; + this.crypto = crypto; + claims = utf8Decode(content); + } + + /** + * Validates a signature contained in the 'crypto' segment. + * + * @param verifier the signature verifier + */ + @Override + public void verifySignature(SignatureVerifier verifier) { + verifier.verify(signingInput(), crypto); + } + + private byte[] signingInput() { + return concat(b64UrlEncode(header.bytes()), JwtHelper.PERIOD, + b64UrlEncode(content)); + } + + /** + * Allows retrieval of the full token. + * + * @return the encoded header, claims and crypto segments concatenated with "." + * characters + */ + @Override + public byte[] bytes() { + return concat(b64UrlEncode(header.bytes()), JwtHelper.PERIOD, + b64UrlEncode(content), JwtHelper.PERIOD, b64UrlEncode(crypto)); + } + + @Override + public String getClaims() { + return utf8Decode(content); + } + + @Override + public String getEncoded() { + return utf8Decode(bytes()); + } + + @Override + public String toString() { + return header + " " + claims + " [" + crypto.length + " crypto bytes]"; + } + + @Override + public HeaderParameters getHeader() { + return header == null ? null : header.parameters; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/Signer.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/Signer.java new file mode 100644 index 00000000000..c0aa544b4b8 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/jwt/Signer.java @@ -0,0 +1,5 @@ +package org.cloudfoundry.identity.uaa.oauth.jwt; + +public interface Signer extends org.springframework.security.jwt.crypto.sign.Signer { + String keyId(); +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/security/ContextSensitiveOAuth2SecurityExpressionMethods.java b/server/src/main/java/org/cloudfoundry/identity/uaa/security/ContextSensitiveOAuth2SecurityExpressionMethods.java index 2d1a450475a..9a25f59dfcb 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/security/ContextSensitiveOAuth2SecurityExpressionMethods.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/security/ContextSensitiveOAuth2SecurityExpressionMethods.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +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.cloudfoundry.identity.uaa.zone.IdentityZone; @@ -21,7 +22,6 @@ import org.cloudfoundry.identity.uaa.zone.ZoneManagementScopes; import org.springframework.security.core.Authentication; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; import org.springframework.security.oauth2.provider.expression.OAuth2SecurityExpressionMethods; import org.springframework.util.StringUtils; diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java index b8ff683379d..5cb4e8a651c 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java @@ -18,10 +18,10 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.cloudfoundry.identity.uaa.oauth.token.CompositeAccessToken; import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; -import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; import org.cloudfoundry.identity.uaa.zone.TokenPolicy; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.approval.Approval; @@ -49,7 +49,6 @@ import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtHelperTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtHelperTest.java new file mode 100644 index 00000000000..83a96ae80dd --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/jwt/JwtHelperTest.java @@ -0,0 +1,20 @@ +package org.cloudfoundry.identity.uaa.oauth.jwt; + +import org.junit.Test; +import org.springframework.security.jwt.crypto.sign.MacSigner; + +import static org.junit.Assert.*; + +public class JwtHelperTest { + + @Test + public void testKidInHeader() { + Signer signer = new IdentifiedSigner("testKid", new MacSigner("symmetricKey")); + Jwt jwt = JwtHelper.encode("testJwtContent", signer); + assertEquals("testKid", jwt.getHeader().getKid()); + + jwt = JwtHelper.decode(jwt.getEncoded()); + assertEquals("testKid", jwt.getHeader().getKid()); + } + +} \ No newline at end of file diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/matchers/AbstractOAuth2AccessTokenMatchers.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/matchers/AbstractOAuth2AccessTokenMatchers.java index 40f2a2516a0..ab88fdaf1df 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/matchers/AbstractOAuth2AccessTokenMatchers.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/matchers/AbstractOAuth2AccessTokenMatchers.java @@ -5,11 +5,11 @@ import java.util.Map; import org.cloudfoundry.identity.uaa.oauth.SignerProvider; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; diff --git a/shared_versions.gradle b/shared_versions.gradle index 47e84d9d307..d62a6fd3d2c 100644 --- a/shared_versions.gradle +++ b/shared_versions.gradle @@ -25,7 +25,7 @@ ext { snakeYamlVersion = '1.12' springVersion = '4.2.2.RELEASE' springSecurityVersion = '4.0.3.RELEASE' - springSecurityJwtVersion = '1.0.3.RELEASE' + springSecurityJwtVersion = '1.0.4.RELEASE' springSecurityOAuthVersion = '2.0.8.RELEASE' springSecurityLdapVersion = '2.0.3.RELEASE' springSecuritySamlVersion = '1.0.1.RELEASE' diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/AuthorizationCodeGrantIntegrationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/AuthorizationCodeGrantIntegrationTests.java index 8e2224f3ea9..2dfba790166 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/AuthorizationCodeGrantIntegrationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/AuthorizationCodeGrantIntegrationTests.java @@ -19,7 +19,7 @@ import org.junit.Rule; import org.junit.Test; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; import java.util.Map; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java index 0055b83e7d0..b45b0a3bfa5 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java @@ -27,7 +27,7 @@ import org.junit.Rule; import org.junit.Test; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.web.client.RestTemplate; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/OpenIdTokenAuthorizationWithApprovalIntegrationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/OpenIdTokenAuthorizationWithApprovalIntegrationTests.java index ef1e1c97530..a8918d191b3 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/OpenIdTokenAuthorizationWithApprovalIntegrationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/OpenIdTokenAuthorizationWithApprovalIntegrationTests.java @@ -35,7 +35,7 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler; import org.springframework.security.oauth2.client.test.OAuth2ContextConfiguration; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/RefreshTokenSupportIntegrationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/RefreshTokenSupportIntegrationTests.java index 4892082742a..a4ce8f31235 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/RefreshTokenSupportIntegrationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/RefreshTokenSupportIntegrationTests.java @@ -33,7 +33,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.security.jwt.JwtHelper; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ImplicitGrantIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ImplicitGrantIT.java index f95aefc7581..4c9e9863697 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ImplicitGrantIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ImplicitGrantIT.java @@ -41,7 +41,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.springframework.security.oauth2.client.test.TestAccounts; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OpenIdTokenGrantsIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OpenIdTokenGrantsIT.java index d8be5487e6a..192abe1b547 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OpenIdTokenGrantsIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OpenIdTokenGrantsIT.java @@ -36,7 +36,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.springframework.security.oauth2.client.test.OAuth2ContextConfiguration; import org.springframework.security.oauth2.client.test.TestAccounts; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java index 8f74ad5a6e8..8a3986a806d 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java @@ -54,7 +54,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.springframework.security.oauth2.client.test.TestAccounts; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.client.BaseClientDetails; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java index 54ebd0f3139..d95bba0f713 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java @@ -58,7 +58,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.codec.Base64; import org.springframework.security.jwt.Jwt; -import org.springframework.security.jwt.JwtHelper; +import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;