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);
+ }
+ }
+}