Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security/Extension] Extension Authentication Backend #2672

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what "er" means. I know there are some conflicting opinions, but I am of the mind that we lose meaning at a certain point of abbreviating things. I am guessing this stands for "____ roles"? Perhaps we can make this a more meaningful key?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think er stands for encrypted roles.

} 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