|
21 | 21 | import java.net.http.HttpClient;
|
22 | 22 | import java.net.http.HttpRequest;
|
23 | 23 | import java.net.http.HttpResponse;
|
| 24 | +import java.nio.charset.StandardCharsets; |
24 | 25 | import java.security.KeyPair;
|
25 | 26 | import java.security.KeyPairGenerator;
|
26 | 27 | import java.security.MessageDigest;
|
|
38 | 39 | import java.util.regex.Pattern;
|
39 | 40 | import javax.servlet.http.HttpSession;
|
40 | 41 | import jenkins.model.Jenkins;
|
| 42 | +import jenkins.security.ApiTokenProperty; |
41 | 43 | import jenkins.security.LastGrantedAuthoritiesProperty;
|
42 | 44 | import org.hamcrest.MatcherAssert;
|
43 | 45 | import org.htmlunit.html.HtmlPage;
|
|
67 | 69 | import static com.github.tomakehurst.wiremock.client.WireMock.verify;
|
68 | 70 | import static com.google.gson.JsonParser.parseString;
|
69 | 71 | import static org.hamcrest.MatcherAssert.assertThat;
|
| 72 | +import static org.hamcrest.Matchers.containsString; |
70 | 73 | import static org.hamcrest.Matchers.empty;
|
71 | 74 | import static org.hamcrest.Matchers.is;
|
72 | 75 | import static org.jenkinsci.plugins.oic.TestRealm.AUTO_CONFIG_FIELD;
|
@@ -367,6 +370,37 @@ private HttpResponse<String> getPageWithGet(String url) throws IOException, Inte
|
367 | 370 | HttpResponse.BodyHandlers.ofString());
|
368 | 371 | }
|
369 | 372 |
|
| 373 | + /** |
| 374 | + * performs a GET request using a basic authorization header |
| 375 | + * @param user - The user id |
| 376 | + * @param token - the password api token to user |
| 377 | + * @param url - the url to request |
| 378 | + * @return HttpResponse |
| 379 | + * @throws IOException |
| 380 | + * @throws InterruptedException |
| 381 | + */ |
| 382 | + private HttpResponse<String> getPageWithGet(String user, String token, String url) |
| 383 | + throws IOException, InterruptedException { |
| 384 | + // fix up the url, if needed |
| 385 | + if (url.startsWith("/")) { |
| 386 | + url = url.substring(1); |
| 387 | + } |
| 388 | + |
| 389 | + HttpClient c = HttpClient.newBuilder() |
| 390 | + .followRedirects(HttpClient.Redirect.ALWAYS) |
| 391 | + .build(); |
| 392 | + return c.send( |
| 393 | + HttpRequest.newBuilder(URI.create(jenkinsRule.getURL() + url)) |
| 394 | + .header( |
| 395 | + "Authorization", |
| 396 | + "Basic " |
| 397 | + + Base64.getEncoder() |
| 398 | + .encodeToString((user + ":" + token).getBytes(StandardCharsets.UTF_8))) |
| 399 | + .GET() |
| 400 | + .build(), |
| 401 | + HttpResponse.BodyHandlers.ofString()); |
| 402 | + } |
| 403 | + |
370 | 404 | @Test
|
371 | 405 | public void testRefreshTokenAndTokenExpiration_withoutRefreshToken() throws Exception {
|
372 | 406 | mockAuthorizationRedirectsToFinishLogin();
|
@@ -948,6 +982,56 @@ public void loginWithCheckTokenFailure() throws Exception {
|
948 | 982 | assertAnonymous();
|
949 | 983 | }
|
950 | 984 |
|
| 985 | + @Test |
| 986 | + public void testAccessUsingJenkinsApiTokens() throws Exception { |
| 987 | + mockAuthorizationRedirectsToFinishLogin(); |
| 988 | + configureWellKnown(null, null, "authorization_code"); |
| 989 | + jenkins.setSecurityRealm(new TestRealm(wireMockRule, null, EMAIL_FIELD, GROUPS_FIELD, AUTO_CONFIG_FIELD)); |
| 990 | + // explicitly ensure allowTokenAccessWithoutOicSession is disabled |
| 991 | + TestRealm testRealm = (TestRealm) jenkins.getSecurityRealm(); |
| 992 | + testRealm.setAllowTokenAccessWithoutOicSession(false); |
| 993 | + |
| 994 | + // login and assert normal auth is working |
| 995 | + mockTokenReturnsIdTokenWithGroup(PluginTest::withoutRefreshToken); |
| 996 | + mockUserInfoWithTestGroups(); |
| 997 | + browseLoginPage(); |
| 998 | + assertTestUser(); |
| 999 | + |
| 1000 | + // create a jenkins api token for the test user |
| 1001 | + String token = User.getById(TEST_USER_USERNAME, false) |
| 1002 | + .getProperty(ApiTokenProperty.class) |
| 1003 | + .generateNewToken("foo") |
| 1004 | + .plainValue; |
| 1005 | + |
| 1006 | + // validate that the token can be used |
| 1007 | + HttpResponse<String> rsp = getPageWithGet(TEST_USER_USERNAME, token, "/whoAmI/api/xml"); |
| 1008 | + MatcherAssert.assertThat("response should have been 200\n" + rsp.body(), rsp.statusCode(), is(200)); |
| 1009 | + |
| 1010 | + MatcherAssert.assertThat( |
| 1011 | + "response should have been 200\n" + rsp.body(), |
| 1012 | + rsp.body(), |
| 1013 | + containsString("<authenticated>true</authenticated>")); |
| 1014 | + |
| 1015 | + // expired oic session tokens, do not refreshed |
| 1016 | + expire(); |
| 1017 | + |
| 1018 | + // the default behavior expects there to be a valid oic session, so token based |
| 1019 | + // access should now fail (unauthorized) |
| 1020 | + rsp = getPageWithGet(TEST_USER_USERNAME, token, "/whoAmI/api/xml"); |
| 1021 | + MatcherAssert.assertThat("response should have been 401\n" + rsp.body(), rsp.statusCode(), is(401)); |
| 1022 | + |
| 1023 | + // enable "traditional api token access" |
| 1024 | + testRealm.setAllowTokenAccessWithoutOicSession(true); |
| 1025 | + |
| 1026 | + // verify that jenkins api token is now working again |
| 1027 | + rsp = getPageWithGet(TEST_USER_USERNAME, token, "/whoAmI/api/xml"); |
| 1028 | + MatcherAssert.assertThat("response should have been 200\n" + rsp.body(), rsp.statusCode(), is(200)); |
| 1029 | + MatcherAssert.assertThat( |
| 1030 | + "response should have been 200\n" + rsp.body(), |
| 1031 | + rsp.body(), |
| 1032 | + containsString("<authenticated>true</authenticated>")); |
| 1033 | + } |
| 1034 | + |
951 | 1035 | private static @NonNull Consumer<OicSecurityRealm> belongsToGroup(String groupName) {
|
952 | 1036 | return sc -> {
|
953 | 1037 | sc.setTokenFieldToCheckKey("contains(groups, '" + groupName + "')");
|
|
0 commit comments