ARTEMIS-4263 authenticator to delegate to artemis jaas login modules and populate callback handler
This commit is contained in:
parent
6254c140e3
commit
cf3afc4096
|
@ -136,6 +136,7 @@
|
|||
org.postgresql*;resolution:=optional,
|
||||
io.netty.buffer;io.netty.*;version="[4.1,5)",
|
||||
java.net.http*;resolution:=optional,
|
||||
com.sun.net.httpserver*;resolution:=optional,
|
||||
*
|
||||
</Import-Package>
|
||||
<_exportcontents>org.apache.activemq.artemis.*;-noimport:=true</_exportcontents>
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.NameCallback;
|
||||
import javax.security.auth.callback.PasswordCallback;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
import javax.security.auth.login.LoginContext;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import com.sun.net.httpserver.Authenticator;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpPrincipal;
|
||||
import com.sun.net.httpserver.HttpsExchange;
|
||||
|
||||
/**
|
||||
* delegate to our JAAS login modules by adapting our handlers to httpserver.httpExchange
|
||||
*/
|
||||
public class HttpServerAuthenticator extends Authenticator {
|
||||
|
||||
static final String REALM_PROPERTY_NAME = "httpServerAuthenticator.realm";
|
||||
static final String REQUEST_SUBJECT_ATTRIBUTE_PROPERTY_NAME = "httpServerAuthenticator.requestSubjectAttribute";
|
||||
static String DEFAULT_SUBJECT_ATTRIBUTE = "org.apache.activemq.artemis.jaasSubject";
|
||||
static final String DEFAULT_REALM = "http_server_authenticator";
|
||||
static final String AUTHORIZATION_HEADER_NAME = "Authorization";
|
||||
|
||||
final String realm = System.getProperty(REALM_PROPERTY_NAME, DEFAULT_REALM);
|
||||
final String subjectRequestAttribute = System.getProperty(REQUEST_SUBJECT_ATTRIBUTE_PROPERTY_NAME, DEFAULT_SUBJECT_ATTRIBUTE);
|
||||
|
||||
@Override
|
||||
public Result authenticate(HttpExchange httpExchange) {
|
||||
|
||||
try {
|
||||
LoginContext loginContext = new LoginContext(realm, callbacks -> {
|
||||
for (Callback callback : callbacks) {
|
||||
if (callback instanceof PasswordCallback) {
|
||||
PasswordCallback passwordCallback = (PasswordCallback) callback;
|
||||
|
||||
StringTokenizer stringTokenizer = new StringTokenizer(extractAuthHeader(httpExchange));
|
||||
String method = stringTokenizer.nextToken();
|
||||
if ("Basic".equalsIgnoreCase(method)) {
|
||||
byte[] authHeaderBytes = Base64.getDecoder().decode(stringTokenizer.nextToken().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// :pass
|
||||
byte[] password = Arrays.copyOfRange(authHeaderBytes, Arrays.binarySearch(authHeaderBytes, (byte) ':') + 1, authHeaderBytes.length);
|
||||
passwordCallback.setPassword(new String(password, StandardCharsets.UTF_8).toCharArray());
|
||||
} else if ("Bearer".equalsIgnoreCase(method)) {
|
||||
passwordCallback.setPassword(stringTokenizer.nextToken().toCharArray());
|
||||
}
|
||||
} else if (callback instanceof NameCallback) {
|
||||
NameCallback nameCallback = (NameCallback) callback;
|
||||
|
||||
StringTokenizer stringTokenizer = new StringTokenizer(extractAuthHeader(httpExchange));
|
||||
String method = stringTokenizer.nextToken();
|
||||
if ("Basic".equalsIgnoreCase(method)) {
|
||||
byte[] authHeaderBytes = Base64.getDecoder().decode(stringTokenizer.nextToken().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// user:
|
||||
byte[] user = Arrays.copyOfRange(authHeaderBytes, 0, Arrays.binarySearch(authHeaderBytes, (byte) ':'));
|
||||
nameCallback.setName(new String(user, StandardCharsets.UTF_8));
|
||||
}
|
||||
} else if (callback instanceof CertificateCallback) {
|
||||
CertificateCallback certCallback = (CertificateCallback) callback;
|
||||
|
||||
if (httpExchange instanceof HttpsExchange) {
|
||||
HttpsExchange httpsExchange = (HttpsExchange) httpExchange;
|
||||
Certificate[] peerCerts = httpsExchange.getSSLSession().getPeerCertificates();
|
||||
if (peerCerts != null && peerCerts.length > 0) {
|
||||
certCallback.setCertificates(new X509Certificate[]{(X509Certificate) peerCerts[0]});
|
||||
}
|
||||
}
|
||||
} else if (callback instanceof PrincipalsCallback) {
|
||||
PrincipalsCallback principalsCallback = (PrincipalsCallback) callback;
|
||||
|
||||
Principal principal = httpExchange.getPrincipal();
|
||||
if (principal == null && httpExchange instanceof HttpsExchange) {
|
||||
HttpsExchange httpsExchange = (HttpsExchange) httpExchange;
|
||||
principal = httpsExchange.getSSLSession().getPeerPrincipal();
|
||||
}
|
||||
if (principal != null) {
|
||||
principalsCallback.setPeerPrincipals(new Principal[]{principal});
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedCallbackException(callback);
|
||||
}
|
||||
}
|
||||
});
|
||||
loginContext.login();
|
||||
httpExchange.setAttribute(subjectRequestAttribute, loginContext.getSubject());
|
||||
return new Authenticator.Success(new HttpPrincipal(nameFromAuthSubject(loginContext.getSubject()), realm));
|
||||
} catch (Exception e) {
|
||||
return new Authenticator.Failure(401);
|
||||
}
|
||||
}
|
||||
|
||||
protected String extractAuthHeader(HttpExchange httpExchange) {
|
||||
return httpExchange.getRequestHeaders().getFirst(AUTHORIZATION_HEADER_NAME);
|
||||
}
|
||||
|
||||
protected String nameFromAuthSubject(Subject subject) {
|
||||
for (Principal p : subject.getPrincipals(UserPrincipal.class)) {
|
||||
return p.getName();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* 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.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Set;
|
||||
|
||||
import com.sun.net.httpserver.Authenticator;
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpsExchange;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import static org.apache.activemq.artemis.spi.core.security.jaas.HttpServerAuthenticator.AUTHORIZATION_HEADER_NAME;
|
||||
import static org.apache.activemq.artemis.spi.core.security.jaas.HttpServerAuthenticator.DEFAULT_SUBJECT_ATTRIBUTE;
|
||||
import static org.apache.activemq.artemis.spi.core.security.jaas.HttpServerAuthenticator.REALM_PROPERTY_NAME;
|
||||
import static org.apache.activemq.artemis.spi.core.security.jaas.HttpServerAuthenticator.REQUEST_SUBJECT_ATTRIBUTE_PROPERTY_NAME;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HttpServerAuthenticatorTest {
|
||||
|
||||
private final HttpsExchange httpsExchange = mock(HttpsExchange.class);
|
||||
|
||||
static final String loginConfigSystemPropName = "java.security.auth.login.config";
|
||||
|
||||
@BeforeClass
|
||||
public static void setSystemProps() {
|
||||
URL url = HttpServerAuthenticatorTest.class.getClassLoader().getResource("login.config");
|
||||
if (url != null) {
|
||||
String val = url.getFile();
|
||||
if (val != null) {
|
||||
System.setProperty(loginConfigSystemPropName, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void unsetSystemProps() {
|
||||
System.clearProperty(loginConfigSystemPropName);
|
||||
System.clearProperty(REALM_PROPERTY_NAME);
|
||||
System.clearProperty(REQUEST_SUBJECT_ATTRIBUTE_PROPERTY_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGuestLogin() {
|
||||
|
||||
System.setProperty(REALM_PROPERTY_NAME, "GuestLogin");
|
||||
System.clearProperty(REQUEST_SUBJECT_ATTRIBUTE_PROPERTY_NAME);
|
||||
|
||||
Object[] tracked = new Object[1];
|
||||
doAnswer((Answer<Void>) invocationOnMock -> {
|
||||
tracked[0] = invocationOnMock.getArgument(1);
|
||||
return null;
|
||||
}).when(httpsExchange).setAttribute(any(String.class), any(Object.class));
|
||||
when(httpsExchange.getAttribute(DEFAULT_SUBJECT_ATTRIBUTE)).then(invocationOnMock -> tracked[0]);
|
||||
|
||||
HttpServerAuthenticator underTest = new HttpServerAuthenticator();
|
||||
Authenticator.Result result = underTest.authenticate(httpsExchange);
|
||||
|
||||
assertTrue(result instanceof Authenticator.Success);
|
||||
assertThat(((Authenticator.Success) result).getPrincipal().getUsername(), is("foo"));
|
||||
|
||||
Subject subject = (Subject) httpsExchange.getAttribute(DEFAULT_SUBJECT_ATTRIBUTE);
|
||||
|
||||
assertThat(subject.getPrincipals(UserPrincipal.class), hasSize(1));
|
||||
subject.getPrincipals(UserPrincipal.class).forEach(p -> assertThat(p.getName(), is("foo")));
|
||||
Set<RolePrincipal> roles = subject.getPrincipals(RolePrincipal.class);
|
||||
assertThat(roles, hasSize(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBasicLogin() {
|
||||
|
||||
System.setProperty(REALM_PROPERTY_NAME, "PropertiesLogin");
|
||||
System.clearProperty(REQUEST_SUBJECT_ATTRIBUTE_PROPERTY_NAME);
|
||||
|
||||
Headers headers = new Headers();
|
||||
headers.add(AUTHORIZATION_HEADER_NAME, "Basic " + Base64.getEncoder().encodeToString("first:secret".getBytes(StandardCharsets.UTF_8)));
|
||||
when(httpsExchange.getRequestHeaders()).thenReturn(headers);
|
||||
|
||||
Object[] tracked = new Object[1];
|
||||
doAnswer((Answer<Void>) invocationOnMock -> {
|
||||
tracked[0] = invocationOnMock.getArgument(1);
|
||||
return null;
|
||||
}).when(httpsExchange).setAttribute(any(String.class), any(Object.class));
|
||||
when(httpsExchange.getAttribute(DEFAULT_SUBJECT_ATTRIBUTE)).then(invocationOnMock -> tracked[0]);
|
||||
|
||||
HttpServerAuthenticator underTest = new HttpServerAuthenticator();
|
||||
Authenticator.Result result = underTest.authenticate(httpsExchange);
|
||||
|
||||
assertTrue(result instanceof Authenticator.Success);
|
||||
assertThat(((Authenticator.Success) result).getPrincipal().getUsername(), is("first"));
|
||||
|
||||
Subject subject = (Subject) httpsExchange.getAttribute(DEFAULT_SUBJECT_ATTRIBUTE);
|
||||
|
||||
assertThat(subject.getPrincipals(UserPrincipal.class), hasSize(1));
|
||||
subject.getPrincipals(UserPrincipal.class).forEach(p -> assertThat(p.getName(), is("first")));
|
||||
|
||||
Set<RolePrincipal> roles = subject.getPrincipals(RolePrincipal.class);
|
||||
assertThat(roles, hasSize(2));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testNonBasic() {
|
||||
|
||||
System.setProperty(REALM_PROPERTY_NAME, "HttpServerAuthenticator");
|
||||
System.clearProperty(REQUEST_SUBJECT_ATTRIBUTE_PROPERTY_NAME);
|
||||
|
||||
Headers headers = new Headers();
|
||||
headers.add(AUTHORIZATION_HEADER_NAME, "Bearer " + Base64.getEncoder().encodeToString("some-random-string".getBytes(StandardCharsets.UTF_8)));
|
||||
when(httpsExchange.getRequestHeaders()).thenReturn(headers);
|
||||
|
||||
|
||||
HttpServerAuthenticator underTest = new HttpServerAuthenticator();
|
||||
Authenticator.Result result = underTest.authenticate(httpsExchange);
|
||||
|
||||
assertTrue(result instanceof Authenticator.Failure);
|
||||
assertNull("no subject", httpsExchange.getAttribute(DEFAULT_SUBJECT_ATTRIBUTE));
|
||||
|
||||
// kube login attempt
|
||||
verify(httpsExchange, times(1)).getRequestHeaders();
|
||||
|
||||
// cert attempt
|
||||
verify(httpsExchange, times(1)).getSSLSession();
|
||||
}
|
||||
}
|
|
@ -225,3 +225,16 @@ LDAPLoginExternalPasswordCodec2 {
|
|||
roleSearchSubtree=false
|
||||
;
|
||||
};
|
||||
|
||||
HttpServerAuthenticator {
|
||||
|
||||
org.apache.activemq.artemis.spi.core.security.jaas.TextFileCertificateLoginModule sufficient
|
||||
debug=true
|
||||
org.apache.activemq.jaas.textfiledn.user="cert-users-SMALL.properties"
|
||||
org.apache.activemq.jaas.textfiledn.role="cert-roles.properties";
|
||||
|
||||
|
||||
org.apache.activemq.artemis.spi.core.security.jaas.KubernetesLoginModule sufficient
|
||||
debug=true
|
||||
org.apache.activemq.jaas.kubernetes.role="cert-roles.properties";
|
||||
};
|
Loading…
Reference in New Issue