Commit b8416202 authored by Nils Bandener's avatar Nils Bandener
Browse files

subject_pattern support for JWT, OIDC, SAML authenticators

parent bfa2743f
......@@ -22,6 +22,9 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
import org.apache.cxf.rs.security.jose.jwt.JwtToken;
......@@ -61,6 +64,7 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
private final String jwtHeaderName;
private final String jwtUrlParameter;
private final String subjectKey;
private final Pattern subjectPattern;
private final String rolesKey;
private final String jsonSubjectPath;
private final String jsonRolesPath;
......@@ -74,7 +78,8 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
subjectKey = settings.get("subject_key");
jsonRolesPath = settings.get("roles_path");
jsonSubjectPath = settings.get("subject_path");
subjectPattern = getSubjectPattern(settings);
try {
this.keyProvider = this.initKeyProvider(settings, configPath);
jwtVerifier = new JwtVerifier(keyProvider);
......@@ -155,7 +160,7 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
if (jwtToken == null || jwtToken.isEmpty()) {
jwtToken = request.param(jwtUrlParameter);
} else {
// just consume to avoid "contains unrecognized parameter"
// just consume to avoid "contains unrecognized para)meter"
request.param(jwtUrlParameter);
}
}
......@@ -201,6 +206,34 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
return null;
}
}
if (subject != null && subjectPattern != null) {
Matcher matcher = subjectPattern.matcher(subject);
if (!matcher.matches()) {
log.warn("Subject " + subject + " does not match subject_pattern " + subjectPattern);
return null;
}
if (matcher.groupCount() == 1) {
subject = matcher.group(1);
} else if (matcher.groupCount() > 1) {
StringBuilder subjectBuilder = new StringBuilder();
for (int i = 1; i <= matcher.groupCount(); i++) {
if (matcher.group(i) != null) {
subjectBuilder.append(matcher.group(i));
}
}
if (subjectBuilder.length() != 0) {
subject = subjectBuilder.toString();
} else {
subject = null;
}
}
}
return subject;
}
......@@ -271,4 +304,18 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
return true;
}
private static Pattern getSubjectPattern(Settings settings) {
String patternString = settings.get("subject_pattern");
if (patternString == null) {
return null;
}
try {
return Pattern.compile(patternString);
} catch (PatternSyntaxException e) {
log.error("Invalid regular expression for subject_pattern: " + patternString, e);
return null;
}
}
}
......@@ -28,6 +28,9 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
......@@ -58,7 +61,7 @@ import net.minidev.json.JSONArray;
public class HTTPJwtAuthenticator implements HTTPAuthenticator {
protected final Logger log = LogManager.getLogger(this.getClass());
private static final Logger log = LogManager.getLogger(HTTPJwtAuthenticator.class);
private static final String BEARER = "bearer ";
private final JwtParser jwtParser;
......@@ -70,9 +73,12 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator {
private final String jsonRolesPath;
private Configuration jsonPathConfig;
private Map<String, JsonPath> attributeMapping;
private final Pattern subjectPattern;
public HTTPJwtAuthenticator(final Settings settings, final Path configPath) {
super();
subjectPattern = getSubjectPattern(settings);
JwtParser _jwtParser = null;
......@@ -232,6 +238,34 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator {
log.error("The provided JSON path {} could not be found ", jsonSubjectPath);
}
}
if (subject != null && subjectPattern != null) {
Matcher matcher = subjectPattern.matcher(subject);
if (!matcher.matches()) {
log.warn("Subject " + subject + " does not match subject_pattern " + subjectPattern);
return null;
}
if (matcher.groupCount() == 1) {
subject = matcher.group(1);
} else if (matcher.groupCount() > 1) {
StringBuilder subjectBuilder = new StringBuilder();
for (int i = 1; i <= matcher.groupCount(); i++) {
if (matcher.group(i) != null) {
subjectBuilder.append(matcher.group(i));
}
}
if (subjectBuilder.length() != 0) {
subject = subjectBuilder.toString();
} else {
subject = null;
}
}
}
return subject;
}
......@@ -296,5 +330,21 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator {
return roles;
}
private static Pattern getSubjectPattern(Settings settings) {
String patternString = settings.get("subject_pattern");
if (patternString == null) {
return null;
}
try {
return Pattern.compile(patternString);
} catch (PatternSyntaxException e) {
log.error("Invalid regular expression for subject_pattern: " + patternString, e);
return null;
}
}
}
......@@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
......@@ -87,6 +88,7 @@ class AuthTokenProcessorHandler {
private ExpiryBaseValue expiryBaseValue = ExpiryBaseValue.AUTO;
private JsonWebKey signingKey;
private JsonMapObjectReaderWriter jsonMapReaderWriter = new JsonMapObjectReaderWriter();
private Pattern subjectPattern;
AuthTokenProcessorHandler(Settings settings, Settings jwtSettings, Saml2SettingsProvider saml2SettingsProvider) throws Exception {
this.saml2SettingsProvider = saml2SettingsProvider;
......@@ -99,6 +101,8 @@ class AuthTokenProcessorHandler {
this.samlRolesSeparator = settings.get("roles_seperator");
this.kibanaRootUrl = settings.get("kibana_url");
this.subjectPattern = getSubjectPattern(settings);
if (samlRolesKey == null || samlRolesKey.length() == 0) {
log.warn("roles_key is not configured, will only extract subject from SAML");
samlRolesKey = null;
......@@ -371,7 +375,7 @@ class AuthTokenProcessorHandler {
private String extractSubject(SamlResponse samlResponse) throws Exception {
if (this.samlSubjectKey == null) {
return samlResponse.getNameId();
return applySubjectPattern(samlResponse.getNameId());
}
List<String> values = samlResponse.getAttributes().get(this.samlSubjectKey);
......@@ -380,9 +384,46 @@ class AuthTokenProcessorHandler {
return null;
}
return values.get(0);
return applySubjectPattern(values.get(0));
}
private String applySubjectPattern(String subject) {
if (subject == null) {
return null;
}
if (subjectPattern == null) {
return subject;
}
Matcher matcher = subjectPattern.matcher(subject);
if (!matcher.matches()) {
log.warn("Subject " + subject + " does not match subject_pattern " + subjectPattern);
return null;
}
if (matcher.groupCount() == 1) {
subject = matcher.group(1);
} else if (matcher.groupCount() > 1) {
StringBuilder subjectBuilder = new StringBuilder();
for (int i = 1; i <= matcher.groupCount(); i++) {
if (matcher.group(i) != null) {
subjectBuilder.append(matcher.group(i));
}
}
if (subjectBuilder.length() != 0) {
subject = subjectBuilder.toString();
} else {
subject = null;
}
}
return subject;
}
private String[] extractRoles(SamlResponse samlResponse) throws XPathExpressionException, ValidationError {
if (this.samlRolesKey == null) {
return new String[0];
......@@ -539,4 +580,19 @@ class AuthTokenProcessorHandler {
}
private static Pattern getSubjectPattern(Settings settings) {
String patternString = settings.get("subject_pattern");
if (patternString == null) {
return null;
}
try {
return Pattern.compile(patternString);
} catch (PatternSyntaxException e) {
log.error("Invalid regular expression for subject_pattern: " + patternString, e);
return null;
}
}
}
......@@ -437,7 +437,7 @@ public class HTTPSamlAuthenticator implements HTTPAuthenticator, Destroyable {
}
}
private enum IdpEndpointType {
SSO, SLO
}
......
......@@ -660,4 +660,20 @@ public class HTTPJwtAuthenticatorTest {
}
@Test
public void testSubjectPattern() throws Exception {
Settings settings = Settings.builder().put("signing_key", BaseEncoding.base64().encode(secretKey)).put("subject_pattern", "^(.+)@(?:.+)$")
.build();
String jwsToken = Jwts.builder().setSubject("leonard@mccoy.com").setAudience("myaud")
.signWith(Keys.hmacShaKeyFor(secretKey), SignatureAlgorithm.HS512).compact();
HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null);
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + jwsToken);
AuthCredentials creds = jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()), null);
Assert.assertNotNull(creds);
Assert.assertEquals("leonard", creds.getUsername());
}
}
......@@ -288,6 +288,23 @@ public class HTTPJwtKeyByOpenIdConnectAuthenticatorTest {
Assert.assertEquals(0, creds.getBackendRoles().size());
Assert.assertEquals(3, creds.getAttributes().size());
}
@Test
public void testSubjectPattern() {
Settings settings = Settings.builder().put("openid_connect_url", mockIdpServer.getDiscoverUri()).put("subject_pattern", "^(.)(?:.*)$").build();
HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null);
AuthCredentials creds = jwtAuth.extractCredentials(
new FakeRestRequest(ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_OCT_1), new HashMap<String, String>()), null);
Assert.assertNotNull(creds);
Assert.assertEquals(TestJwts.MCCOY_SUBJECT.substring(0, 1), creds.getUsername());
Assert.assertEquals(TestJwts.TEST_AUDIENCE, creds.getAttributes().get("attr.jwt.aud"));
Assert.assertEquals(0, creds.getBackendRoles().size());
Assert.assertEquals(3, creds.getAttributes().size());
}
static class TestRestChannel implements RestChannel {
......
......@@ -493,6 +493,42 @@ public class HTTPSamlAuthenticatorTest {
Assert.assertEquals("horst", jwt.getClaim("sub"));
}
}
@Test
public void subjectPatternTest() throws Exception {
mockSamlIdpServer.setSignResponses(true);
mockSamlIdpServer.loadSigningKeys("saml/kirk-keystore.jks", "kirk");
mockSamlIdpServer.setAuthenticateUser("leonard@example.com");
mockSamlIdpServer.setEndpointQueryString(null);
Settings settings = Settings.builder().put("idp.metadata_url", mockSamlIdpServer.getMetadataUri())
.put("kibana_url", "http://wherever").put("idp.entity_id", mockSamlIdpServer.getIdpEntityId())
.put("exchange_key", "abc").put("roles_key", "roles").put("path.home", ".").put("subject_pattern", "^(.+)@(?:.+)$").build();
HTTPSamlAuthenticator samlAuthenticator = new HTTPSamlAuthenticator(settings, null);
AuthenticateHeaders authenticateHeaders = getAutenticateHeaders(samlAuthenticator);
String encodedSamlResponse = mockSamlIdpServer.handleSsoGetRequestURI(authenticateHeaders.location);
RestRequest tokenRestRequest = buildTokenExchangeRestRequest(encodedSamlResponse, authenticateHeaders);
TestRestChannel tokenRestChannel = new TestRestChannel(tokenRestRequest);
samlAuthenticator.reRequestAuthentication(tokenRestChannel, null);
String responseJson = new String(BytesReference.toBytes(tokenRestChannel.response.content()));
HashMap<String, Object> response = DefaultObjectMapper.objectMapper.readValue(responseJson,
new TypeReference<HashMap<String, Object>>() {
});
String authorization = (String) response.get("authorization");
Assert.assertNotNull("Expected authorization attribute in JSON: " + responseJson, authorization);
JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(authorization.replaceAll("\\s*bearer\\s*", ""));
JwtToken jwt = jwtConsumer.getJwtToken();
Assert.assertEquals("leonard", jwt.getClaim("sub"));
}
private AuthenticateHeaders getAutenticateHeaders(HTTPSamlAuthenticator samlAuthenticator) {
RestRequest restRequest = new FakeRestRequest(ImmutableMap.of(), new HashMap<String, String>());
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment