Skip to content

Commit bbade38

Browse files
committed
plain sasl server.
1 parent 4fe619e commit bbade38

File tree

9 files changed

+353
-2
lines changed

9 files changed

+353
-2
lines changed

fluss-auth/fluss-auth-sasl/src/main/java/com/alibaba/fluss/security/auth/sasl/JaasContext.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,19 @@ private static JaasContext defaultContext(
207207

208208
return new JaasContext(contextName, contextType, jaasConfig, null);
209209
}
210+
211+
/**
212+
* Returns the configuration option for <code>key</code> from this context.
213+
* If login module name is specified, return option value only from that module.
214+
*/
215+
public static String configEntryOption(List<AppConfigurationEntry> configurationEntries, String key, String loginModuleName) {
216+
for (AppConfigurationEntry entry : configurationEntries) {
217+
if (loginModuleName != null && !loginModuleName.equals(entry.getLoginModuleName()))
218+
continue;
219+
Object val = entry.getOptions().get(key);
220+
if (val != null)
221+
return (String) val;
222+
}
223+
return null;
224+
}
210225
}

fluss-auth/fluss-auth-sasl/src/main/java/com/alibaba/fluss/security/auth/sasl/SaslServerAuthenticator.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.alibaba.fluss.security.acl.FlussPrincipal;
77
import com.alibaba.fluss.security.auth.ServerAuthenticator;
88

9+
import com.alibaba.fluss.security.auth.sasl.plain.PlainServerCallbackHandler;
910
import org.slf4j.Logger;
1011
import org.slf4j.LoggerFactory;
1112

