ARTEMIS-4263 authenticator to delegate to artemis jaas login modules and populate callback handler

This commit is contained in:
Gary Tully 2023-01-27 10:34:42 +00:00
parent 6254c140e3
commit cf3afc4096
4 changed files with 300 additions and 0 deletions

View File

@ -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>

View File

@ -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 "";
}
}

View File

@ -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();
}
}

View File

@ -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";
};