Skip to content

Commit

Permalink
[Security/Extension] Extension Authentication Backend (#2672)
Browse files Browse the repository at this point in the history
* Extension Authentication-backend

Signed-off-by: Ryan Liang <jiallian@amazon.com>
  • Loading branch information
RyanL1997 authored May 11, 2023
1 parent 8f02d8d commit 95f9c77
Show file tree
Hide file tree
Showing 7 changed files with 570 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
import org.opensearch.security.dlic.rest.api.SecurityRestApiActions;
import org.opensearch.security.filter.SecurityFilter;
import org.opensearch.security.filter.SecurityRestFilter;
import org.opensearch.security.http.HTTPOnBehalfOfJwtAuthenticator;
import org.opensearch.security.http.SecurityHttpServerTransport;
import org.opensearch.security.http.SecurityNonSslHttpServerTransport;
import org.opensearch.security.http.XFFResolver;
Expand Down Expand Up @@ -846,6 +847,8 @@ public Collection<Object> createComponents(Client localClient, ClusterService cl

securityRestHandler = new SecurityRestFilter(backendRegistry, auditLog, threadPool,
principalExtractor, settings, configPath, compatConfig);
//TODO: CREATE A INSTANCE OF HTTPExtensionAuthenticationBackend
HTTPOnBehalfOfJwtAuthenticator acInstance = new HTTPOnBehalfOfJwtAuthenticator();

final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih);
dcf.registerDCFListener(backendRegistry);
Expand All @@ -854,6 +857,7 @@ public Collection<Object> createComponents(Client localClient, ClusterService cl
dcf.registerDCFListener(xffResolver);
dcf.registerDCFListener(evaluator);
dcf.registerDCFListener(securityRestHandler);
dcf.registerDCFListener(acInstance);
if (!(auditLog instanceof NullAuditLog)) {
// Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog
dcf.registerDCFListener(auditLog);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ public boolean authenticate(final RestRequest request, final RestChannel channel

HTTPAuthenticator firstChallengingHttpAuthenticator = null;

//TODO: ADD OUR AUTHC BACKEND IN/BEFORE THIS LIST

//loop over all http/rest auth domains
for (final AuthDomain authDomain: restAuthDomains) {
if (isDebugEnabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ public String createJwt(String issuer, String subject, String audience, Integer
throw new Exception("The expiration time should be a positive integer");
}

//TODO: IF USER ENABLES THE BWC MODE, WE ARE EXPECTING TO SET PLAIN TEXT ROLE AS `dr`
if (roles != null) {
String listOfRoles = String.join(",", roles);
jwtClaims.setProperty("roles", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles));
jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles));
} else {
throw new Exception("Roles cannot be null");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c
if (!checkAndAuthenticateRequest(request, channel, client)) {
User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER);
if (userIsSuperAdmin(user, adminDNs) || (whitelistingSettings.checkRequestIsAllowed(request, channel, client) && allowlistingSettings.checkRequestIsAllowed(request, channel, client))) {
//TODO: If the request is going to the extension, issue a JWT for authenticated user.
//TODO: If the request is going to the ext, issue a JWT for authenticated user.
original.handleRequest(request, channel, client);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.http;

import java.security.AccessController;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.WeakKeyException;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.greenrobot.eventbus.Subscribe;

import org.opensearch.OpenSearchSecurityException;
import org.opensearch.SpecialPermission;
import org.opensearch.common.util.concurrent.ThreadContext;
import org.opensearch.rest.RestChannel;
import org.opensearch.rest.RestRequest;
import org.opensearch.security.auth.HTTPAuthenticator;
import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil;
import org.opensearch.security.securityconf.DynamicConfigModel;
import org.opensearch.security.user.AuthCredentials;

public class HTTPOnBehalfOfJwtAuthenticator implements HTTPAuthenticator {

protected final Logger log = LogManager.getLogger(this.getClass());

private static final Pattern BEARER = Pattern.compile("^\\s*Bearer\\s.*", Pattern.CASE_INSENSITIVE);
private static final String BEARER_PREFIX = "bearer ";

//TODO: TO SEE IF WE NEED THE FINAL FOR FOLLOWING
private JwtParser jwtParser;
private String subjectKey;

private String signingKey;
private String encryptionKey;

public HTTPOnBehalfOfJwtAuthenticator() {
super();
init();
}

// FOR TESTING
public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey){
this.signingKey = signingKey;
this.encryptionKey = encryptionKey;
init();
}

private void init() {

try {
if(signingKey == null || signingKey.length() == 0) {
log.error("signingKey must not be null or empty. JWT authentication will not work");
} else {

signingKey = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "");
signingKey = signingKey.replace("-----END PUBLIC KEY-----", "");

byte[] decoded = Decoders.BASE64.decode(signingKey);
Key key = null;

try {
key = getPublicKey(decoded, "RSA");
} catch (Exception e) {
log.debug("No public RSA key, try other algos ({})", e.toString());
}

try {
key = getPublicKey(decoded, "EC");
} catch (Exception e) {
log.debug("No public ECDSA key, try other algos ({})", e.toString());
}

if(key != null) {
jwtParser = Jwts.parser().setSigningKey(key);
} else {
jwtParser = Jwts.parser().setSigningKey(decoded);
}

}
} catch (Throwable e) {
log.error("Error while creating JWT authenticator", e);
throw new RuntimeException(e);
}

subjectKey = "sub";
}

@Override
@SuppressWarnings("removal")
public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException {
final SecurityManager sm = System.getSecurityManager();

if (sm != null) {
sm.checkPermission(new SpecialPermission());
}

AuthCredentials creds = AccessController.doPrivileged(new PrivilegedAction<AuthCredentials>() {
@Override
public AuthCredentials run() {
return extractCredentials0(request);
}
});

return creds;
}

private AuthCredentials extractCredentials0(final RestRequest request) {
if (jwtParser == null) {
log.error("Missing Signing Key. JWT authentication will not work");
return null;
}

String jwtToken = request.header(HttpHeaders.AUTHORIZATION);

if (jwtToken == null || jwtToken.length() == 0) {
if(log.isDebugEnabled()) {
log.debug("No JWT token found in '{}' header", HttpHeaders.AUTHORIZATION);
}
return null;
}

if (!BEARER.matcher(jwtToken).matches()) {
jwtToken = null;
}

final int index;
if((index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { //detect Bearer
jwtToken = jwtToken.substring(index+BEARER_PREFIX.length());
} else {
if(log.isDebugEnabled()) {
log.debug("No Bearer scheme found in header");
}
}

try {
final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody();

final String subject = extractSubject(claims, request);

final String audience = claims.getAudience();

//TODO: GET ROLESCLAIM DEPENDING ON THE STATUS OF BWC MODE. ON: er / OFF: dr
Object rolesObject = null;
String[] roles;

try {
rolesObject = claims.get("er");
} catch (Throwable e) {
log.debug("No encrypted role founded in the claim, continue searching for decrypted roles.");
}

try {
rolesObject = claims.get("dr");
} catch (Throwable e) {
log.debug("No decrypted role founded in the claim.");
}

if (rolesObject == null) {
log.warn(
"Failed to get roles from JWT claims. Check if this key is correct and available in the JWT payload.");
roles = new String[0];
} else {
final String rolesClaim = rolesObject.toString();

// Extracting roles based on the compatbility mode
String decryptedRoles = rolesClaim;
if (rolesObject == claims.get("er")) {
//TODO: WHERE TO GET THE ENCRYTION KEY
decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim);
}
roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).toArray(String[]::new);
}

if (subject == null) {
log.error("No subject found in JWT token");
return null;
}

if (audience == null) {
log.error("No audience found in JWT token");
}

final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete();

for(Entry<String, Object> claim: claims.entrySet()) {
ac.addAttribute("attr.jwt."+claim.getKey(), String.valueOf(claim.getValue()));
}

return ac;

} catch (WeakKeyException e) {
log.error("Cannot authenticate user with JWT because of ", e);
return null;
} catch (Exception e) {
if(log.isDebugEnabled()) {
log.debug("Invalid or expired JWT token.", e);
}
return null;
}
}

@Override
public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) {
return false;
}

@Override
public String getType() {
return "onbehalfof_jwt";
}

//TODO: Extract the audience (ext_id) and inject it into thread context

protected String extractSubject(final Claims claims, final RestRequest request) {
String subject = claims.getSubject();
if(subjectKey != null) {
// try to get roles from claims, first as Object to avoid having to catch the ExpectedTypeException
Object subjectObject = claims.get(subjectKey, Object.class);
if(subjectObject == null) {
log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey);
return null;
}
// We expect a String. If we find something else, convert to String but issue a warning
if(!(subjectObject instanceof String)) {
log.warn("Expected type String in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.", subjectKey, subjectObject, subjectObject.getClass());
}
subject = String.valueOf(subjectObject);
}
return subject;
}

private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, InvalidKeySpecException {
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance(algo);
return kf.generatePublic(spec);
}

@Subscribe
public void onDynamicConfigModelChanged(DynamicConfigModel dcm) {

//TODO: #2615 FOR CONFIGURATION
//For Testing
signingKey = "abcd1234";
encryptionKey = RandomStringUtils.randomAlphanumeric(16);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ public void testCreateJwtWithRoles() throws Exception {
Assert.assertNotNull(jwt.getClaim("iat"));
Assert.assertNotNull(jwt.getClaim("exp"));
Assert.assertEquals(expectedExp, jwt.getClaim("exp"));
Assert.assertNotEquals(expectedRoles, jwt.getClaim("roles"));
Assert.assertEquals(expectedRoles, EncryptionDecryptionUtil.decrypt(claimsEncryptionKey, jwt.getClaim("roles").toString()));
Assert.assertNotEquals(expectedRoles, jwt.getClaim("er"));
Assert.assertEquals(expectedRoles, EncryptionDecryptionUtil.decrypt(claimsEncryptionKey, jwt.getClaim("er").toString()));
}

@Test (expected = Exception.class)
Expand Down
Loading

0 comments on commit 95f9c77

Please sign in to comment.