/* * Copyright 2012-2017 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.springframework.security.oauth2.provider.token.store.jwk; import org.springframework.security.jwt.Jwt; import org.springframework.security.jwt.JwtHelper; import org.springframework.security.jwt.crypto.sign.SignatureVerifier; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.common.util.JsonParser; import org.springframework.security.oauth2.common.util.JsonParserFactory; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import java.util.Map; import static org.springframework.security.oauth2.provider.token.store.jwk.JwkAttributes.ALGORITHM; import static org.springframework.security.oauth2.provider.token.store.jwk.JwkAttributes.KEY_ID; /** * A specialized extension of {@link JwtAccessTokenConverter} that is responsible for verifying * the JSON Web Signature (JWS) for a JSON Web Token (JWT) using the corresponding JSON Web Key (JWK). * This implementation is associated with a {@link JwkDefinitionSource} for looking up * the matching {@link JwkDefinition} using the value of the JWT header parameter <b>"kid"</b>. * <br> * <br> * * The JWS is verified in the following step sequence: * <br> * <br> * <ol> * <li>Extract the <b>"kid"</b> parameter from the JWT header.</li> * <li>Find the matching {@link JwkDefinition} from the {@link JwkDefinitionSource} with the corresponding <b>"kid"</b> attribute.</li> * <li>Obtain the {@link SignatureVerifier} associated with the {@link JwkDefinition} via the {@link JwkDefinitionSource} and verify the signature.</li> * </ol> * <br> * <b>NOTE:</b> The algorithms currently supported by this implementation are: RS256, RS384 and RS512. * <br> * <br> * * <b>NOTE:</b> This {@link JwtAccessTokenConverter} <b>does not</b> support signing JWTs (JWS) and therefore * the {@link #encode(OAuth2AccessToken, OAuth2Authentication)} method implementation, if called, * will explicitly throw a {@link JwkException} reporting <i>"JWT signing (JWS) is not supported."</i>. * <br> * <br> * * @see JwtAccessTokenConverter * @see JwtHeaderConverter * @see JwkDefinitionSource * @see JwkDefinition * @see SignatureVerifier * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a> * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a> * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a> * * @author Joe Grandja */ class JwkVerifyingJwtAccessTokenConverter extends JwtAccessTokenConverter { private final JwkDefinitionSource jwkDefinitionSource; private final JwtHeaderConverter jwtHeaderConverter = new JwtHeaderConverter(); private final JsonParser jsonParser = JsonParserFactory.create(); /** * Creates a new instance using the provided {@link JwkDefinitionSource} * as the primary source for looking up {@link JwkDefinition}(s). * * @param jwkDefinitionSource the source for {@link JwkDefinition}(s) */ JwkVerifyingJwtAccessTokenConverter(JwkDefinitionSource jwkDefinitionSource) { this.jwkDefinitionSource = jwkDefinitionSource; } /** * Decodes and validates the supplied JWT followed by signature verification * before returning the Claims from the JWT Payload. * * @param token the JSON Web Token * @return a <code>Map</code> of the JWT Claims * @throws JwkException if the JWT is invalid or if the JWS could not be verified */ @Override protected Map<String, Object> decode(String token) { Map<String, String> headers = this.jwtHeaderConverter.convert(token); // Validate "kid" header String keyIdHeader = headers.get(KEY_ID); if (keyIdHeader == null) { throw new InvalidTokenException("Invalid JWT/JWS: " + KEY_ID + " is a required JOSE Header"); } JwkDefinition jwkDefinition = this.jwkDefinitionSource.getDefinitionLoadIfNecessary(keyIdHeader); if (jwkDefinition == null) { throw new InvalidTokenException("Invalid JOSE Header " + KEY_ID + " (" + keyIdHeader + ")"); } // Validate "alg" header String algorithmHeader = headers.get(ALGORITHM); if (algorithmHeader == null) { throw new InvalidTokenException("Invalid JWT/JWS: " + ALGORITHM + " is a required JOSE Header"); } if (!algorithmHeader.equals(jwkDefinition.getAlgorithm().headerParamValue())) { throw new InvalidTokenException("Invalid JOSE Header " + ALGORITHM + " (" + algorithmHeader + ")" + " does not match algorithm associated to JWK with " + KEY_ID + " (" + keyIdHeader + ")"); } // Verify signature SignatureVerifier verifier = this.jwkDefinitionSource.getVerifier(keyIdHeader); Jwt jwt = JwtHelper.decode(token); jwt.verifySignature(verifier); Map<String, Object> claims = this.jsonParser.parseMap(jwt.getClaims()); if (claims.containsKey(EXP) && claims.get(EXP) instanceof Integer) { Integer expiryInt = (Integer) claims.get(EXP); claims.put(EXP, new Long(expiryInt)); } return claims; } /** * This operation (JWT signing) is not supported and if called, * will throw a {@link JwkException}. * * @throws JwkException */ @Override protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { throw new JwkException("JWT signing (JWS) is not supported."); } }