Skip to content

Commit

Permalink
MINOR: Test SASL authorization id
Browse files Browse the repository at this point in the history
Author: Rajini Sivaram <rajinisivaram@googlemail.com>

Reviewers: Ismael Juma <ismael@juma.me.uk>

Closes #3766 from rajinisivaram/MINOR-sasl

(cherry picked from commit 47c2753)
Signed-off-by: Rajini Sivaram <rajinisivaram@googlemail.com>
  • Loading branch information
rajinisivaram committed Aug 31, 2017
1 parent 0cffb62 commit 9f34686
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class PlainSaslServer implements SaslServer {
private final JaasContext jaasContext;

private boolean complete;
private String authorizationID;
private String authorizationId;

public PlainSaslServer(JaasContext jaasContext) {
this.jaasContext = jaasContext;
Expand Down Expand Up @@ -79,7 +79,7 @@ public byte[] evaluateResponse(byte[] response) throws SaslException {
}
if (tokens.length != 3)
throw new SaslException("Invalid SASL/PLAIN response: expected 3 tokens, got " + tokens.length);
authorizationID = tokens[0];
String authorizationIdFromClient = tokens[0];
String username = tokens[1];
String password = tokens[2];

Expand All @@ -89,14 +89,18 @@ public byte[] evaluateResponse(byte[] response) throws SaslException {
if (password.isEmpty()) {
throw new SaslException("Authentication failed: password not specified");
}
if (authorizationID.isEmpty())
authorizationID = username;

String expectedPassword = jaasContext.configEntryOption(JAAS_USER_PREFIX + username,
PlainLoginModule.class.getName());
if (!password.equals(expectedPassword)) {
throw new SaslException("Authentication failed: Invalid username or password");
}

if (!authorizationIdFromClient.isEmpty() && !authorizationIdFromClient.equals(username))
throw new SaslException("Authentication failed: Client requested an authorization id that is different from username");

this.authorizationId = username;

complete = true;
return new byte[0];
}
Expand All @@ -105,7 +109,7 @@ public byte[] evaluateResponse(byte[] response) throws SaslException {
public String getAuthorizationID() {
if (!complete)
throw new IllegalStateException("Authentication exchange has not completed");
return authorizationID;
return authorizationId;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ enum State {
private String username;
private ClientFirstMessage clientFirstMessage;
private ServerFirstMessage serverFirstMessage;
private String serverNonce;
private ScramCredential scramCredential;

public ScramSaslServer(ScramMechanism mechanism, Map<String, ?> props, CallbackHandler callbackHandler) throws NoSuchAlgorithmException {
Expand All @@ -80,7 +79,7 @@ public byte[] evaluateResponse(byte[] response) throws SaslException {
switch (state) {
case RECEIVE_CLIENT_FIRST_MESSAGE:
this.clientFirstMessage = new ClientFirstMessage(response);
serverNonce = formatter.secureRandomString();
String serverNonce = formatter.secureRandomString();
try {
String saslName = clientFirstMessage.saslName();
this.username = formatter.username(saslName);
Expand All @@ -90,6 +89,10 @@ public byte[] evaluateResponse(byte[] response) throws SaslException {
this.scramCredential = credentialCallback.scramCredential();
if (scramCredential == null)
throw new SaslException("Authentication failed: Invalid user credentials");
String authorizationIdFromClient = clientFirstMessage.authorizationId();
if (!authorizationIdFromClient.isEmpty() && !authorizationIdFromClient.equals(username))
throw new SaslException("Authentication failed: Client requested an authorization id that is different from username");

if (scramCredential.iterations() < mechanism.minIterations())
throw new SaslException("Iterations " + scramCredential.iterations() + " is less than the minimum " + mechanism.minIterations() + " for " + mechanism);
this.serverFirstMessage = new ServerFirstMessage(clientFirstMessage.nonce(),
Expand All @@ -109,6 +112,7 @@ public byte[] evaluateResponse(byte[] response) throws SaslException {
byte[] serverKey = scramCredential.serverKey();
byte[] serverSignature = formatter.serverSignature(serverKey, clientFirstMessage, serverFirstMessage, clientFinalMessage);
ServerFinalMessage serverFinalMessage = new ServerFinalMessage(null, serverSignature);
clearCredentials();
setState(State.COMPLETE);
return serverFinalMessage.toBytes();
} catch (InvalidKeyException e) {
Expand All @@ -119,6 +123,7 @@ public byte[] evaluateResponse(byte[] response) throws SaslException {
throw new IllegalSaslStateException("Unexpected challenge in Sasl server state " + state);
}
} catch (SaslException e) {
clearCredentials();
setState(State.FAILED);
throw e;
}
Expand All @@ -128,8 +133,7 @@ public byte[] evaluateResponse(byte[] response) throws SaslException {
public String getAuthorizationID() {
if (!isComplete())
throw new IllegalStateException("Authentication exchange has not completed");
String authzId = clientFirstMessage.authorizationId();
return authzId == null || authzId.length() == 0 ? username : authzId;
return username;
}

@Override
Expand Down Expand Up @@ -184,6 +188,12 @@ private void verifyClientProof(ClientFinalMessage clientFinalMessage) throws Sas
}
}

private void clearCredentials() {
scramCredential = null;
clientFirstMessage = null;
serverFirstMessage = null;
}

public static class ScramSaslServerFactory implements SaslServerFactory {

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.kafka.common.security.plain;

import org.junit.Before;
import org.junit.Test;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import javax.security.sasl.SaslException;

import static org.junit.Assert.assertEquals;

import org.apache.kafka.common.security.JaasContext;
import org.apache.kafka.common.security.authenticator.TestJaasConfig;

public class PlainSaslServerTest {

private static final String USER_A = "userA";
private static final String PASSWORD_A = "passwordA";
private static final String USER_B = "userB";
private static final String PASSWORD_B = "passwordB";

private PlainSaslServer saslServer;

@Before
public void setUp() throws Exception {
TestJaasConfig jaasConfig = new TestJaasConfig();
Map<String, Object> options = new HashMap<>();
options.put("user_" + USER_A, PASSWORD_A);
options.put("user_" + USER_B, PASSWORD_B);
jaasConfig.addEntry("jaasContext", PlainLoginModule.class.getName(), options);
JaasContext jaasContext = new JaasContext("jaasContext", JaasContext.Type.SERVER, jaasConfig);
saslServer = new PlainSaslServer(jaasContext);
}

@Test
public void noAuthorizationIdSpecified() throws Exception {
byte[] nextChallenge = saslServer.evaluateResponse(saslMessage("", USER_A, PASSWORD_A));
assertEquals(0, nextChallenge.length);
}

@Test
public void authorizatonIdEqualsAuthenticationId() throws Exception {
byte[] nextChallenge = saslServer.evaluateResponse(saslMessage(USER_A, USER_A, PASSWORD_A));
assertEquals(0, nextChallenge.length);
}

@Test(expected = SaslException.class)
public void authorizatonIdNotEqualsAuthenticationId() throws Exception {
saslServer.evaluateResponse(saslMessage(USER_B, USER_A, PASSWORD_A));
}

private byte[] saslMessage(String authorizationId, String userName, String password) {
String nul = "\u0000";
String message = String.format("%s%s%s%s%s", authorizationId, nul, userName, nul, password);
return message.getBytes(StandardCharsets.UTF_8);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.kafka.common.security.scram;

import org.junit.Before;
import org.junit.Test;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;

import javax.security.sasl.SaslException;

import static org.junit.Assert.assertTrue;

import org.apache.kafka.common.security.authenticator.CredentialCache;

public class ScramSaslServerTest {

private static final String USER_A = "userA";
private static final String USER_B = "userB";

private ScramMechanism mechanism;
private ScramFormatter formatter;
private ScramSaslServer saslServer;

@Before
public void setUp() throws Exception {
mechanism = ScramMechanism.SCRAM_SHA_256;
formatter = new ScramFormatter(mechanism);
CredentialCache.Cache<ScramCredential> credentialCache = new CredentialCache().createCache(mechanism.mechanismName(), ScramCredential.class);
credentialCache.put(USER_A, formatter.generateCredential("passwordA", 4096));
credentialCache.put(USER_B, formatter.generateCredential("passwordB", 4096));
ScramServerCallbackHandler callbackHandler = new ScramServerCallbackHandler(credentialCache);
saslServer = new ScramSaslServer(mechanism, new HashMap<String, Object>(), callbackHandler);
}

@Test
public void noAuthorizationIdSpecified() throws Exception {
byte[] nextChallenge = saslServer.evaluateResponse(clientFirstMessage(USER_A, null));
assertTrue("Next challenge is empty", nextChallenge.length > 0);
}

@Test
public void authorizatonIdEqualsAuthenticationId() throws Exception {
byte[] nextChallenge = saslServer.evaluateResponse(clientFirstMessage(USER_A, USER_A));
assertTrue("Next challenge is empty", nextChallenge.length > 0);
}

@Test(expected = SaslException.class)
public void authorizatonIdNotEqualsAuthenticationId() throws Exception {
saslServer.evaluateResponse(clientFirstMessage(USER_A, USER_B));
}

private byte[] clientFirstMessage(String userName, String authorizationId) {
String nonce = formatter.secureRandomString();
String authorizationField = authorizationId != null ? "a=" + authorizationId : "";
String firstMessage = String.format("n,%s,n=%s,r=%s", authorizationField, userName, nonce);
return firstMessage.getBytes(StandardCharsets.UTF_8);
}
}

0 comments on commit 9f34686

Please sign in to comment.