Skip to content

Commit 2f6cedf

Browse files
authored
Merge pull request #10 from kbase/dev-new_client
Add validateToken method to auth client
2 parents 1cf7e33 + d48bd24 commit 2f6cedf

File tree

6 files changed

+235
-7
lines changed

6 files changed

+235
-7
lines changed

.github/workflows/test.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,18 @@ jobs:
3434
java-version: ${{matrix.java}}
3535

3636
- name: Run tests
37-
run: ./gradlew test
37+
env:
38+
KBASE_CI_TOKEN: ${{ secrets.KBASE_CI_TOKEN }}
39+
KBASE_CI_TOKEN2: ${{ secrets.KBASE_CI_TOKEN2 }}
40+
run: |
41+
cp test.cfg.example test.cfg
42+
sed -i "s#^auth_token1 =.*#auth_token1 = $KBASE_CI_TOKEN#" test.cfg
43+
sed -i "s#^auth_token2 =.*#auth_token2 = $KBASE_CI_TOKEN2#" test.cfg
44+
sed -i "s#^auth_user1 =.*#auth_user1 = kbase_bot#" test.cfg
45+
sed -i "s#^auth_user2 =.*#auth_user2 = sychan168#" test.cfg
46+
sed -i "s#^good_users =.*#good_users = kbasetest2 , kbasetest7 , kbasehelp#" test.cfg
47+
48+
./gradlew test
3849
3950
- name: Upload coverage to Codecov
4051
uses: codecov/codecov-action@v3

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ compileJava {
2727
}
2828

