Skip to content

Commit

Permalink
[pulsar-broker] Add support for other algorithms in token auth (apach…
Browse files Browse the repository at this point in the history
…e#4528)

Before this patch, all keys are read as RSA, which meant that only RSA
compatible JWT signing algorithms could be used, specifically, this
limited the use of ECDSA family of JWT keys.

This changes this by changing the signature we use to parse keys to also
take a SignatureAlgorithm and also adds a new config option
`tokenPublicAlg` which can be used to signify what algorithm the
broker/proxy should use when reading public keys. However, these all
default to RS256, which, should indicate to decode as RSA (even if
another RS/PS algoritm is used).

This also adds some new options to the Token CLI tool for those commands
that weren't respecting the algorithm, but these are defaulted to RS256
as well.
  • Loading branch information
addisonj authored and merlimat committed Jun 14, 2019
1 parent 6636c79 commit 04e5fee
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.io.IOException;
import java.security.Key;

import javax.naming.AuthenticationException;

import io.jsonwebtoken.security.SignatureException;
import org.apache.commons.lang3.StringUtils;
import org.apache.pulsar.broker.ServiceConfiguration;
import org.apache.pulsar.broker.authentication.utils.AuthTokenUtils;
Expand All @@ -46,18 +48,24 @@ public class AuthenticationProviderToken implements AuthenticationProvider {
// The token's claim that corresponds to the "role" string
final static String CONF_TOKEN_AUTH_CLAIM = "tokenAuthClaim";

// When using public key's, the algorithm of the key
final static String CONF_TOKEN_PUBLIC_ALG = "tokenPublicAlg";

final static String TOKEN = "token";

private Key validationKey;
private String roleClaim;
private SignatureAlgorithm publicKeyAlg;

@Override
public void close() throws IOException {
// noop
}

@Override
public void initialize(ServiceConfiguration config) throws IOException {
public void initialize(ServiceConfiguration config) throws IOException, IllegalArgumentException {
// we need to fetch the algorithm before we fetch the key
this.publicKeyAlg = getPublicKeyAlgType(config);
this.validationKey = getValidationKey(config);
this.roleClaim = getTokenRoleClaim(config);
}
Expand Down Expand Up @@ -130,7 +138,7 @@ private Key getValidationKey(ServiceConfiguration conf) throws IOException {
&& StringUtils.isNotBlank((String) conf.getProperty(CONF_TOKEN_PUBLIC_KEY))) {
final String validationKeyConfig = (String) conf.getProperty(CONF_TOKEN_PUBLIC_KEY);
final byte[] validationKey = AuthTokenUtils.readKeyFromUrl(validationKeyConfig);
return AuthTokenUtils.decodePublicKey(validationKey);
return AuthTokenUtils.decodePublicKey(validationKey, publicKeyAlg);
} else {
throw new IOException("No secret key was provided for token authentication");
}
Expand All @@ -144,4 +152,18 @@ private String getTokenRoleClaim(ServiceConfiguration conf) throws IOException {
return Claims.SUBJECT;
}
}

private SignatureAlgorithm getPublicKeyAlgType(ServiceConfiguration conf) throws IllegalArgumentException {
if (conf.getProperty(CONF_TOKEN_PUBLIC_ALG) != null
&& StringUtils.isNotBlank((String) conf.getProperty(CONF_TOKEN_PUBLIC_ALG))) {
String alg = (String) conf.getProperty(CONF_TOKEN_PUBLIC_ALG);
try {
return SignatureAlgorithm.forName(alg);
} catch (SignatureException ex) {
throw new IllegalArgumentException("invalid algorithm provided " + alg, ex);
}
} else {
return SignatureAlgorithm.RS256;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,38 @@ public static SecretKey decodeSecretKey(byte[] secretKey) {
return Keys.hmacShaKeyFor(secretKey);
}

public static PrivateKey decodePrivateKey(byte[] key) throws IOException {
public static PrivateKey decodePrivateKey(byte[] key, SignatureAlgorithm algType) throws IOException {
try {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(key);
KeyFactory kf = KeyFactory.getInstance("RSA");
KeyFactory kf = KeyFactory.getInstance(keyTypeForSignatureAlgorithm(algType));
return kf.generatePrivate(spec);
} catch (Exception e) {
throw new IOException("Failed to decode private key", e);
}
}

public static PublicKey decodePublicKey(byte[] key) throws IOException {

public static PublicKey decodePublicKey(byte[] key, SignatureAlgorithm algType) throws IOException {
try {
X509EncodedKeySpec spec = new X509EncodedKeySpec(key);
KeyFactory kf = KeyFactory.getInstance("RSA");
KeyFactory kf = KeyFactory.getInstance(keyTypeForSignatureAlgorithm(algType));
return kf.generatePublic(spec);
} catch (Exception e) {
throw new IOException("Failed to decode public key", e);
}
}

private static String keyTypeForSignatureAlgorithm(SignatureAlgorithm alg) {
if (alg.getFamilyName().equals("RSA")) {
return "RSA";
} else if (alg.getFamilyName().equals("ECDSA")) {
return "EC";
} else {
String msg = "The " + alg.name() + " algorithm does not support Key Pairs.";
throw new IllegalArgumentException(msg);
}
}

public static String encodeKeyBase64(Key key) {
return Encoders.BASE64.encode(key.getEncoded());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,13 @@ public void testSerializeKeyPair() throws Exception {
String privateKey = AuthTokenUtils.encodeKeyBase64(keyPair.getPrivate());
String publicKey = AuthTokenUtils.encodeKeyBase64(keyPair.getPublic());

String token = AuthTokenUtils.createToken(AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKey)),
String token = AuthTokenUtils.createToken(AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKey), SignatureAlgorithm.RS256),
SUBJECT,
Optional.empty());

@SuppressWarnings("unchecked")
Jwt<?, Claims> jwt = Jwts.parser()
.setSigningKey(AuthTokenUtils.decodePublicKey(Decoders.BASE64.decode(publicKey)))
.setSigningKey(AuthTokenUtils.decodePublicKey(Decoders.BASE64.decode(publicKey), SignatureAlgorithm.RS256))
.parse(token);

assertNotNull(jwt);
Expand Down Expand Up @@ -274,7 +274,7 @@ public void testAuthSecretKeyPair() throws Exception {
provider.initialize(conf);

// Use private key to generate token
PrivateKey privateKey = AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKeyStr));
PrivateKey privateKey = AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKeyStr), SignatureAlgorithm.RS256);
String token = AuthTokenUtils.createToken(privateKey, SUBJECT, Optional.empty());

// Pulsar protocol auth
Expand Down Expand Up @@ -318,7 +318,7 @@ public void testAuthSecretKeyPairWithCustomClaim() throws Exception {


// Use private key to generate token
PrivateKey privateKey = AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKeyStr));
PrivateKey privateKey = AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKeyStr), SignatureAlgorithm.RS256);
String token = Jwts.builder()
.setClaims(new HashMap<String, Object>() {{
put(authRoleClaim, authRole);
Expand All @@ -344,6 +344,46 @@ public String getCommandData() {
provider.close();
}

@Test
public void testAuthSecretKeyPairWithECDSA() throws Exception {
KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.ES256);

String privateKeyStr = AuthTokenUtils.encodeKeyBase64(keyPair.getPrivate());
String publicKeyStr = AuthTokenUtils.encodeKeyBase64(keyPair.getPublic());

AuthenticationProviderToken provider = new AuthenticationProviderToken();

Properties properties = new Properties();
// Use public key for validation
properties.setProperty(AuthenticationProviderToken.CONF_TOKEN_PUBLIC_KEY, publicKeyStr);
// Set that we are using EC keys
properties.setProperty(AuthenticationProviderToken.CONF_TOKEN_PUBLIC_ALG, SignatureAlgorithm.ES256.getValue());

ServiceConfiguration conf = new ServiceConfiguration();
conf.setProperties(properties);
provider.initialize(conf);

// Use private key to generate token
PrivateKey privateKey = AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKeyStr), SignatureAlgorithm.ES256);
String token = AuthTokenUtils.createToken(privateKey, SUBJECT, Optional.empty());

// Pulsar protocol auth
String subject = provider.authenticate(new AuthenticationDataSource() {
@Override
public boolean hasDataFromCommand() {
return true;
}

@Override
public String getCommandData() {
return token;
}
});
assertEquals(subject, SUBJECT);

provider.close();
}

