|
20 | 20 | import java.time.Instant; |
21 | 21 | import java.util.Arrays; |
22 | 22 | import java.util.Base64; |
23 | | -import java.util.HashMap; |
24 | | -import java.util.Map; |
| 23 | +import java.util.List; |
25 | 24 | import java.util.Optional; |
26 | 25 |
|
27 | | -import net.minidev.json.JSONArray; |
28 | | -import net.minidev.json.JSONObject; |
| 26 | +import okhttp3.HttpUrl; |
29 | 27 | import okhttp3.mockwebserver.Dispatcher; |
30 | 28 | import okhttp3.mockwebserver.MockResponse; |
31 | 29 | import okhttp3.mockwebserver.MockWebServer; |
|
36 | 34 |
|
37 | 35 | import org.springframework.core.convert.converter.Converter; |
38 | 36 | import org.springframework.http.HttpHeaders; |
| 37 | +import org.springframework.http.HttpMethod; |
39 | 38 | import org.springframework.http.HttpStatus; |
40 | 39 | import org.springframework.http.MediaType; |
41 | 40 | import org.springframework.http.RequestEntity; |
|
47 | 46 | import static org.assertj.core.api.Assertions.assertThat; |
48 | 47 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
49 | 48 | import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
50 | | -import static org.assertj.core.api.Assumptions.assumeThat; |
51 | | -import static org.mockito.ArgumentMatchers.any; |
52 | | -import static org.mockito.ArgumentMatchers.eq; |
53 | 49 | import static org.mockito.BDDMockito.given; |
54 | 50 | import static org.mockito.Mockito.mock; |
55 | 51 | import static org.mockito.Mockito.verify; |
@@ -122,16 +118,6 @@ public class NimbusOpaqueTokenIntrospectorTests { |
122 | 118 | + " }"; |
123 | 119 | // @formatter:on |
124 | 120 |
|
125 | | - private static final ResponseEntity<String> ACTIVE = response(ACTIVE_RESPONSE); |
126 | | - |
127 | | - private static final ResponseEntity<String> INACTIVE = response(INACTIVE_RESPONSE); |
128 | | - |
129 | | - private static final ResponseEntity<String> INVALID = response(INVALID_RESPONSE); |
130 | | - |
131 | | - private static final ResponseEntity<String> MALFORMED_ISSUER = response(MALFORMED_ISSUER_RESPONSE); |
132 | | - |
133 | | - private static final ResponseEntity<String> MALFORMED_SCOPE = response(MALFORMED_SCOPE_RESPONSE); |
134 | | - |
135 | 121 | @Test |
136 | 122 | public void introspectWhenActiveTokenThenOk() throws Exception { |
137 | 123 | try (MockWebServer server = new MockWebServer()) { |
@@ -170,96 +156,116 @@ public void introspectWhenBadClientCredentialsThenError() throws IOException { |
170 | 156 | } |
171 | 157 |
|
172 | 158 | @Test |
173 | | - public void introspectWhenInactiveTokenThenInvalidToken() { |
174 | | - RestOperations restOperations = mock(RestOperations.class); |
175 | | - OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL, |
176 | | - restOperations); |
177 | | - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(INACTIVE); |
178 | | - // @formatter:off |
179 | | - assertThatExceptionOfType(OAuth2IntrospectionException.class) |
180 | | - .isThrownBy(() -> introspectionClient.introspect("token")) |
181 | | - .withMessage("Provided token isn't active"); |
182 | | - // @formatter:on |
| 159 | + public void introspectWhenInactiveTokenThenInvalidToken() throws IOException { |
| 160 | + try (MockWebServer server = new MockWebServer()) { |
| 161 | + server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, INACTIVE_RESPONSE)); |
| 162 | + String introspectUri = server.url("/introspect").toString(); |
| 163 | + OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID, |
| 164 | + CLIENT_SECRET); |
| 165 | + // @formatter:off |
| 166 | + assertThatExceptionOfType(OAuth2IntrospectionException.class) |
| 167 | + .isThrownBy(() -> introspectionClient.introspect("token")) |
| 168 | + .withMessage("Provided token isn't active"); |
| 169 | + // @formatter:on |
| 170 | + } |
183 | 171 | } |
184 | 172 |
|
185 | 173 | @Test |
186 | | - public void introspectWhenActiveTokenThenParsesValuesInResponse() { |
187 | | - Map<String, Object> introspectedValues = new HashMap<>(); |
188 | | - introspectedValues.put(OAuth2TokenIntrospectionClaimNames.ACTIVE, true); |
189 | | - introspectedValues.put(OAuth2TokenIntrospectionClaimNames.AUD, Arrays.asList("aud")); |
190 | | - introspectedValues.put(OAuth2TokenIntrospectionClaimNames.NBF, 29348723984L); |
191 | | - RestOperations restOperations = mock(RestOperations.class); |
192 | | - OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL, |
193 | | - restOperations); |
194 | | - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) |
195 | | - .willReturn(response(new JSONObject(introspectedValues).toJSONString())); |
196 | | - OAuth2AuthenticatedPrincipal authority = introspectionClient.introspect("token"); |
197 | | - // @formatter:off |
198 | | - assertThat(authority.getAttributes()) |
199 | | - .isNotNull() |
200 | | - .containsEntry(OAuth2TokenIntrospectionClaimNames.ACTIVE, true) |
201 | | - .containsEntry(OAuth2TokenIntrospectionClaimNames.AUD, Arrays.asList("aud")) |
202 | | - .containsEntry(OAuth2TokenIntrospectionClaimNames.NBF, Instant.ofEpochSecond(29348723984L)) |
203 | | - .doesNotContainKey(OAuth2TokenIntrospectionClaimNames.CLIENT_ID) |
204 | | - .doesNotContainKey(OAuth2TokenIntrospectionClaimNames.SCOPE); |
205 | | - // @formatter:on |
| 174 | + public void introspectWhenActiveTokenThenParsesValuesInResponse() throws IOException { |
| 175 | + try (MockWebServer server = new MockWebServer()) { |
| 176 | + String introspectedValues = """ |
| 177 | + { |
| 178 | + "active": true, |
| 179 | + "aud": "aud", |
| 180 | + "nbf": 29348723984 |
| 181 | + } |
| 182 | + """; |
| 183 | + server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, introspectedValues)); |
| 184 | + String introspectUri = server.url("/introspect").toString(); |
| 185 | + OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID, |
| 186 | + CLIENT_SECRET); |
| 187 | + OAuth2AuthenticatedPrincipal authority = introspectionClient.introspect("token"); |
| 188 | + // @formatter:off |
| 189 | + assertThat(authority.getAttributes()) |
| 190 | + .isNotNull() |
| 191 | + .containsEntry(OAuth2TokenIntrospectionClaimNames.ACTIVE, true) |
| 192 | + .containsEntry(OAuth2TokenIntrospectionClaimNames.AUD, Arrays.asList("aud")) |
| 193 | + .containsEntry(OAuth2TokenIntrospectionClaimNames.NBF, Instant.ofEpochSecond(29348723984L)) |
| 194 | + .doesNotContainKey(OAuth2TokenIntrospectionClaimNames.CLIENT_ID) |
| 195 | + .doesNotContainKey(OAuth2TokenIntrospectionClaimNames.SCOPE); |
| 196 | + // @formatter:on |
| 197 | + } |
206 | 198 | } |
207 | 199 |
|
208 | 200 | @Test |
209 | | - public void introspectWhenIntrospectionEndpointThrowsExceptionThenInvalidToken() { |
210 | | - RestOperations restOperations = mock(RestOperations.class); |
211 | | - OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL, |
212 | | - restOperations); |
213 | | - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) |
214 | | - .willThrow(new IllegalStateException("server was unresponsive")); |
215 | | - // @formatter:off |
216 | | - assertThatExceptionOfType(OAuth2IntrospectionException.class) |
217 | | - .isThrownBy(() -> introspectionClient.introspect("token")) |
218 | | - .withMessage("server was unresponsive"); |
219 | | - // @formatter:on |
| 201 | + public void introspectWhenIntrospectionEndpointThrowsExceptionThenInvalidToken() throws IOException { |
| 202 | + try (MockWebServer server = new MockWebServer()) { |
| 203 | + server.setDispatcher(new Dispatcher() { |
| 204 | + @Override |
| 205 | + public MockResponse dispatch(final RecordedRequest request) { |
| 206 | + return new MockResponse().setResponseCode(500); |
| 207 | + } |
| 208 | + }); |
| 209 | + String introspectUri = server.url("/introspect").toString(); |
| 210 | + OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID, |
| 211 | + CLIENT_SECRET); |
| 212 | + // @formatter:off |
| 213 | + assertThatExceptionOfType(OAuth2IntrospectionException.class) |
| 214 | + .isThrownBy(() -> introspectionClient.introspect("token")) |
| 215 | + .withMessageContaining("500"); |
| 216 | + // @formatter:on |
| 217 | + } |
220 | 218 | } |
221 | 219 |
|
222 | 220 | @Test |
223 | | - public void introspectWhenIntrospectionEndpointReturnsMalformedResponseThenInvalidToken() { |
224 | | - RestOperations restOperations = mock(RestOperations.class); |
225 | | - OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL, |
226 | | - restOperations); |
227 | | - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(response("malformed")); |
228 | | - assertThatExceptionOfType(OAuth2IntrospectionException.class) |
229 | | - .isThrownBy(() -> introspectionClient.introspect("token")); |
| 221 | + public void introspectWhenIntrospectionEndpointReturnsMalformedResponseThenInvalidToken() throws IOException { |
| 222 | + try (MockWebServer server = new MockWebServer()) { |
| 223 | + server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, "malformed")); |
| 224 | + String introspectUri = server.url("/introspect").toString(); |
| 225 | + OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID, |
| 226 | + CLIENT_SECRET); |
| 227 | + assertThatExceptionOfType(OAuth2IntrospectionException.class) |
| 228 | + .isThrownBy(() -> introspectionClient.introspect("token")); |
| 229 | + } |
230 | 230 | } |
231 | 231 |
|
232 | 232 | @Test |
233 | | - public void introspectWhenIntrospectionTokenReturnsInvalidResponseThenInvalidToken() { |
234 | | - RestOperations restOperations = mock(RestOperations.class); |
235 | | - OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL, |
236 | | - restOperations); |
237 | | - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(INVALID); |
238 | | - assertThatExceptionOfType(OAuth2IntrospectionException.class) |
239 | | - .isThrownBy(() -> introspectionClient.introspect("token")); |
| 233 | + public void introspectWhenIntrospectionTokenReturnsInvalidResponseThenInvalidToken() throws IOException { |
| 234 | + try (MockWebServer server = new MockWebServer()) { |
| 235 | + server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, INVALID_RESPONSE)); |
| 236 | + String introspectUri = server.url("/introspect").toString(); |
| 237 | + OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID, |
| 238 | + CLIENT_SECRET); |
| 239 | + assertThatExceptionOfType(OAuth2IntrospectionException.class) |
| 240 | + .isThrownBy(() -> introspectionClient.introspect("token")); |
| 241 | + } |
240 | 242 | } |
241 | 243 |
|
242 | 244 | @Test |
243 | | - public void introspectWhenIntrospectionTokenReturnsMalformedIssuerResponseThenInvalidToken() { |
244 | | - RestOperations restOperations = mock(RestOperations.class); |
245 | | - OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL, |
246 | | - restOperations); |
247 | | - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(MALFORMED_ISSUER); |
248 | | - assertThatExceptionOfType(OAuth2IntrospectionException.class) |
249 | | - .isThrownBy(() -> introspectionClient.introspect("token")); |
| 245 | + public void introspectWhenIntrospectionTokenReturnsMalformedIssuerResponseThenInvalidToken() throws IOException { |
| 246 | + try (MockWebServer server = new MockWebServer()) { |
| 247 | + server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, MALFORMED_ISSUER_RESPONSE)); |
| 248 | + String introspectUri = server.url("/introspect").toString(); |
| 249 | + OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID, |
| 250 | + CLIENT_SECRET); |
| 251 | + assertThatExceptionOfType(OAuth2IntrospectionException.class) |
| 252 | + .isThrownBy(() -> introspectionClient.introspect("token")); |
| 253 | + } |
250 | 254 | } |
251 | 255 |
|
252 | 256 | // gh-7563 |
253 | 257 | @Test |
254 | | - public void introspectWhenIntrospectionTokenReturnsMalformedScopeThenEmptyAuthorities() { |
255 | | - RestOperations restOperations = mock(RestOperations.class); |
256 | | - OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL, |
257 | | - restOperations); |
258 | | - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(MALFORMED_SCOPE); |
259 | | - OAuth2AuthenticatedPrincipal principal = introspectionClient.introspect("token"); |
260 | | - assertThat(principal.getAuthorities()).isEmpty(); |
261 | | - JSONArray scope = principal.getAttribute("scope"); |
262 | | - assertThat(scope).containsExactly("read", "write", "dolphin"); |
| 258 | + public void introspectWhenIntrospectionTokenReturnsMalformedScopeThenEmptyAuthorities() throws IOException { |
| 259 | + try (MockWebServer server = new MockWebServer()) { |
| 260 | + server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, MALFORMED_SCOPE_RESPONSE)); |
| 261 | + String introspectUri = server.url("/introspect").toString(); |
| 262 | + OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID, |
| 263 | + CLIENT_SECRET); |
| 264 | + OAuth2AuthenticatedPrincipal principal = introspectionClient.introspect("token"); |
| 265 | + assertThat(principal.getAuthorities()).isEmpty(); |
| 266 | + List<String> scope = principal.getAttribute("scope"); |
| 267 | + assertThat(scope).containsExactly("read", "write", "dolphin"); |
| 268 | + } |
263 | 269 | } |
264 | 270 |
|
265 | 271 | @Test |
@@ -297,50 +303,58 @@ public void setRequestEntityConverterWhenConverterIsNullThenExceptionIsThrown() |
297 | 303 |
|
298 | 304 | @SuppressWarnings("unchecked") |
299 | 305 | @Test |
300 | | - public void setRequestEntityConverterWhenNonNullConverterGivenThenConverterUsed() { |
301 | | - RestOperations restOperations = mock(RestOperations.class); |
302 | | - Converter<String, RequestEntity<?>> requestEntityConverter = mock(Converter.class); |
303 | | - RequestEntity requestEntity = mock(RequestEntity.class); |
304 | | - String tokenToIntrospect = "some token"; |
305 | | - given(requestEntityConverter.convert(tokenToIntrospect)).willReturn(requestEntity); |
306 | | - given(restOperations.exchange(requestEntity, String.class)).willReturn(ACTIVE); |
307 | | - NimbusOpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL, |
308 | | - restOperations); |
309 | | - introspectionClient.setRequestEntityConverter(requestEntityConverter); |
310 | | - introspectionClient.introspect(tokenToIntrospect); |
311 | | - verify(requestEntityConverter).convert(tokenToIntrospect); |
| 306 | + public void setRequestEntityConverterWhenNonNullConverterGivenThenConverterUsed() throws IOException { |
| 307 | + try (MockWebServer server = new MockWebServer()) { |
| 308 | + server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, ACTIVE_RESPONSE)); |
| 309 | + HttpUrl introspectUri = server.url("/introspect"); |
| 310 | + Converter<String, RequestEntity<?>> requestEntityConverter = mock(Converter.class); |
| 311 | + RequestEntity requestEntity = new RequestEntity<>(HttpHeaders.EMPTY, HttpMethod.POST, introspectUri.uri()); |
| 312 | + String tokenToIntrospect = "some token"; |
| 313 | + given(requestEntityConverter.convert(tokenToIntrospect)).willReturn(requestEntity); |
| 314 | + NimbusOpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector( |
| 315 | + introspectUri.toString(), CLIENT_ID, CLIENT_SECRET); |
| 316 | + introspectionClient.setRequestEntityConverter(requestEntityConverter); |
| 317 | + introspectionClient.introspect(tokenToIntrospect); |
| 318 | + verify(requestEntityConverter).convert(tokenToIntrospect); |
| 319 | + } |
312 | 320 | } |
313 | 321 |
|
314 | 322 | @Test |
315 | | - public void handleMissingContentType() { |
316 | | - RestOperations restOperations = mock(RestOperations.class); |
317 | | - ResponseEntity<String> stubResponse = ResponseEntity.ok(ACTIVE_RESPONSE); |
318 | | - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(stubResponse); |
319 | | - OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL, |
320 | | - restOperations); |
321 | | - |
322 | | - // Protect against potential regressions where a default content type might be |
323 | | - // added by default. |
324 | | - assumeThat(stubResponse.getHeaders().getContentType()).isNull(); |
| 323 | + public void handleMissingContentType() throws IOException { |
| 324 | + try (MockWebServer server = new MockWebServer()) { |
| 325 | + server.setDispatcher(new Dispatcher() { |
| 326 | + @Override |
| 327 | + public MockResponse dispatch(final RecordedRequest request) { |
| 328 | + return ok(ACTIVE_RESPONSE).removeHeader(HttpHeaders.CONTENT_TYPE); |
| 329 | + } |
| 330 | + }); |
| 331 | + String introspectUri = server.url("/introspect").toString(); |
| 332 | + OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID, |
| 333 | + CLIENT_SECRET); |
325 | 334 |
|
326 | | - assertThatExceptionOfType(OAuth2IntrospectionException.class) |
327 | | - .isThrownBy(() -> introspectionClient.introspect("sometokenhere")); |
| 335 | + assertThatExceptionOfType(OAuth2IntrospectionException.class) |
| 336 | + .isThrownBy(() -> introspectionClient.introspect("sometokenhere")); |
| 337 | + } |
328 | 338 | } |
329 | 339 |
|
330 | 340 | @ParameterizedTest(name = "{displayName} when Content-Type={0}") |
331 | 341 | @ValueSource(strings = { MediaType.APPLICATION_CBOR_VALUE, MediaType.TEXT_MARKDOWN_VALUE, |
332 | 342 | MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_OCTET_STREAM_VALUE }) |
333 | | - public void handleNonJsonContentType(String type) { |
334 | | - RestOperations restOperations = mock(RestOperations.class); |
335 | | - ResponseEntity<String> stubResponse = ResponseEntity.ok() |
336 | | - .contentType(MediaType.parseMediaType(type)) |
337 | | - .body(ACTIVE_RESPONSE); |
338 | | - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(stubResponse); |
339 | | - OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL, |
340 | | - restOperations); |
| 343 | + public void handleNonJsonContentType(String type) throws IOException { |
| 344 | + try (MockWebServer server = new MockWebServer()) { |
| 345 | + server.setDispatcher(new Dispatcher() { |
| 346 | + @Override |
| 347 | + public MockResponse dispatch(final RecordedRequest request) { |
| 348 | + return ok(ACTIVE_RESPONSE).setHeader(HttpHeaders.CONTENT_TYPE, type); |
| 349 | + } |
| 350 | + }); |
| 351 | + String introspectUri = server.url("/introspect").toString(); |
| 352 | + OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID, |
| 353 | + CLIENT_SECRET); |
341 | 354 |
|
342 | | - assertThatExceptionOfType(OAuth2IntrospectionException.class) |
343 | | - .isThrownBy(() -> introspectionClient.introspect("sometokenhere")); |
| 355 | + assertThatExceptionOfType(OAuth2IntrospectionException.class) |
| 356 | + .isThrownBy(() -> introspectionClient.introspect("sometokenhere")); |
| 357 | + } |
344 | 358 | } |
345 | 359 |
|
346 | 360 | private static ResponseEntity<String> response(String content) { |
|
0 commit comments