Skip to content

Commit fae1adc

Browse files
authored
ISSUE-610: Provide JAAS config dynamically via config #610 (#611)
* ISSUE-610: Provide JAAS config dynamically via config #610 * Adding licence for new source files.
1 parent 21f73ec commit fae1adc

File tree

8 files changed

+494
-23
lines changed

8 files changed

+494
-23
lines changed

common-auth/src/main/java/com/hortonworks/registries/auth/AbstractLogin.java

+18-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import javax.security.auth.callback.NameCallback;
2525
import javax.security.auth.callback.PasswordCallback;
2626
import javax.security.auth.callback.UnsupportedCallbackException;
27+
import javax.security.auth.login.Configuration;
2728
import javax.security.auth.login.LoginContext;
2829
import javax.security.auth.login.LoginException;
2930
import javax.security.sasl.RealmCallback;
@@ -39,19 +40,33 @@ public abstract class AbstractLogin implements Login {
3940
protected String loginContextName;
4041
protected LoginContext loginContext;
4142
protected static final String JAAS_CONFIG_SYSTEM_PROPERTY = "java.security.auth.login.config";
43+
protected Configuration jaasConfiguration;
4244

4345
@Override
4446
public void configure(Map<String, ?> configs, String loginContextName) {
4547
this.loginContextName = loginContextName;
4648
}
4749

50+
/**
51+
* Configures this login instance with a dynamic configuration.
52+
*/
53+
public void configure(Map<String, ?> configs, String loginContextName, Configuration jaasConfiguration) {
54+
this.loginContextName = loginContextName;
55+
this.jaasConfiguration = jaasConfiguration;
56+
}
57+
4858
@Override
4959
public LoginContext login() throws LoginException {
5060
String jaasConfigFile = System.getProperty(JAAS_CONFIG_SYSTEM_PROPERTY);
51-
if (jaasConfigFile == null) {
52-
log.error("System property " + JAAS_CONFIG_SYSTEM_PROPERTY + " for jaas config file is not set, using default JAAS configuration.");
61+
if (jaasConfiguration != null) {
62+
loginContext = new LoginContext(loginContextName, null, new LoginCallbackHandler(), jaasConfiguration);
63+
} else {
64+
if (jaasConfigFile == null) {
65+
log.error("System property " + JAAS_CONFIG_SYSTEM_PROPERTY + " for jaas config file is not set, using default JAAS configuration.");
66+
}
67+
log.debug("Defaulting to static JAAS Config for {}", loginContextName);
68+
loginContext = new LoginContext(loginContextName, new LoginCallbackHandler());
5369
}
54-
loginContext = new LoginContext(loginContextName, new LoginCallbackHandler());
5570
loginContext.login();
5671
log.info("Successfully logged in.");
5772
return loginContext;

common-auth/src/main/java/com/hortonworks/registries/auth/KerberosLogin.java

+26-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,30 @@ public void configure(Map<String, ?> configs, final String loginContextName) {
9494
}
9595
}
9696

97+
/**
98+
* Method to configure this instance with specific properties
99+
* @param loginContextName
100+
* name of section in JAAS file that will be used to login.
101+
* Passed as first param to javax.security.auth.login.LoginContext().
102+
* @param configs configure Login with the given key-value pairs.
103+
*/
104+
@Override
105+
public void configure(Map<String, ?> configs, final String loginContextName, Configuration jaasConfig) {
106+
super.configure(configs, loginContextName, jaasConfig);
107+
if (configs.get(TICKET_RENEW_WINDOW_FACTOR) != null) {
108+
this.ticketRenewWindowFactor = Double.parseDouble((String) configs.get(TICKET_RENEW_WINDOW_FACTOR));
109+
}
110+
if (configs.get(TICKET_RENEW_JITTER) != null) {
111+
this.ticketRenewJitter = Double.parseDouble((String) configs.get(TICKET_RENEW_JITTER));
112+
}
113+
if (configs.get(MIN_TIME_BEFORE_RELOGIN) != null) {
114+
this.minTimeBeforeRelogin = Long.parseLong((String) configs.get(MIN_TIME_BEFORE_RELOGIN));
115+
}
116+
if (configs.get(KINIT_CMD) != null) {
117+
this.kinitCmd = (String) configs.get(KINIT_CMD);
118+
}
119+
}
120+
97121
public KerberosLogin() {
98122
this.tgtRenewalLock = new ReentrantReadWriteLock(true);
99123
this.tgtRenewalTimeoutMS = DEFAULT_TGT_RENEWAL_TIMEOUT_MS;
@@ -121,7 +145,8 @@ public LoginContext login() throws LoginException {
121145
return loginContext;
122146
}
123147
log.info("It is a Kerberos ticket");
124-
AppConfigurationEntry[] entries = Configuration.getConfiguration().getAppConfigurationEntry(loginContextName);
148+
AppConfigurationEntry[] entries = (jaasConfiguration != null) ? jaasConfiguration.getAppConfigurationEntry(loginContextName) :
149+
Configuration.getConfiguration().getAppConfigurationEntry(loginContextName);
125150
if (entries.length == 0) {
126151
isUsingTicketCache = false;
127152
principal = null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Copyright 2016-2019 Cloudera, Inc.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* <p>
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
* <p>
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
**/
15+
package com.hortonworks.registries.auth.util;
16+
17+
import java.io.IOException;
18+
import java.io.StreamTokenizer;
19+
import java.io.StringReader;
20+
import java.util.ArrayList;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Locale;
24+
import java.util.Map;
25+
26+
import javax.security.auth.login.AppConfigurationEntry;
27+
import javax.security.auth.login.Configuration;
28+
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
29+
30+
31+
/**
32+
* Class representing the Dynamic JAAS configuration.
33+
* JAAS configuration can be provided to an application using a static file provided by java.security.auth.login.config property.
34+
* Alternately, the JAAS Configuration can be constructed dynamically using this class which parses the configuration string.
35+
* <p/>
36+
* JAAS configuration file format is described <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/#ConfigFile.html">here</a>.
37+
* The format of the property value is:
38+
* <pre>
39+
* {@code
40+
* <loginModuleClass> <controlFlag> (<optionName>=<optionValue>)*;
41+
* }
42+
* </pre>
43+
*/
44+
public class JaasConfiguration extends Configuration {
45+
46+
private final String loginContextName;
47+
private final List<AppConfigurationEntry> configEntries;
48+
49+
public JaasConfiguration(String loginContextName, String jaasConfigParams) {
50+
StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(jaasConfigParams));
51+
tokenizer.slashSlashComments(true);
52+
tokenizer.slashStarComments(true);
53+
tokenizer.wordChars('-', '-');
54+
tokenizer.wordChars('_', '_');
55+
tokenizer.wordChars('$', '$');
56+
57+
try {
58+
configEntries = new ArrayList<>();
59+
while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
60+
configEntries.add(parseAppConfigurationEntry(tokenizer));
61+
}
62+
if (configEntries.isEmpty())
63+
throw new IllegalArgumentException("Login module not specified in JAAS config");
64+
65+
this.loginContextName = loginContextName;
66+
67+
} catch (IOException e) {
68+
throw new IllegalArgumentException("Unexpected exception while parsing JAAS config", e);
69+
}
70+
}
71+
72+
@Override
73+
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
74+
if (this.loginContextName.equals(name))
75+
return configEntries.toArray(new AppConfigurationEntry[0]);
76+
else
77+
return null;
78+
}
79+
80+
private LoginModuleControlFlag loginModuleControlFlag(String flag) {
81+
LoginModuleControlFlag controlFlag;
82+
switch (flag.toUpperCase(Locale.ROOT)) {
83+
case "REQUIRED":
84+
controlFlag = LoginModuleControlFlag.REQUIRED;
85+
break;
86+
case "REQUISITE":
87+
controlFlag = LoginModuleControlFlag.REQUISITE;
88+
break;
89+
case "SUFFICIENT":
90+
controlFlag = LoginModuleControlFlag.SUFFICIENT;
91+
break;
92+
case "OPTIONAL":
93+
controlFlag = LoginModuleControlFlag.OPTIONAL;
94+
break;
95+
default:
96+
throw new IllegalArgumentException("Invalid login module control flag '" + flag + "' in JAAS config");
97+
}
98+
return controlFlag;
99+
}
100+
101+
private AppConfigurationEntry parseAppConfigurationEntry(StreamTokenizer tokenizer) throws IOException {
102+
String loginModule = tokenizer.sval;
103+
if (tokenizer.nextToken() == StreamTokenizer.TT_EOF)
104+
throw new IllegalArgumentException("Login module control flag not specified in JAAS config");
105+
LoginModuleControlFlag controlFlag = loginModuleControlFlag(tokenizer.sval);
106+
Map<String, String> options = new HashMap<>();
107+
while (tokenizer.nextToken() != StreamTokenizer.TT_EOF && tokenizer.ttype != ';') {
108+
String key = tokenizer.sval;
109+
if (tokenizer.nextToken() != '=' || tokenizer.nextToken() == StreamTokenizer.TT_EOF || tokenizer.sval == null)
110+
throw new IllegalArgumentException("Value not specified for key '" + key + "' in JAAS config");
111+
String value = tokenizer.sval;
112+
options.put(key, value);
113+
}
114+
if (tokenizer.ttype != ';')
115+
throw new IllegalArgumentException("JAAS config entry not terminated by semi-colon");
116+
return new AppConfigurationEntry(loginModule, controlFlag, options);
117+
}
118+
}
119+

common-auth/src/test/java/com/hortonworks/registries/auth/KerberosTestUtils.java

+6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ public static String getClientPrincipal() {
4747
return "client@EXAMPLE.COM";
4848
}
4949

50+
public static String getJaasConfigForClientPrincipal() {
51+
System.out.println(keytabFile);
52+
return "com.sun.security.auth.module.Krb5LoginModule required doNotPrompt=true useTicketCache=false principal=\"client@EXAMPLE.COM\" " +
53+
"useKeyTab=true keyTab=\"" + keytabFile +"\" debug=true;";
54+
}
55+
5056
public static String getClientPrincipal1() {
5157
return "client1@EXAMPLE.COM";
5258
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License. See accompanying LICENSE file.
13+
*/
14+
package com.hortonworks.registries.auth;
15+
16+
import com.hortonworks.registries.auth.client.KerberosAuthenticator;
17+
import com.hortonworks.registries.auth.server.AuthenticationToken;
18+
import com.hortonworks.registries.auth.server.KerberosAuthenticationHandler;
19+
import com.hortonworks.registries.auth.util.JaasConfiguration;
20+
import com.hortonworks.registries.auth.util.KerberosUtil;
21+
import org.apache.commons.codec.binary.Base64;
22+
import org.apache.hadoop.minikdc.KerberosSecurityTestcase;
23+
import org.ietf.jgss.GSSContext;
24+
import org.ietf.jgss.GSSManager;
25+
import org.ietf.jgss.GSSName;
26+
import org.ietf.jgss.Oid;
27+
import org.junit.After;
28+
import org.junit.Assert;
29+
import org.junit.Before;
30+
import org.junit.Test;
31+
import org.mockito.Mockito;
32+
33+
import javax.security.auth.Subject;
34+
import javax.servlet.http.HttpServletRequest;
35+
import javax.servlet.http.HttpServletResponse;
36+
import java.io.File;
37+
import java.security.PrivilegedExceptionAction;
38+
import java.util.HashMap;
39+
import java.util.Properties;
40+
import java.util.concurrent.Callable;
41+
42+
public class TestKerberosLogin extends KerberosSecurityTestcase {
43+
44+
protected KerberosAuthenticationHandler handler;
45+
46+
protected KerberosAuthenticationHandler getNewAuthenticationHandler() {
47+
return new KerberosAuthenticationHandler();
48+
}
49+
50+
protected Properties getDefaultProperties() {
51+
Properties props = new Properties();
52+
props.setProperty(KerberosAuthenticationHandler.PRINCIPAL,
53+
KerberosTestUtils.getServerPrincipal());
54+
props.setProperty(KerberosAuthenticationHandler.KEYTAB,
55+
KerberosTestUtils.getKeytabFile());
56+
props.setProperty(KerberosAuthenticationHandler.NAME_RULES,
57+
"RULE:[1:$1@$0](.*@" + KerberosTestUtils.getRealm() + ")s/@.*//\n");
58+
return props;
59+
}
60+
61+
@Before
62+
public void setup() throws Exception {
63+
// create keytab
64+
File keytabFile = new File(KerberosTestUtils.getKeytabFile());
65+
String clientPrincipal = KerberosTestUtils.getClientPrincipal();
66+
String serverPrincipal = KerberosTestUtils.getServerPrincipal();
67+
serverPrincipal = serverPrincipal.substring(0, serverPrincipal.lastIndexOf("@"));
68+
clientPrincipal = clientPrincipal.substring(0, clientPrincipal.lastIndexOf("@"));
69+
getKdc().createPrincipal(keytabFile, serverPrincipal, clientPrincipal);
70+
// handler
71+
handler = getNewAuthenticationHandler();
72+
Properties props = getDefaultProperties();
73+
try {
74+
handler.init(props);
75+
} catch (Exception ex) {
76+
handler = null;
77+
throw ex;
78+
}
79+
}
80+
81+
@Test(timeout = 60000)
82+
public void testRequestWithKerberosAuthorization() throws Exception {
83+
KerberosLogin login = new KerberosLogin();
84+
System.out.println(KerberosTestUtils.getJaasConfigForClientPrincipal());
85+
login.configure(new HashMap<>(), "RegistryClient",
86+
new JaasConfiguration("RegistryClient", KerberosTestUtils.getJaasConfigForClientPrincipal()));
87+
login.login();
88+
89+
String token = Subject.doAs(login.loginContext.getSubject(), (PrivilegedExceptionAction<String>) () -> {
90+
GSSManager gssManager = GSSManager.getInstance();
91+
GSSContext gssContext = null;
92+
try {
93+
String servicePrincipal = KerberosTestUtils.getServerPrincipal();
94+
Oid oid = KerberosUtil.getOidInstance("NT_GSS_KRB5_PRINCIPAL");
95+
GSSName serviceName = gssManager.createName(servicePrincipal,
96+
oid);
97+
oid = KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID");
98+
gssContext = gssManager.createContext(serviceName, oid, null,
99+
GSSContext.DEFAULT_LIFETIME);
100+
gssContext.requestCredDeleg(true);
101+
gssContext.requestMutualAuth(true);
102+
103+
byte[] inToken = new byte[0];
104+
byte[] outToken = gssContext.initSecContext(inToken, 0, inToken.length);
105+
Base64 base64 = new Base64(0);
106+
return base64.encodeToString(outToken);
107+
108+
} finally {
109+
if (gssContext != null) {
110+
gssContext.dispose();
111+
}
112+
}
113+
}
114+
);
115+
116+
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
117+
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
118+
119+
Mockito.when(request.getHeader(KerberosAuthenticator.AUTHORIZATION))
120+
.thenReturn(KerberosAuthenticator.NEGOTIATE + " " + token);
121+
Mockito.when(request.getServerName()).thenReturn("localhost");
122+
Mockito.when(request.getMethod()).thenReturn("GET");
123+
124+
AuthenticationToken authToken = handler.authenticate(request, response);
125+
126+
if (authToken != null) {
127+
Mockito.verify(response).setHeader(Mockito.eq(KerberosAuthenticator.WWW_AUTHENTICATE),
128+
Mockito.matches(KerberosAuthenticator.NEGOTIATE + " .*"));
129+
Mockito.verify(response).setStatus(HttpServletResponse.SC_OK);
130+
131+
Assert.assertEquals(KerberosTestUtils.getClientPrincipal(), authToken.getName());
132+
Assert.assertTrue(KerberosTestUtils.getClientPrincipal().startsWith(authToken.getUserName()));
133+
} else {
134+
Mockito.verify(response).setHeader(Mockito.eq(KerberosAuthenticator.WWW_AUTHENTICATE),
135+
Mockito.matches(KerberosAuthenticator.NEGOTIATE + " .*"));
136+
Mockito.verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
137+
}
138+
}
139+
140+
@After
141+
public void tearDown() throws Exception {
142+
if (handler != null) {
143+
handler.destroy();
144+
handler = null;
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)