diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoder.java new file mode 100644 index 00000000000..f0bf571de4d --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.jwt; + +import java.util.Map; + +/** + * Implementations of this interface are responsible for "encoding" + * a JSON Web Token (JWT) from a {@link Jwt} to it's compact claims representation format. + * + *

+ * JWTs may be represented using the JWS Compact Serialization format for a + * JSON Web Signature (JWS) structure or JWE Compact Serialization format for a + * JSON Web Encryption (JWE) structure. Implementors can pick which format to produce. + * + * @author Gergely Krajcsovszki + * @since TODO + * @see Jwt + * @see JwtDecoder + * @see JSON Web Token (JWT) + * @see JSON Web Signature (JWS) + * @see JSON Web Encryption (JWE) + * @see JWS Compact Serialization + * @see JWE Compact Serialization + */ +@FunctionalInterface +public interface JwtEncoder { + + // TODO: should the claims be a new type, or is a Map OK? + + /** + * Encodes the JWT from a set of claims to it's compact claims representation format. + * + * @param claims the JWT claims + * @return a {@link Jwt}, its {@code tokenValue} containing its compact claims representation format + * @throws JwtException if an error occurs while attempting to encode the JWT + */ + Jwt encode(Map claims) throws JwtException; +} + +// TODO: JwtEncoders a' la JwtDecoders? + +// TODO: reactive stuff diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoderFactory.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoderFactory.java new file mode 100644 index 00000000000..8acac39d884 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoderFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.jwt; + +/** + * A factory for {@link JwtEncoder}(s). + * This factory should be supplied with a type that provides + * contextual information used to create a specific {@code JwtEncoder}. + * + * @author Gergely Krajcsovszki + * @since TODO + * @see JwtEncoder + * + * @param The type that provides contextual information used to create a specific {@code JwtEncoder}. + */ +@FunctionalInterface +public interface JwtEncoderFactory { + + /** + * Creates a {@code JwtEncoder} using the supplied "contextual" type. + * + * @param context the type that provides contextual information + * @return a {@link JwtEncoder} + */ + JwtEncoder createEncoder(C context); + +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtSigningException.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtSigningException.java new file mode 100644 index 00000000000..58236f6a089 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtSigningException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.jwt; + +/** + * An exception thrown when a JWT signing-related operation fails. + * + * @author Gergely Krajcsovszki + * @since TODO + */ +public class JwtSigningException extends JwtException { + public JwtSigningException(String message) { + super(message); + } + + public JwtSigningException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java index b8e805fdfdf..f2a9149b380 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java @@ -448,6 +448,8 @@ JWTProcessor processor() { this.jwsAlgorithm + ". Please indicate one of RS256, RS384, or RS512."); } + // TODO: support EC? others? + JWSKeySelector jwsKeySelector = new SingleKeyJWSKeySelector<>(this.jwsAlgorithm, this.key); DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java new file mode 100644 index 00000000000..e30426c3611 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java @@ -0,0 +1,296 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.jwt; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.produce.JWSSignerFactory; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.springframework.lang.Nullable; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.util.Assert; + +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.HashMap; +import java.util.Map; + +/** + * A low-level Nimbus implementation of {@link JwtEncoder} which takes a raw Nimbus configuration. + *

+ * This class currently supports signing JWTs according to the JSON Web Signature (JWS) specification + * and encoding them in the JWS Compact Serialization format. + * + * @author Gergely Krajcsovszki + * @see JSON Web Signature (JWS) + * @see JWS Compact Serialization + * @since TODO + */ +public final class NimbusJwtEncoder implements JwtEncoder { + private static final String ENCODING_ERROR_MESSAGE_TEMPLATE = + "An error occurred while attempting to encode the Jwt: %s"; + private static final String SIGNER_CREATION_ERROR_MESSAGE_TEMPLATE = + "An error occurred while creating a Jwt signer: %s"; + private static final String JWK_CREATION_ERROR_MESSAGE_TEMPLATE = + "An error occurred while creating a JWK: %s"; + + private final JWSSigner jwsSigner; + + private final JWSAlgorithm jwsAlgorithm; + + /** + * Configures a {@link NimbusJwtEncoder} with the given parameters + * + * @param jwsSigner the {@link JWSSigner} to use + * @param preferredJwsAlgorithm the {@link JWSAlgorithm} to use. + * If left null, the first one returned by {@link JWSSigner#supportedJWSAlgorithms()} will be used. + * Must be compatible with the keys set in the {@link JWSSigner}. + */ + public NimbusJwtEncoder(JWSSigner jwsSigner, @Nullable JWSAlgorithm preferredJwsAlgorithm) { + Assert.notNull(jwsSigner, "jwsSigner cannot be null"); + this.jwsSigner = jwsSigner; + this.jwsAlgorithm = + (preferredJwsAlgorithm != null + ? preferredJwsAlgorithm + : jwsSigner.supportedJWSAlgorithms().iterator().next()); + } + + @Override + public Jwt encode(Map claims) throws JwtException { + JWSHeader header = createHeader(); + JWTClaimsSet claimsSet = createClaims(claims); + SignedJWT signedJWT = new SignedJWT(header, claimsSet); + try { + signedJWT.sign(jwsSigner); + } catch (JOSEException ex) { + throw new JwtSigningException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); + } + return createJwt(signedJWT); + } + + private JWTClaimsSet createClaims(Map claims) { + JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); + claims.forEach(builder::claim); + return builder.build(); + } + + private JWSHeader createHeader() { + JWSHeader.Builder builder = new JWSHeader.Builder(jwsAlgorithm); + + // TODO: add other headers + + return builder.build(); + } + + private Jwt createJwt(SignedJWT nimbusJwt) { + try { + HashMap headers = nimbusJwt.getHeader().toJSONObject(); + Map claims = nimbusJwt.getJWTClaimsSet().getClaims(); + return Jwt.withTokenValue(nimbusJwt.serialize()) + .headers(h -> h.putAll(headers)) + .claims(c -> c.putAll(claims)) + .build(); + } catch (Exception ex) { + throw new BadJwtException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); + } + } + + // TODO: builder from local JWKSet and optional JWKSelector? + + /** + * Use the private key from the given key pair to sign JWTs. The supplied {@link KeyPair} must contain + * both a public and a private key for the same, supported signing algorithm. The public key is used + * to determine the algorithm to use and to get any required parameters for it, while the private key + * will be used to generate the signature. + * + * @param keys the {@link KeyPair} to use + * @return a {@link PrivateKeyJwtEncoderBuilder} for further configurations + */ + public static PrivateKeyJwtEncoderBuilder withPrivateKey(KeyPair keys) { + return new PrivateKeyJwtEncoderBuilder(keys.getPublic(), keys.getPrivate()); + } + + /** + * Use the given {@code SecretKey} to sign JWTs + * + * @param secretKey the {@code SecretKey} to use + * @return a {@link SecretKeyJwtEncoderBuilder} for further configurations + */ + public static SecretKeyJwtEncoderBuilder withSecretKey(SecretKey secretKey) { + return new SecretKeyJwtEncoderBuilder(secretKey); + } + + /** + * A builder for creating {@link NimbusJwtEncoder} instances based on a private key. + */ + public static final class PrivateKeyJwtEncoderBuilder extends JwtEncoderBuilderBase { + + private PrivateKeyJwtEncoderBuilder(PublicKey publicKey, PrivateKey privateKey) { + super(buildJwk(publicKey, privateKey)); + } + + private static JWK buildJwk(PublicKey publicKey, PrivateKey privateKey) { + Assert.notNull(publicKey, "publicKey cannot be null"); + Assert.notNull(privateKey, "privateKey cannot be null"); + + if (publicKey instanceof RSAPublicKey) { + try { + return new RSAKey.Builder((RSAPublicKey) publicKey).privateKey(privateKey).build(); + } catch (Exception e) { + throw new JwtSigningException( + String.format(JWK_CREATION_ERROR_MESSAGE_TEMPLATE, + "Failed to create RSAKey from supplied public and private key: " + e.getMessage()), e); + } + } + + + if (publicKey instanceof ECPublicKey) { + try { + ECPublicKey ecPublicKey = (ECPublicKey) publicKey; + return new ECKey.Builder(Curve.forECParameterSpec(ecPublicKey.getParams()), ecPublicKey) + .privateKey(privateKey) + .build(); + } catch (Exception e) { + throw new JwtSigningException( + String.format(JWK_CREATION_ERROR_MESSAGE_TEMPLATE, + "Failed to create ECKey from supplied public and private key: " + e.getMessage()), e); + } + } + + throw new JwtSigningException( + String.format(JWK_CREATION_ERROR_MESSAGE_TEMPLATE, + "The supplied public key is not supported, expected " + RSAPublicKey.class.getSimpleName() + + " or " + ECPublicKey.class.getSimpleName() + ", got " + + publicKey.getClass().getSimpleName())); + } + + /** + * Use the given signing + * algorithm. + *

+ * Must be compatible with the keys set in the constructor. + *

+ * If not set, the first one in the list of supported algorithms of the {@link JWSSigner} generated + * from the {@link PrivateKey} will be used. + * + * @param signatureAlgorithm the algorithm to use + * @return a {@link PrivateKeyJwtEncoderBuilder} for further configurations + */ + public PrivateKeyJwtEncoderBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null"); + this.jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName()); + return this; + } + } + + /** + * A builder for creating {@link NimbusJwtEncoder} instances based on a {@code SecretKey}. + */ + public static final class SecretKeyJwtEncoderBuilder extends JwtEncoderBuilderBase { + + private SecretKeyJwtEncoderBuilder(SecretKey secretKey) { + super(buildJwk(secretKey)); + } + + private static JWK buildJwk(SecretKey secretKey) { + Assert.notNull(secretKey, "secretKey cannot be null"); + return new OctetSequenceKey.Builder(secretKey).build(); + } + + /** + * Use the given + * algorithm + * when generating the MAC. + *

+ * Must be compatible with the keys set in the constructor. + *

+ * If not set, the first one in the list of supported algorithms of the {@link JWSSigner} generated + * from the {@link SecretKey} will be used. + * + * @param macAlgorithm the MAC algorithm to use + * @return this builder for further configurations + */ + public SecretKeyJwtEncoderBuilder macAlgorithm(MacAlgorithm macAlgorithm) { + Assert.notNull(macAlgorithm, "macAlgorithm cannot be null"); + this.jwsAlgorithm = JWSAlgorithm.parse(macAlgorithm.getName()); + return this; + } + } + + /** + * A base class for builders for creating {@link NimbusJwtEncoder} instances. + */ + static abstract class JwtEncoderBuilderBase { + JWSAlgorithm jwsAlgorithm; + private final JWK jwk; + private JWSSignerFactory jwsSignerFactory; + + JwtEncoderBuilderBase(JWK jwk) { + this.jwk = jwk; + } + + /** + * Use the given {@link JWSSignerFactory}. + *

+ * If not specified, a {@link DefaultJWSSignerFactory} will be used. + * + * @param jwsSignerFactory the {@link JWSSignerFactory} to use + * @return this builder for further configurations + */ + @SuppressWarnings("unchecked") + public T jwsSignerFactory(JWSSignerFactory jwsSignerFactory) { + Assert.notNull(jwsSignerFactory, "jwsSignerFactory cannot be null"); + this.jwsSignerFactory = jwsSignerFactory; + return (T) this; + } + + JWSSigner jwsSigner() { + if (jwsSignerFactory == null) { + jwsSignerFactory = new DefaultJWSSignerFactory(); + } + try { + return jwsSignerFactory.createJWSSigner(jwk); + } catch (JOSEException ex) { + throw new JwtSigningException( + String.format(SIGNER_CREATION_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); + } + } + + /** + * Build the configured {@link NimbusJwtEncoder}. + * + * @return the configured {@link NimbusJwtEncoder} + */ + public NimbusJwtEncoder build() { + return new NimbusJwtEncoder(jwsSigner(), jwsAlgorithm); + } + } +}