2929
test {
30-
systemProperty "AUTH2_TEST_CONFIG", "./test.cfg"
30+
systemProperty "test.cfg", "./test.cfg"
3131
testLogging {
3232
exceptionFormat = 'full'
3333
showStandardStreams = true
@@ -57,6 +57,7 @@ dependencies {
5757
implementation 'com.fasterxml.jackson.core:jackson-databind:2.5.4'
5858
implementation 'org.slf4j:slf4j-api:1.7.25'
5959

60+
testImplementation 'org.ini4j:ini4j:0.5.2'
6061
testImplementation 'junit:junit:4.12'
6162
testImplementation 'nl.jqno.equalsverifier:equalsverifier:3.1.10'
6263
testImplementation 'org.apache.commons:commons-lang3:3.1'

src/main/java/us/kbase/auth/client/AuthClient.java

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.io.Reader;
88
import java.net.HttpURLConnection;
99
import java.net.URI;
10+
import java.net.URISyntaxException;
1011
import java.nio.charset.StandardCharsets;
1112
import java.util.Map;
1213

@@ -15,6 +16,8 @@
1516
import com.fasterxml.jackson.databind.ObjectMapper;
1617

1718
import us.kbase.auth.AuthException;
19+
import us.kbase.auth.AuthToken;
20+
import us.kbase.auth.client.internal.TokenCache;
1821

1922
/** A client for the KBase Auth2 authentication server (https://github.com/kbase/auth2).
2023
*
@@ -27,6 +30,9 @@ public class AuthClient {
2730
// TODO CODE use the built in client in Java 11 when we drop java 8
2831

2932
private static final ObjectMapper MAPPER = new ObjectMapper();
33+
34+
// make this configurable? Add a builder if so
35+
private final TokenCache tokenCache = new TokenCache(1000, 2000); // same as old auth client
3036

3137
private final URI rootURI;
3238

@@ -47,19 +53,31 @@ private AuthClient(final URI auth2RootURI) throws IOException, AuthException {
4753
if (!"https".equals(auth2RootURI.getScheme())) {
4854
LoggerFactory.getLogger(getClass()).warn("auth root URI is insecure");
4955
}
50-
rootURI = auth2RootURI;
51-
final Map<String, Object> doc = request(rootURI);
56+
final Map<String, Object> doc = request(auth2RootURI);
5257
if (!"Authentication Service".equals(doc.get("servicename"))) {
5358
throw new AuthException(String.format(
54-
"Service at %s is not the authentication service", rootURI));
59+
"Service at %s is not the authentication service", auth2RootURI));
60+
}
61+
try {
62+
rootURI = new URI(auth2RootURI.toString() + "/").normalize();
63+
} catch (URISyntaxException e) {
64+
throw new RuntimeException("this should be impossible", e);
5565
}
5666
}
5767

5868
private Map<String, Object> request(final URI target) throws IOException, AuthException {
59-
// tried to use the Jersey client here but kept getting ssl handshake errors if I used
60-
// it more than once
69+
return request(target, null);
70+
}
71+
72+
private Map<String, Object> request(final URI target, final String token)
73+
throws IOException, AuthException {
74+
// tried to use the Jersey client here but kept getting ssl handshake errors if I made
75+
// more than one request
6176
final HttpURLConnection conn = (HttpURLConnection) target.toURL().openConnection();
6277
conn.addRequestProperty("Accept", "application/json");
78+
if (token != null) {
79+
conn.addRequestProperty("Authorization", token);
80+
}
6381
try {
6482
final int code = conn.getResponseCode();
6583
final String res = readResponse(conn, code != 200);
@@ -135,5 +153,30 @@ public String getServerVersion() throws IOException, AuthException {
135153
final Map<String, Object> doc = request(rootURI);
136154
return (String) doc.get("version");
137155
}
156+
157+
// TODO CODE could do a lot more here later w/ the return data from the auth server
158+
// - token type, custom expiration time, etc.
159+
160+
/** Validate a token and get name of the user that owns the token.
161+
* @param token the token.
162+
* @return an authtoken containing the token and the username.
163+
* @throws IOException if an IOException occurs communicating with the auth service.
164+
* @throws AuthException if an auth exception occurs communicating with the auth service.
165+
*/
166+
public AuthToken validateToken(final String token) throws IOException, AuthException {
167+
if (token == null || token.trim().isEmpty()) {
168+
throw new IllegalArgumentException("token must be a non-whitespace string");
169+
}
170+
final AuthToken t = tokenCache.getToken(token);
171+
if (t != null) {
172+
return t;
173+
}
174+
final URI target = rootURI.resolve("api/V2/token");
175+
final Map<String, Object> res = request(target, token.trim());
176+
// assume we're good at this point
177+
final AuthToken authToken = new AuthToken(token, (String) res.get("user"));
178+
tokenCache.putValidToken(authToken);
179+
return authToken;
180+
}
138181

139182
}

src/test/java/us/kbase/test/auth/client/AuthClientTest.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import ch.qos.logback.classic.spi.ILoggingEvent;
2020
import ch.qos.logback.core.read.ListAppender;
2121
import us.kbase.auth.AuthException;
22+
import us.kbase.auth.AuthToken;
2223
import us.kbase.auth.client.AuthClient;
2324
import us.kbase.test.common.TestCommon;
2425

@@ -121,4 +122,61 @@ public void getServerVersion() throws Exception {
121122
// don't anchor right side to allow for pre-release strings
122123
assertThat("not a semver", ver.matches("^\\d+\\.\\d+\\.\\d+"), is(true));
123124
}
125+
126+
@Test
127+
public void validateToken() throws Exception {
128+
final String token1 = TestCommon.getAuthToken1();
129+
final String user1 = TestCommon.getAuthUser1();
130+
final String token2 = TestCommon.getAuthToken2();
131+
final String user2 = TestCommon.getAuthUser2();
132+
assertThat("both tokens are the same", token1.equals(token2), is(false));
133+
assertThat("both users are the same", user1.equals(user2), is(false));
134+
135+
final AuthClient c = AuthClient.from(new URI(TestCommon.getAuthURI()));
136+
137+
// First time from service
138+
final AuthToken t = c.validateToken(token1);
139+
assertThat("incorrect user", t.getUserName(), is(user1)); // for easier debugging
140+
assertThat("incorrect auth token", t, is(new AuthToken(token1, user1)));
141+
142+
// Second time from cache. No way to actually verify that's what's happening though.
143+
// If we refactor to use the same client for every request, can inject the client
144+
// and mock it
145+
final AuthToken t2 = c.validateToken(token1);
146+
assertThat("incorrect auth token", t2, is(new AuthToken(token1, user1)));
147+
148+
// First time from service
149+
final AuthToken t3 = c.validateToken(token2);
150+
assertThat("incorrect user", t3.getUserName(), is(user2)); // for easier debugging
151+
assertThat("incorrect auth token", t3, is(new AuthToken(token2, user2)));
152+
153+
// Second time from cache.
154+
final AuthToken t4 = c.validateToken(token2);
155+
assertThat("incorrect auth token", t4, is(new AuthToken(token2, user2)));
156+
}
157+
158+
@Test
159+
public void validateTokenFailEmptyToken() throws Exception {
160+
final URI uri = new URI("https://ci.kbase.us/services/auth");
161+
validateTokenFail(uri, null, new IllegalArgumentException(
162+
"token must be a non-whitespace string"));
163+
validateTokenFail(uri, " \t ", new IllegalArgumentException(
164+
"token must be a non-whitespace string"));
165+
}
166+
167+
@Test
168+
public void validateTokenFailInvalidToken() throws Exception {
169+
final URI uri = new URI("https://ci.kbase.us/services/auth");
170+
validateTokenFail(uri, "faketoken", new AuthException(
171+
"Auth service returned an error: 10020 Invalid token"));
172+
}
173+
174+
private void validateTokenFail(final URI uri, final String token, final Exception expected) {
175+
try {
176+
AuthClient.from(uri).validateToken(token);
177+
fail("expected exception");
178+
} catch (Exception got) {
179+
TestCommon.assertExceptionCorrect(got, expected);
180+
}
181+
}
124182
}

src/test/java/us/kbase/test/common/TestCommon.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,100 @@
44
import static org.hamcrest.CoreMatchers.instanceOf;
55
import static org.junit.Assert.assertThat;
66

7+
import java.io.IOException;
8+
import java.nio.file.Path;
9+
import java.nio.file.Paths;
10+
import java.util.Arrays;
11+
import java.util.List;
12+
import java.util.Map;
13+
714
import org.apache.commons.lang3.exception.ExceptionUtils;
15+
import org.ini4j.Ini;
16+
817

918
public class TestCommon {
19+
20+
public static final String AUTH_URL = "auth_url";
21+
public static final String TOKEN1 = "auth_token1";
22+
public static final String TOKEN2 = "auth_token2";
23+
public static final String USER1 = "auth_user1";
24+
public static final String USER2 = "auth_user2";
25+
public static final String GOOD_USERS = "good_users";
26+
27+
public static final String TEST_CONFIG_FILE_PROP_NAME = "test.cfg";
28+
public static final String TEST_CONFIG_FILE_SECTION = "auth_client_test";
29+
30+
private static Map<String, String> testConfig;
1031

32+
public static String getAuthURI() {
33+
return getTestProperty(AUTH_URL);
34+
}
35+
36+
public static String getAuthToken1() {
37+
return getTestProperty(TOKEN1);
38+
}
39+
40+
public static String getAuthToken2() {
41+
return getTestProperty(TOKEN2);
42+
}
43+
44+
public static String getAuthUser1() {
45+
return getTestProperty(USER1);
46+
}
47+
48+
public static String getAuthUser2() {
49+
return getTestProperty(USER2);
50+
}
51+
52+
public static List<String> getGoodUsers() {
53+
return Arrays.asList(getTestProperty(GOOD_USERS).split(","));
54+
}
55+
56+
public static String getTestProperty(final String propertyKey, final boolean allowNull) {
57+
getTestConfig();
58+
final String prop = testConfig.get(propertyKey);
59+
if (!allowNull && (prop == null || prop.trim().isEmpty())) {
60+
throw new TestException(String.format(
61+
"Property %s in section %s of test file %s is missing",
62+
propertyKey, TEST_CONFIG_FILE_SECTION, getConfigFilePath()));
63+
}
64+
return prop;
65+
}
66+
67+
public static String getTestProperty(final String propertyKey) {
68+
return getTestProperty(propertyKey, false);
69+
}
70+
71+
private static void getTestConfig() {
72+
if (testConfig != null) {
73+
return;
74+
}
75+
final Path testCfgFilePath = getConfigFilePath();
76+
final Ini ini;
77+
try {
78+
ini = new Ini(testCfgFilePath.toFile());
79+
} catch (IOException ioe) {
80+
throw new TestException(String.format(
81+
"IO Error reading the test configuration file %s: %s",
82+
testCfgFilePath, ioe.getMessage()), ioe);
83+
}
84+
testConfig = ini.get(TEST_CONFIG_FILE_SECTION);
85+
if (testConfig == null) {
86+
throw new TestException(String.format("No section %s found in test config file %s",
87+
TEST_CONFIG_FILE_SECTION, testCfgFilePath));
88+
}
89+
}
90+
91+
private static Path getConfigFilePath() {
92+
final String testCfgFilePathStr = System.getProperty(TEST_CONFIG_FILE_PROP_NAME);
93+
if (testCfgFilePathStr == null || testCfgFilePathStr.trim().isEmpty()) {
94+
throw new TestException(String.format("Cannot get the test config file path." +
95+
" Ensure the java system property %s is set to the test config file location.",
96+
TEST_CONFIG_FILE_PROP_NAME));
97+
}
98+
return Paths.get(testCfgFilePathStr).toAbsolutePath().normalize();
99+
}
100+
11101
public static void assertExceptionCorrect(
12102
final Throwable got,
13103
final Throwable expected) {
@@ -17,5 +107,19 @@ public static void assertExceptionCorrect(
17107
is(expected.getLocalizedMessage()));
18108
assertThat("incorrect exception type", got, instanceOf(expected.getClass()));
19109
}
110+
111+
public static class TestException extends RuntimeException {
112+
113+
private static final long serialVersionUID = 1L;
114+
115+
116+
public TestException(final String message) {
117+
super(message);
118+
}
119+
120+
public TestException(final String message, final Throwable cause) {
121+
super(message, cause);
122+
}
123+
}
20124

21125
}

test.cfg.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[auth_client_test]
2+
3+
auth_url = https://ci.kbase.us/services/auth/
4+
5+
auth_token1 = <token goes here>
6+
auth_user1 = <user name goes here>
7+
8+
auth_token2 = <token goes here>
9+
auth_user2 = <user name goes here>
10+
11+
good_users = <comma separated list of user names>

0 commit comments

Comments
 (0)