Skip to content

Commit 95f9c77

Browse files
authored
[Security/Extension] Extension Authentication Backend (#2672)
* Extension Authentication-backend Signed-off-by: Ryan Liang <jiallian@amazon.com>
1 parent 8f02d8d commit 95f9c77

File tree

7 files changed

+570
-4
lines changed

7 files changed

+570
-4
lines changed

src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
import org.opensearch.security.dlic.rest.api.SecurityRestApiActions;
146146
import org.opensearch.security.filter.SecurityFilter;
147147
import org.opensearch.security.filter.SecurityRestFilter;
148+
import org.opensearch.security.http.HTTPOnBehalfOfJwtAuthenticator;
148149
import org.opensearch.security.http.SecurityHttpServerTransport;
149150
import org.opensearch.security.http.SecurityNonSslHttpServerTransport;
150151
import org.opensearch.security.http.XFFResolver;
@@ -846,6 +847,8 @@ public Collection<Object> createComponents(Client localClient, ClusterService cl
846847

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

850853
final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, settings, configPath, localClient, threadPool, cih);
851854
dcf.registerDCFListener(backendRegistry);
@@ -854,6 +857,7 @@ public Collection<Object> createComponents(Client localClient, ClusterService cl
854857
dcf.registerDCFListener(xffResolver);
855858
dcf.registerDCFListener(evaluator);
856859
dcf.registerDCFListener(securityRestHandler);
860+
dcf.registerDCFListener(acInstance);
857861
if (!(auditLog instanceof NullAuditLog)) {
858862
// Don't register if advanced modules is disabled in which case auditlog is instance of NullAuditLog
859863
dcf.registerDCFListener(auditLog);

src/main/java/org/opensearch/security/auth/BackendRegistry.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ public boolean authenticate(final RestRequest request, final RestChannel channel
224224

225225
HTTPAuthenticator firstChallengingHttpAuthenticator = null;
226226

227+
//TODO: ADD OUR AUTHC BACKEND IN/BEFORE THIS LIST
228+
227229
//loop over all http/rest auth domains
228230
for (final AuthDomain authDomain: restAuthDomains) {
229231
if (isDebugEnabled) {

src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,10 @@ public String createJwt(String issuer, String subject, String audience, Integer
166166
throw new Exception("The expiration time should be a positive integer");
167167
}
168168

169+
//TODO: IF USER ENABLES THE BWC MODE, WE ARE EXPECTING TO SET PLAIN TEXT ROLE AS `dr`
169170
if (roles != null) {
170171
String listOfRoles = String.join(",", roles);
171-
jwtClaims.setProperty("roles", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles));
172+
jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles));
172173
} else {
173174
throw new Exception("Roles cannot be null");
174175
}

src/main/java/org/opensearch/security/filter/SecurityRestFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c
124124
if (!checkAndAuthenticateRequest(request, channel, client)) {
125125
User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER);
126126
if (userIsSuperAdmin(user, adminDNs) || (whitelistingSettings.checkRequestIsAllowed(request, channel, client) && allowlistingSettings.checkRequestIsAllowed(request, channel, client))) {
127-
//TODO: If the request is going to the extension, issue a JWT for authenticated user.
127+
//TODO: If the request is going to the ext, issue a JWT for authenticated user.
128128
original.handleRequest(request, channel, client);
129129
}
130130
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*
8+
* Modifications Copyright OpenSearch Contributors. See
9+
* GitHub history for details.
10+
*/
11+
12+
package org.opensearch.security.http;
13+
14+
import java.security.AccessController;
15+
import java.security.Key;
16+
import java.security.KeyFactory;
17+
import java.security.NoSuchAlgorithmException;
18+
import java.security.PrivilegedAction;
19+
import java.security.PublicKey;
20+
import java.security.spec.InvalidKeySpecException;
21+
import java.security.spec.X509EncodedKeySpec;
22+
import java.util.Arrays;
23+
import java.util.Map.Entry;
24+
import java.util.regex.Pattern;
25+
26+
import io.jsonwebtoken.Claims;
27+
import io.jsonwebtoken.JwtParser;
28+
import io.jsonwebtoken.Jwts;
29+
import io.jsonwebtoken.io.Decoders;
30+
import io.jsonwebtoken.security.WeakKeyException;
31+
import org.apache.commons.lang3.RandomStringUtils;
32+
import org.apache.hc.core5.http.HttpHeaders;
33+
import org.apache.logging.log4j.LogManager;
34+
import org.apache.logging.log4j.Logger;
35+
import org.greenrobot.eventbus.Subscribe;
36+
37+
import org.opensearch.OpenSearchSecurityException;
38+
import org.opensearch.SpecialPermission;
39+
import org.opensearch.common.util.concurrent.ThreadContext;
40+
import org.opensearch.rest.RestChannel;
41+
import org.opensearch.rest.RestRequest;
42+
import org.opensearch.security.auth.HTTPAuthenticator;
43+
import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil;
44+
import org.opensearch.security.securityconf.DynamicConfigModel;
45+
import org.opensearch.security.user.AuthCredentials;
46+
47+
public class HTTPOnBehalfOfJwtAuthenticator implements HTTPAuthenticator {
48+
49+
protected final Logger log = LogManager.getLogger(this.getClass());
50+
51+
private static final Pattern BEARER = Pattern.compile("^\\s*Bearer\\s.*", Pattern.CASE_INSENSITIVE);
52+
private static final String BEARER_PREFIX = "bearer ";
53+
54+
//TODO: TO SEE IF WE NEED THE FINAL FOR FOLLOWING
55+
private JwtParser jwtParser;
56+
private String subjectKey;
57+
58+
private String signingKey;
59+
private String encryptionKey;
60+
61+
public HTTPOnBehalfOfJwtAuthenticator() {
62+
super();
63+
init();
64+
}
65+
66+
// FOR TESTING
67+
public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey){
68+
this.signingKey = signingKey;
69+
this.encryptionKey = encryptionKey;
70+
init();
71+
}
72+
73+
private void init() {
74+
75+
try {
76+
if(signingKey == null || signingKey.length() == 0) {
77+
log.error("signingKey must not be null or empty. JWT authentication will not work");
78+
} else {
79+
80+
signingKey = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "");
81+
signingKey = signingKey.replace("-----END PUBLIC KEY-----", "");
82+
83+
byte[] decoded = Decoders.BASE64.decode(signingKey);
84+
Key key = null;
85+
86+
try {
87+
key = getPublicKey(decoded, "RSA");
88+
} catch (Exception e) {
89+
log.debug("No public RSA key, try other algos ({})", e.toString());
90+
}
91+
92+
try {
93+
key = getPublicKey(decoded, "EC");
94+
} catch (Exception e) {
95+
log.debug("No public ECDSA key, try other algos ({})", e.toString());
96+
}
97+
98+
if(key != null) {
99+
jwtParser = Jwts.parser().setSigningKey(key);
100+
} else {
101+
jwtParser = Jwts.parser().setSigningKey(decoded);
102+
}
103+
104+
}
105+
} catch (Throwable e) {
106+
log.error("Error while creating JWT authenticator", e);
107+
throw new RuntimeException(e);
108+
}
109+
110+
subjectKey = "sub";
111+
}
112+
113+
@Override
114+
@SuppressWarnings("removal")
115+
public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws OpenSearchSecurityException {
116+
final SecurityManager sm = System.getSecurityManager();
117+
118+
if (sm != null) {
119+
sm.checkPermission(new SpecialPermission());
120+
}
121+
122+
AuthCredentials creds = AccessController.doPrivileged(new PrivilegedAction<AuthCredentials>() {
123+
@Override
124+
public AuthCredentials run() {
125+
return extractCredentials0(request);
126+
}
127+
});
128+
129+
return creds;
130+
}
131+
132+
private AuthCredentials extractCredentials0(final RestRequest request) {
133+
if (jwtParser == null) {
134+
log.error("Missing Signing Key. JWT authentication will not work");
135+
return null;
136+
}
137+
138+
String jwtToken = request.header(HttpHeaders.AUTHORIZATION);
139+
140+
if (jwtToken == null || jwtToken.length() == 0) {
141+
if(log.isDebugEnabled()) {
142+
log.debug("No JWT token found in '{}' header", HttpHeaders.AUTHORIZATION);
143+
}
144+
return null;
145+
}
146+
147+
if (!BEARER.matcher(jwtToken).matches()) {
148+
jwtToken = null;
149+
}
150+
151+
final int index;
152+
if((index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { //detect Bearer
153+
jwtToken = jwtToken.substring(index+BEARER_PREFIX.length());
154+
} else {
155+
if(log.isDebugEnabled()) {
156+
log.debug("No Bearer scheme found in header");
157+
}
158+
}
159+
160+
try {
161+
final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody();
162+
163+
final String subject = extractSubject(claims, request);
164+
165+
final String audience = claims.getAudience();
166+
167+
//TODO: GET ROLESCLAIM DEPENDING ON THE STATUS OF BWC MODE. ON: er / OFF: dr
168+
Object rolesObject = null;
169+
String[] roles;
170+
171+
try {
172+
rolesObject = claims.get("er");
173+
} catch (Throwable e) {
174+
log.debug("No encrypted role founded in the claim, continue searching for decrypted roles.");
175+
}
176+
177+
try {
178+
rolesObject = claims.get("dr");
179+
} catch (Throwable e) {
180+
log.debug("No decrypted role founded in the claim.");
181+
}
182+
183+
if (rolesObject == null) {
184+
log.warn(
185+
"Failed to get roles from JWT claims. Check if this key is correct and available in the JWT payload.");
186+
roles = new String[0];
187+
} else {
188+
final String rolesClaim = rolesObject.toString();
189+
190+
// Extracting roles based on the compatbility mode
191+
String decryptedRoles = rolesClaim;
192+
if (rolesObject == claims.get("er")) {
193+
//TODO: WHERE TO GET THE ENCRYTION KEY
194+
decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim);
195+
}
196+
roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).toArray(String[]::new);
197+
}
198+
199+
if (subject == null) {
200+
log.error("No subject found in JWT token");
201+
return null;
202+
}
203+
204+
if (audience == null) {
205+
log.error("No audience found in JWT token");
206+
}
207+
208+
final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete();
209+
210+
for(Entry<String, Object> claim: claims.entrySet()) {
211+
ac.addAttribute("attr.jwt."+claim.getKey(), String.valueOf(claim.getValue()));
212+
}
213+
214+
return ac;
215+
216+
} catch (WeakKeyException e) {
217+
log.error("Cannot authenticate user with JWT because of ", e);
218+
return null;
219+
} catch (Exception e) {
220+
if(log.isDebugEnabled()) {
221+
log.debug("Invalid or expired JWT token.", e);
222+
}
223+
return null;
224+
}
225+
}
226+
227+
@Override
228+
public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) {
229+
return false;
230+
}
231+
232+
@Override
233+
public String getType() {
234+
return "onbehalfof_jwt";
235+
}
236+
237+
//TODO: Extract the audience (ext_id) and inject it into thread context
238+
239+
protected String extractSubject(final Claims claims, final RestRequest request) {
240+
String subject = claims.getSubject();
241+
if(subjectKey != null) {
242+
// try to get roles from claims, first as Object to avoid having to catch the ExpectedTypeException
243+
Object subjectObject = claims.get(subjectKey, Object.class);
244+
if(subjectObject == null) {
245+
log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey);
246+
return null;
247+
}
248+
// We expect a String. If we find something else, convert to String but issue a warning
249+
if(!(subjectObject instanceof String)) {
250+
log.warn("Expected type String in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.", subjectKey, subjectObject, subjectObject.getClass());
251+
}
252+
subject = String.valueOf(subjectObject);
253+
}
254+
return subject;
255+
}
256+
257+
private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, InvalidKeySpecException {
258+
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
259+
KeyFactory kf = KeyFactory.getInstance(algo);
260+
return kf.generatePublic(spec);
261+
}
262+
263+
@Subscribe
264+
public void onDynamicConfigModelChanged(DynamicConfigModel dcm) {
265+
266+
//TODO: #2615 FOR CONFIGURATION
267+
//For Testing
268+
signingKey = "abcd1234";
269+
encryptionKey = RandomStringUtils.randomAlphanumeric(16);
270+
}
271+
272+
}

src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ public void testCreateJwtWithRoles() throws Exception {
6868
Assert.assertNotNull(jwt.getClaim("iat"));
6969
Assert.assertNotNull(jwt.getClaim("exp"));
7070
Assert.assertEquals(expectedExp, jwt.getClaim("exp"));
71-
Assert.assertNotEquals(expectedRoles, jwt.getClaim("roles"));
72-
Assert.assertEquals(expectedRoles, EncryptionDecryptionUtil.decrypt(claimsEncryptionKey, jwt.getClaim("roles").toString()));
71+
Assert.assertNotEquals(expectedRoles, jwt.getClaim("er"));
72+
Assert.assertEquals(expectedRoles, EncryptionDecryptionUtil.decrypt(claimsEncryptionKey, jwt.getClaim("er").toString()));
7373
}
7474

7575
@Test (expected = Exception.class)

0 commit comments

Comments
 (0)