|
43 | 43 | import org.opensearch.security.user.AuthCredentials; |
44 | 44 |
|
45 | 45 | 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; |
58 | 58 | private final String requiredAudience; |
59 | 59 | private final String requiredIssuer; |
60 | 60 |
|
61 | 61 | 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); |
71 | 71 | requiredAudience = settings.get("required_audience"); |
72 | 72 | requiredIssuer = settings.get("required_issuer"); |
73 | 73 |
|
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); |
77 | 77 |
|
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(); |
83 | 133 |
|
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 | + } |
89 | 137 |
|
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 | + } |
239 | 236 |
|
240 | 237 | public String getRquiredAudience() { |
241 | 238 | return requiredAudience; |
|
0 commit comments