Skip to content

Commit 8efd3b0

Browse files
[FEATURE] usage of JWKS with JWT (w/o OpenID connect)
Signed-off-by: Sebastian Michalski <shekerama@gmail.com>
1 parent b8c3889 commit 8efd3b0

File tree

1 file changed

+180
-183
lines changed

1 file changed

+180
-183
lines changed

src/main/java/com/amazon/dlic/auth/http/jwt/AbstractHTTPJwtAuthenticator.java

Lines changed: 180 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -43,199 +43,196 @@
4343
import org.opensearch.security.user.AuthCredentials;
4444

4545
public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator {
46-
private final static Logger log = LogManager.getLogger(AbstractHTTPJwtAuthenticator.class);
47-
48-
private static final String BEARER = "bearer ";
49-
private static final Pattern BASIC = Pattern.compile("^\\s*Basic\\s.*", Pattern.CASE_INSENSITIVE);
50-
51-
private KeyProvider keyProvider;
52-
private JwtVerifier jwtVerifier;
53-
private final String jwtHeaderName;
54-
private final boolean isDefaultAuthHeader;
55-
private final String jwtUrlParameter;
56-
private final String subjectKey;
57-
private final String rolesKey;
46+
private final static Logger log = LogManager.getLogger(AbstractHTTPJwtAuthenticator.class);
47+
48+
private static final String BEARER = "bearer ";
49+
private static final Pattern BASIC = Pattern.compile("^\\s*Basic\\s.*", Pattern.CASE_INSENSITIVE);
50+
51+
private KeyProvider keyProvider;
52+
private JwtVerifier jwtVerifier;
53+
private final String jwtHeaderName;
54+
private final boolean isDefaultAuthHeader;
55+
private final String jwtUrlParameter;
56+
private final String subjectKey;
57+
private final String rolesKey;
5858
private final String requiredAudience;
5959
private final String requiredIssuer;
6060

6161
public static final int DEFAULT_CLOCK_SKEW_TOLERANCE_SECONDS = 30;
62-
private final int clockSkewToleranceSeconds ;
63-
64-
public AbstractHTTPJwtAuthenticator(Settings settings, Path configPath) {
65-
jwtUrlParameter = settings.get("jwt_url_parameter");
66-
jwtHeaderName = settings.get("jwt_header", HttpHeaders.AUTHORIZATION);
67-
isDefaultAuthHeader = HttpHeaders.AUTHORIZATION.equalsIgnoreCase(jwtHeaderName);
68-
rolesKey = settings.get("roles_key");
69-
subjectKey = settings.get("subject_key");
70-
clockSkewToleranceSeconds = settings.getAsInt("jwt_clock_skew_tolerance_seconds", DEFAULT_CLOCK_SKEW_TOLERANCE_SECONDS);
62+
private final int clockSkewToleranceSeconds;
63+
64+
public AbstractHTTPJwtAuthenticator(Settings settings, Path configPath) {
65+
jwtUrlParameter = settings.get("jwt_url_parameter");
66+
jwtHeaderName = settings.get("jwt_header", HttpHeaders.AUTHORIZATION);
67+
isDefaultAuthHeader = HttpHeaders.AUTHORIZATION.equalsIgnoreCase(jwtHeaderName);
68+
rolesKey = settings.get("roles_key");
69+
subjectKey = settings.get("subject_key");
70+
clockSkewToleranceSeconds = settings.getAsInt("jwt_clock_skew_tolerance_seconds", DEFAULT_CLOCK_SKEW_TOLERANCE_SECONDS);
7171
requiredAudience = settings.get("required_audience");
7272
requiredIssuer = settings.get("required_issuer");
7373

74-
try {
75-
this.keyProvider = this.initKeyProvider(settings, configPath);
76-
jwtVerifier = new JwtVerifier(keyProvider, clockSkewToleranceSeconds );
74+
try {
75+
this.keyProvider = this.initKeyProvider(settings, configPath);
76+
jwtVerifier = new JwtVerifier(keyProvider, clockSkewToleranceSeconds);
7777

78-
} catch (Exception e) {
79-
log.error("Error creating JWT authenticator. JWT authentication will not work", e);
80-
throw new RuntimeException(e);
81-
}
82-
}
78+
} catch (Exception e) {
79+
log.error("Error creating JWT authenticator. JWT authentication will not work", e);
80+
throw new RuntimeException(e);
81+
}
82+
}
83+
84+
@Override @SuppressWarnings("removal") public AuthCredentials extractCredentials(RestRequest request, ThreadContext context)
85+
throws OpenSearchSecurityException {
86+
final SecurityManager sm = System.getSecurityManager();
87+
88+
if (sm != null) {
89+
sm.checkPermission(new SpecialPermission());
90+
}
91+
92+
AuthCredentials creds = AccessController.doPrivileged(new PrivilegedAction<AuthCredentials>() {
93+
@Override public AuthCredentials run() {
94+
return extractCredentials0(request);
95+
}
96+
});
97+
98+
return creds;
99+
}
100+
101+
private AuthCredentials extractCredentials0(final RestRequest request) throws OpenSearchSecurityException {
102+
103+
String jwtString = getJwtTokenString(request);
104+
105+
if (Strings.isNullOrEmpty(jwtString)) {
106+
return null;
107+
}
108+
109+
JwtToken jwt;
110+
111+
try {
112+
jwt = jwtVerifier.getVerifiedJwtToken(jwtString);
113+
} catch (AuthenticatorUnavailableException e) {
114+
log.info(e.toString());
115+
throw new OpenSearchSecurityException(e.getMessage(), RestStatus.SERVICE_UNAVAILABLE);
116+
} catch (BadCredentialsException e) {
117+
log.info("Extracting JWT token from {} failed", jwtString, e);
118+
return null;
119+
}
120+
121+
JwtClaims claims = jwt.getClaims();
122+
123+
final String subject = extractSubject(claims);
124+
125+
if (subject == null) {
126+
log.error("No subject found in JWT token");
127+
return null;
128+
}
129+
130+
final String[] roles = extractRoles(claims);
131+
132+
final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete();
83133

84-
@Override
85-
@SuppressWarnings("removal")
86-
public AuthCredentials extractCredentials(RestRequest request, ThreadContext context)
87-
throws OpenSearchSecurityException {
88-
final SecurityManager sm = System.getSecurityManager();
134+
for (Entry<String, Object> claim : claims.asMap().entrySet()) {
135+
ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue()));
136+
}
89137

90-
if (sm != null) {
91-
sm.checkPermission(new SpecialPermission());
92-
}
93-
94-
AuthCredentials creds = AccessController.doPrivileged(new PrivilegedAction<AuthCredentials>() {
95-
@Override
96-
public AuthCredentials run() {
97-
return extractCredentials0(request);
98-
}
99-
});
100-
101-
return creds;
102-
}
103-
104-
private AuthCredentials extractCredentials0(final RestRequest request) throws OpenSearchSecurityException {
105-
106-
String jwtString = getJwtTokenString(request);
107-
108-
if (Strings.isNullOrEmpty(jwtString)) {
109-
return null;
110-
}
111-
112-
JwtToken jwt;
113-
114-
try {
115-
jwt = jwtVerifier.getVerifiedJwtToken(jwtString);
116-
} catch (AuthenticatorUnavailableException e) {
117-
log.info(e.toString());
118-
throw new OpenSearchSecurityException(e.getMessage(), RestStatus.SERVICE_UNAVAILABLE);
119-
} catch (BadCredentialsException e) {
120-
log.info("Extracting JWT token from {} failed", jwtString, e);
121-
return null;
122-
}
123-
124-
JwtClaims claims = jwt.getClaims();
125-
126-
final String subject = extractSubject(claims);
127-
128-
if (subject == null) {
129-
log.error("No subject found in JWT token");
130-
return null;
131-
}
132-
133-
final String[] roles = extractRoles(claims);
134-
135-
final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete();
136-
137-
for (Entry<String, Object> claim : claims.asMap().entrySet()) {
138-
ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue()));
139-
}
140-
141-
return ac;
142-
143-
}
144-
145-
protected String getJwtTokenString(RestRequest request) {
146-
String jwtToken = request.header(jwtHeaderName);
147-
if (isDefaultAuthHeader && jwtToken != null && BASIC.matcher(jwtToken).matches()) {
148-
jwtToken = null;
149-
}
150-
151-
if (jwtUrlParameter != null) {
152-
if (jwtToken == null || jwtToken.isEmpty()) {
153-
jwtToken = request.param(jwtUrlParameter);
154-
} else {
155-
// just consume to avoid "contains unrecognized parameter"
156-
request.param(jwtUrlParameter);
157-
}
158-
}
159-
160-
if (jwtToken == null) {
161-
return null;
162-
}
163-
164-
int index;
165-
166-
if ((index = jwtToken.toLowerCase().indexOf(BEARER)) > -1) { // detect Bearer
167-
jwtToken = jwtToken.substring(index + BEARER.length());
168-
}
169-
170-
return jwtToken;
171-
}
172-
173-
@VisibleForTesting
174-
public String extractSubject(JwtClaims claims) {
175-
String subject = claims.getSubject();
176-
177-
if (subjectKey != null) {
178-
Object subjectObject = claims.getClaim(subjectKey);
179-
180-
if (subjectObject == null) {
181-
log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey);
182-
return null;
183-
}
184-
185-
// We expect a String. If we find something else, convert to String but issue a
186-
// warning
187-
if (!(subjectObject instanceof String)) {
188-
log.warn(
189-
"Expected type String for roles in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.",
190-
subjectKey, subjectObject, subjectObject.getClass());
191-
subject = String.valueOf(subjectObject);
192-
} else {
193-
subject = (String) subjectObject;
194-
}
195-
}
196-
return subject;
197-
}
198-
199-
@SuppressWarnings("unchecked")
200-
@VisibleForTesting
201-
public String[] extractRoles(JwtClaims claims) {
202-
if (rolesKey == null) {
203-
return new String[0];
204-
}
205-
206-
Object rolesObject = claims.getClaim(rolesKey);
207-
208-
if (rolesObject == null) {
209-
log.warn(
210-
"Failed to get roles from JWT claims with roles_key '{}'. Check if this key is correct and available in the JWT payload.",
211-
rolesKey);
212-
return new String[0];
213-
}
214-
215-
String[] roles = String.valueOf(rolesObject).split(",");
216-
217-
// We expect a String or Collection. If we find something else, convert to
218-
// String but issue a warning
219-
if (!(rolesObject instanceof String) && !(rolesObject instanceof Collection<?>)) {
220-
log.warn(
221-
"Expected type String or Collection for roles in the JWT for roles_key {}, but value was '{}' ({}). Will convert this value to String.",
222-
rolesKey, rolesObject, rolesObject.getClass());
223-
} else if (rolesObject instanceof Collection<?>) {
224-
roles = ((Collection<String>) rolesObject).toArray(new String[0]);
225-
}
226-
227-
return roles;
228-
}
229-
230-
protected abstract KeyProvider initKeyProvider(Settings settings, Path configPath) throws Exception;
231-
232-
@Override
233-
public boolean reRequestAuthentication(RestChannel channel, AuthCredentials authCredentials) {
234-
final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, "");
235-
wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Bearer realm=\"OpenSearch Security\"");
236-
channel.sendResponse(wwwAuthenticateResponse);
237-
return true;
238-
}
138+
return ac;
139+
140+
}
141+
142+
protected String getJwtTokenString(RestRequest request) {
143+
String jwtToken = request.header(jwtHeaderName);
144+
if (isDefaultAuthHeader && jwtToken != null && BASIC.matcher(jwtToken).matches()) {
145+
jwtToken = null;
146+
}
147+
148+
if (jwtUrlParameter != null) {
149+
if (jwtToken == null || jwtToken.isEmpty()) {
150+
jwtToken = request.param(jwtUrlParameter);
151+
} else {
152+
// just consume to avoid "contains unrecognized parameter"
153+
request.param(jwtUrlParameter);
154+
}
155+
}
156+
157+
if (jwtToken == null) {
158+
return null;
159+
}
160+
161+
int index;
162+
163+
if ((index = jwtToken.toLowerCase().indexOf(BEARER)) > -1) { // detect Bearer
164+
jwtToken = jwtToken.substring(index + BEARER.length());
165+
}
166+
167+
return jwtToken;
168+
}
169+
170+
@VisibleForTesting public String extractSubject(JwtClaims claims) {
171+
String subject = claims.getSubject();
172+
173+
if (subjectKey != null) {
174+
Object subjectObject = claims.getClaim(subjectKey);
175+
176+
if (subjectObject == null) {
177+
log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey);
178+
return null;
179+
}
180+
181+
// We expect a String. If we find something else, convert to String but issue a
182+
// warning
183+
if (!(subjectObject instanceof String)) {
184+
log.warn(
185+
"Expected type String for roles in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.",
186+
subjectKey,
187+
subjectObject,
188+
subjectObject.getClass());
189+
subject = String.valueOf(subjectObject);
190+
} else {
191+
subject = (String) subjectObject;
192+
}
193+
}
194+
return subject;
195+
}
196+
197+
@SuppressWarnings("unchecked") @VisibleForTesting public String[] extractRoles(JwtClaims claims) {
198+
if (rolesKey == null) {
199+
return new String[0];
200+
}
201+
202+
Object rolesObject = claims.getClaim(rolesKey);
203+
204+
if (rolesObject == null) {
205+
log.warn(
206+
"Failed to get roles from JWT claims with roles_key '{}'. Check if this key is correct and available in the JWT payload.",
207+
rolesKey);
208+
return new String[0];
209+
}
210+
211+
String[] roles = String.valueOf(rolesObject).split(",");
212+
213+
// We expect a String or Collection. If we find something else, convert to
214+
// String but issue a warning
215+
if (!(rolesObject instanceof String) && !(rolesObject instanceof Collection<?>)) {
216+
log.warn(
217+
"Expected type String or Collection for roles in the JWT for roles_key {}, but value was '{}' ({}). Will convert this value to String.",
218+
rolesKey,
219+
rolesObject,
220+
rolesObject.getClass());
221+
} else if (rolesObject instanceof Collection<?>) {
222+
roles = ((Collection<String>) rolesObject).toArray(new String[0]);
223+
}
224+
225+
return roles;
226+
}
227+
228+
protected abstract KeyProvider initKeyProvider(Settings settings, Path configPath) throws Exception;
229+
230+
@Override public boolean reRequestAuthentication(RestChannel channel, AuthCredentials authCredentials) {
231+
final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, "");
232+
wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Bearer realm=\"OpenSearch Security\"");
233+
channel.sendResponse(wwwAuthenticateResponse);
234+
return true;
235+
}
239236

240237
public String getRquiredAudience() {
241238
return requiredAudience;

0 commit comments

Comments
 (0)