@@ -64,6 +65,7 @@ public void initialize(AuthenticateContext context) {
6465
JaasContext.loadServerContext(listenerName, mechanism, dynamicJaasConfig);
6566
try {
6667
LoginManager loginManager = LoginManager.acquireLoginManager(jaasContext, mechanism);
68+
// todo: 抽象方法
6769
createSaslServer(mechanism, loginManager.subject(), address);
6870
} catch (Exception e) {
6971
throw new RuntimeException(e);
@@ -98,8 +100,13 @@ public FlussPrincipal createPrincipal() {
98100
private void createSaslServer(String mechanism, Subject subject, String hostName)
99101
throws IOException {
100102
this.saslMechanism = mechanism;
101-
final AuthenticateCallbackHandler callbackHandler = callbackHandlers.get(mechanism);
102103
// todo: check一下 digest login module, 当前支持
104+
AuthenticateCallbackHandler callbackHandler;
105+
if(mechanism.equals("PLAIN")){
106+
callbackHandler = new PlainServerCallbackHandler();
107+
} else {
108+
throw new IllegalArgumentException("Unsupported mechanism: " + mechanism);
109+
}
103110

104111
try {
105112
saslServer =
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.alibaba.fluss.security.auth.sasl.plain;
2+
3+
import javax.security.auth.callback.Callback;
4+
5+
/*
6+
* Authentication callback for SASL/PLAIN authentication. Callback handler must
7+
* set authenticated flag to true if the client provided password in the callback
8+
* matches the expected password.
9+
*/
10+
// todo: 这个主要将token的username, password传递给PlainServerCallbackHandler来校验
11+
public class PlainAuthenticateCallback implements Callback {
12+
13+
private final char[] password;
14+
private boolean authenticated;
15+
16+
/**
17+
* Creates a callback with the password provided by the client
18+
* @param password The password provided by the client during SASL/PLAIN authentication
19+
*/
20+
public PlainAuthenticateCallback(char[] password) {
21+
this.password = password;
22+
}
23+
24+
/**
25+
* Returns the password provided by the client during SASL/PLAIN authentication
26+
*/
27+
public char[] password() {
28+
return password;
29+
}
30+
31+
/**
32+
* Returns true if client password matches expected password, false otherwise.
33+
* This state is set the server-side callback handler.
34+
*/
35+
public boolean authenticated() {
36+
return this.authenticated;
37+
}
38+
39+
/**
40+
* Sets the authenticated state. This is set by the server-side callback handler
41+
* by matching the client provided password with the expected password.
42+
*
43+
* @param authenticated true indicates successful authentication
44+
*/
45+
public void authenticated(boolean authenticated) {
46+
this.authenticated = authenticated;
47+
}
48+
}

fluss-auth/fluss-auth-sasl/src/main/java/com/alibaba/fluss/security/auth/sasl/PlainLoginModule.java renamed to fluss-auth/fluss-auth-sasl/src/main/java/com/alibaba/fluss/security/auth/sasl/plain/PlainLoginModule.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.alibaba.fluss.security.auth.sasl;
1+
package com.alibaba.fluss.security.auth.sasl.plain;
22

33
import javax.security.auth.Subject;
44
import javax.security.auth.callback.CallbackHandler;
@@ -11,6 +11,11 @@ public class PlainLoginModule implements LoginModule {
1111
private static final String USERNAME_CONFIG = "username";
1212
private static final String PASSWORD_CONFIG = "password";
1313

14+
static {
15+
// Register sasl server provider.
16+
PlainSaslServerProvider.initialize();
17+
}
18+
1419
@Override
1520
public void initialize(
1621
Subject subject,
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package com.alibaba.fluss.security.auth.sasl.plain;
2+
3+
import com.alibaba.fluss.exception.AuthenticationException;
4+
5+
import javax.security.auth.callback.Callback;
6+
import javax.security.auth.callback.CallbackHandler;
7+
import javax.security.auth.callback.NameCallback;
8+
import javax.security.sasl.Sasl;
9+
import javax.security.sasl.SaslException;
10+
import javax.security.sasl.SaslServer;
11+
import javax.security.sasl.SaslServerFactory;
12+
import java.nio.charset.StandardCharsets;
13+
import java.util.ArrayList;
14+
import java.util.Arrays;
15+
import java.util.List;
16+
import java.util.Map;
17+
18+
public class PlainSaslServer implements SaslServer {
19+
20+
public static final String PLAIN_MECHANISM = "PLAIN";
21+
22+
private final CallbackHandler callbackHandler;
23+
private boolean complete;
24+
private String authorizationId;
25+
26+
public PlainSaslServer(CallbackHandler callbackHandler) {
27+
this.callbackHandler = callbackHandler;
28+
}
29+
30+
31+
@Override
32+
public byte[] evaluateResponse(byte[] responseBytes) throws AuthenticationException {
33+
/*
34+
* Message format (from https://tools.ietf.org/html/rfc4616):
35+
*
36+
* message = [authzid] UTF8NUL authcid UTF8NUL passwd
37+
* authcid = 1*SAFE ; MUST accept up to 255 octets
38+
* authzid = 1*SAFE ; MUST accept up to 255 octets
39+
* passwd = 1*SAFE ; MUST accept up to 255 octets
40+
* UTF8NUL = %x00 ; UTF-8 encoded NUL character
41+
*
42+
* SAFE = UTF1 / UTF2 / UTF3 / UTF4
43+
* ;; any UTF-8 encoded Unicode character except NUL
44+
*/
45+
46+
String response = new String(responseBytes, StandardCharsets.UTF_8);
47+
List<String> tokens = extractTokens(response);
48+
String authorizationIdFromClient = tokens.get(0);
49+
String username = tokens.get(1);
50+
String password = tokens.get(2);
51+
52+
if (username.isEmpty()) {
53+
throw new AuthenticationException("Authentication failed: username not specified");
54+
}
55+
if (password.isEmpty()) {
56+
throw new AuthenticationException("Authentication failed: password not specified");
57+
}
58+
59+
NameCallback nameCallback = new NameCallback("username", username);
60+
PlainAuthenticateCallback authenticateCallback = new PlainAuthenticateCallback(password.toCharArray());
61+
try {
62+
callbackHandler.handle(new Callback[]{nameCallback, authenticateCallback});
63+
} catch (Throwable e) {
64+
throw new AuthenticationException("Authentication failed: credentials for user could not be verified", e);
65+
}
66+
if (!authenticateCallback.authenticated())
67+
throw new AuthenticationException("Authentication failed: Invalid username or password");
68+
if (!authorizationIdFromClient.isEmpty() && !authorizationIdFromClient.equals(username))
69+
throw new AuthenticationException("Authentication failed: Client requested an authorization id that is different from username");
70+
71+
this.authorizationId = username;
72+
73+
complete = true;
74+
return new byte[0];
75+
}
76+
77+
private List<String> extractTokens(String string) {
78+
List<String> tokens = new ArrayList<>();
79+
int startIndex = 0;
80+
for (int i = 0; i < 4; ++i) {
81+
int endIndex = string.indexOf("\u0000", startIndex);
82+
if (endIndex == -1) {
83+
tokens.add(string.substring(startIndex));
84+
break;
85+
}
86+
tokens.add(string.substring(startIndex, endIndex));
87+
startIndex = endIndex + 1;
88+
}
89+
90+
if (tokens.size() != 3)
91+
throw new AuthenticationException("Invalid SASL/PLAIN response: expected 3 tokens, got " +
92+
tokens.size());
93+
94+
return tokens;
95+
}
96+
97+
@Override
98+
public String getAuthorizationID() {
99+
if (!complete)
100+
throw new IllegalStateException("Authentication exchange has not completed");
101+
return authorizationId;
102+
}
103+
104+
@Override
105+
public String getMechanismName() {
106+
return PLAIN_MECHANISM;
107+
}
108+
109+
@Override
110+
public Object getNegotiatedProperty(String propName) {
111+
if (!complete)
112+
throw new IllegalStateException("Authentication exchange has not completed");
113+
return null;
114+
}
115+
116+
@Override
117+
public boolean isComplete() {
118+
return complete;
119+
}
120+
121+
@Override
122+
public byte[] unwrap(byte[] incoming, int offset, int len) {
123+
if (!complete)
124+
throw new IllegalStateException("Authentication exchange has not completed");
125+
return Arrays.copyOfRange(incoming, offset, offset + len);
126+
}
127+
128+
@Override
129+
public byte[] wrap(byte[] outgoing, int offset, int len) {
130+
if (!complete)
131+
throw new IllegalStateException("Authentication exchange has not completed");
132+
return Arrays.copyOfRange(outgoing, offset, offset + len);
133+
}
134+
135+
@Override
136+
public void dispose() {
137+
}
138+
139+
public static class PlainSaslServerFactory implements SaslServerFactory {
140+
141+
@Override
142+
public SaslServer createSaslServer(String mechanism, String protocol, String serverName, Map<String, ?> props, CallbackHandler cbh)
143+
throws SaslException {
144+
145+
if (!PLAIN_MECHANISM.equals(mechanism))
146+
throw new SaslException(String.format("Mechanism \'%s\' is not supported. Only PLAIN is supported.", mechanism));
147+
148+
return new PlainSaslServer(cbh);
149+
}
150+
151+
@Override
152+
public String[] getMechanismNames(Map<String, ?> props) {
153+
if (props == null) return new String[]{PLAIN_MECHANISM};
154+
String noPlainText = (String) props.get(Sasl.POLICY_NOPLAINTEXT);
155+
if ("true".equals(noPlainText))
156+
return new String[]{};
157+
else
158+
return new String[]{PLAIN_MECHANISM};
159+
}
160+
}
161+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.alibaba.fluss.security.auth.sasl.plain;
2+
3+
import java.security.Provider;
4+
import java.security.Security;
5+
6+
public class PlainSaslServerProvider extends Provider {
7+
8+
private static final long serialVersionUID = 1L;
9+
10+
@SuppressWarnings("this-escape")
11+
protected PlainSaslServerProvider() {
12+
super("Simple SASL/PLAIN Server Provider", 1.0, "Simple SASL/PLAIN Server Provider for Kafka");
13+
put("SaslServerFactory." + PlainSaslServer.PLAIN_MECHANISM, PlainSaslServer.PlainSaslServerFactory.class.getName());
14+
}
15+
16+
public static void initialize() {
17+
Security.addProvider(new PlainSaslServerProvider());
18+
}
19+
}
20+
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.alibaba.fluss.security.auth.sasl.plain;
2+
3+
import com.alibaba.fluss.security.auth.sasl.AuthenticateCallbackHandler;
4+
import com.alibaba.fluss.security.auth.sasl.JaasContext;
5+
import com.alibaba.fluss.utils.ArrayUtils;
6+
7+
import javax.security.auth.callback.Callback;
8+
import javax.security.auth.callback.NameCallback;
9+
import javax.security.auth.callback.UnsupportedCallbackException;
10+
import javax.security.auth.login.AppConfigurationEntry;
11+
import java.io.IOException;
12+
import java.util.List;
13+
14+
// todo: PlainServerCallbackHandler作为连接saslserver的bride, 用于比较用户传入的token和server端配置的账号密码
15+
public class PlainServerCallbackHandler implements AuthenticateCallbackHandler {
16+
private static final String JAAS_USER_PREFIX = "user_";
17+
private List<AppConfigurationEntry> jaasConfigEntries;
18+
19+
@Override
20+
public void configure(String saslMechanism, List<AppConfigurationEntry> jaasConfigEntries){
21+
this.jaasConfigEntries = jaasConfigEntries;
22+
}
23+
24+
@Override
25+
public void close() {
26+
// todo
27+
}
28+
29+
@Override
30+
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
31+
String username = null;
32+
for (Callback callback: callbacks) {
33+
if (callback instanceof NameCallback)
34+
username = ((NameCallback) callback).getDefaultName();
35+
else if (callback instanceof PlainAuthenticateCallback) {
36+
PlainAuthenticateCallback plainCallback = (PlainAuthenticateCallback) callback;
37+
boolean authenticated = authenticate(username, plainCallback.password());
38+
plainCallback.authenticated(authenticated);
39+
} else
40+
throw new UnsupportedCallbackException(callback);
41+
}
42+
}
43+
44+
protected boolean authenticate(String username, char[] password) throws IOException {
45+
if (username == null)
46+
return false;
47+
else {
48+
String expectedPassword = JaasContext.configEntryOption(jaasConfigEntries,
49+
JAAS_USER_PREFIX + username,
50+
PlainLoginModule.class.getName());
51+
return expectedPassword != null && ArrayUtils.isEqualConstantTime(password, expectedPassword.toCharArray());
52+
}
53+
}
54+
55+
}
56+
57+

fluss-auth/fluss-auth-sasl/src/test/java/com/alibaba/fluss/security/auth/sasl/TestJaasConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.alibaba.fluss.security.auth.sasl;
1818

19+
import com.alibaba.fluss.security.auth.sasl.plain.PlainLoginModule;
20+
1921
import javax.security.auth.login.AppConfigurationEntry;
2022
import javax.security.auth.login.Configuration;
2123

0 commit comments

Comments
 (0)