@Test(expectedExceptions = AuthenticationException.class)
public void testAuthenticateWhenNoJwtPassed() throws AuthenticationException {
AuthenticationProviderToken provider = new AuthenticationProviderToken();
Expand Down Expand Up @@ -481,4 +521,16 @@ public void testInitializeWhenPublicKeyFilePathIsInvalid() throws IOException {

new AuthenticationProviderToken().initialize(conf);
}

@Test(expectedExceptions = IllegalArgumentException.class)
public void testValidationWhenPublicKeyAlgIsInvalid() throws IOException {
Properties properties = new Properties();
properties.setProperty(AuthenticationProviderToken.CONF_TOKEN_PUBLIC_ALG,
"invalid");

ServiceConfiguration conf = new ServiceConfiguration();
conf.setProperties(properties);

new AuthenticationProviderToken().initialize(conf);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ public void run() throws IOException {

@Parameters(commandDescription = "Create a new token")
public static class CommandCreateToken {
@Parameter(names = { "-a",
"--signature-algorithm" }, description = "The signature algorithm for the new key pair.")
SignatureAlgorithm algorithm = SignatureAlgorithm.RS256;

@Parameter(names = { "-s",
"--subject" }, description = "Specify the 'subject' or 'principal' associate with this token", required = true)
Expand Down Expand Up @@ -141,7 +144,7 @@ public void run() throws Exception {

if (privateKey != null) {
byte[] encodedKey = AuthTokenUtils.readKeyFromUrl(privateKey);
signingKey = AuthTokenUtils.decodePrivateKey(encodedKey);
signingKey = AuthTokenUtils.decodePrivateKey(encodedKey, algorithm);
} else {
byte[] encodedKey = AuthTokenUtils.readKeyFromUrl(secretKey);
signingKey = AuthTokenUtils.decodeSecretKey(encodedKey);
Expand Down Expand Up @@ -202,6 +205,10 @@ public void run() throws Exception {
@Parameters(commandDescription = "Validate a token against a key")
public static class CommandValidateToken {

@Parameter(names = { "-a",
"--signature-algorithm" }, description = "The signature algorithm for the key pair if using public key.")
SignatureAlgorithm algorithm = SignatureAlgorithm.RS256;

@Parameter(description = "The token string", arity = 1)
private java.util.List<String> args;

Expand Down Expand Up @@ -254,7 +261,7 @@ public void run() throws Exception {

if (publicKey != null) {
byte[] encodedKey = AuthTokenUtils.readKeyFromUrl(publicKey);
validationKey = AuthTokenUtils.decodePublicKey(encodedKey);
validationKey = AuthTokenUtils.decodePublicKey(encodedKey, algorithm);
} else {
byte[] encodedKey = AuthTokenUtils.readKeyFromUrl(secretKey);
validationKey = AuthTokenUtils.decodeSecretKey(encodedKey);
Expand Down
2 changes: 2 additions & 0 deletions site2/docs/reference-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Pulsar brokers are responsible for handling incoming messages from producers, di
|tlsCiphers|Specify the tls cipher the broker will use to negotiate during TLS Handshake. Multiple values can be specified, separated by commas. Example:- ```TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256```||
|tokenSecretKey| Configure the secret key to be used to validate auth tokens. The key can be specified like: `tokenSecretKey=data:base64,xxxxxxxxx` or `tokenSecretKey=file:///my/secret.key`||
|tokenPublicKey| Configure the public key to be used to validate auth tokens. The key can be specified like: `tokenPublicKey=data:base64,xxxxxxxxx` or `tokenPublicKey=file:///my/secret.key`||
|tokenPublicAlg| Configure the algorithm to be used to validate auth tokens. This can be any of the asymettric algorithms supported by Java JWT (https://github.com/jwtk/jjwt#signature-algorithms-keys) |RS256|
|tokenAuthClaim| Specify which of the token's claims will be used as the authentication "principal" or "role". The default "sub" claim will be used if this is left blank ||
|maxUnackedMessagesPerConsumer| Max number of unacknowledged messages allowed to receive messages by a consumer on a shared subscription. Broker will stop sending messages to consumer once, this limit reaches until consumer starts acknowledging messages back. Using a value of 0, is disabling unackeMessage limit check and consumer can receive messages without any restriction |50000|
|maxUnackedMessagesPerSubscription| Max number of unacknowledged messages allowed per shared subscription. Broker will stop dispatching messages to all consumers of the subscription once this limit reaches until consumer starts acknowledging messages back and unack count reaches to limit/2. Using a value of 0, is disabling unackedMessage-limit check and dispatcher can dispatch messages without any restriction |200000|
Expand Down Expand Up @@ -458,6 +459,7 @@ The [Pulsar proxy](concepts-architecture-overview.md#pulsar-proxy) can be config
|tlsCiphers|Specify the tls cipher the broker will use to negotiate during TLS Handshake. Multiple values can be specified, separated by commas. Example:- ```TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256```||
|tokenSecretKey| Configure the secret key to be used to validate auth tokens. The key can be specified like: `tokenSecretKey=data:base64,xxxxxxxxx` or `tokenSecretKey=file:///my/secret.key`||
|tokenPublicKey| Configure the public key to be used to validate auth tokens. The key can be specified like: `tokenPublicKey=data:base64,xxxxxxxxx` or `tokenPublicKey=file:///my/secret.key`||
|tokenPublicAlg| Configure the algorithm to be used to validate auth tokens. This can be any of the asymettric algorithms supported by Java JWT (https://github.com/jwtk/jjwt#signature-algorithms-keys) |RS256|
|tokenAuthClaim| Specify the token claim that will be used as the authentication "principal" or "role". The "subject" field will be used if this is left blank ||

## ZooKeeper
Expand Down
6 changes: 3 additions & 3 deletions site2/docs/security-token-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,18 @@ the brokers to allow them to validate the clients.

#### Creating a secret key

> Output file will be generated in the root of your pulsar installation directory. You can also provide absolute path for the output file.
> Output file will be generated in the root of your pulsar installation directory. You can also provide absolute path for the output file.
```shell
$ bin/pulsar tokens create-secret-key --output my-secret.key
```
To generate base64 encoded private key
To generate base64 encoded private key
```shell
$ bin/pulsar tokens create-secret-key --output /opt/my-secret.key --base64
```

### Public/Private keys

With public/private, we need to create a pair of keys.
With public/private, we need to create a pair of keys. Pulsar supports all algorithms supported by the Java JWT library shown [here](https://github.com/jwtk/jjwt#signature-algorithms-keys)

#### Creating a key pair

Expand Down

0 comments on commit 04e5fee

Please sign in to comment.