Skip to content

Commit f5cb802

Browse files
committed
Allow clients to provide an existing JWT to the API. With this option it is possible to create multiple API instances which share the same JWT token. Reduced access level for some methods and constructors based on their current usage.
1 parent 3010e77 commit f5cb802

File tree

5 files changed

+112
-79
lines changed

5 files changed

+112
-79
lines changed

src/main/java/com/github/m0nk3y2k4/thetvdb/api/TheTVDBApi.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import javax.annotation.Nonnull;
99
import java.util.List;
1010
import java.util.Map;
11+
import java.util.Optional;
1112

1213
public interface TheTVDBApi {
1314

@@ -20,6 +21,27 @@ public interface TheTVDBApi {
2021
*/
2122
void init() throws APIException;
2223

24+
/**
25+
* Initializes the current API with the given token. This token will be used for authentication of all requests that are sent to the remote service by this API instance.
26+
* The given string must be a valid Base64 encoded token in the regular JWT format <i>"{header}.{payload}.{signature}"</i>.
27+
* <p/>
28+
* If the given token is (or becomes) expired it will be replaced by a new JWT automatically. The new token will be requested from the remove service based
29+
* on the constructor parameters used to create this API instance.
30+
*
31+
* @param token JSON Web Token to be used for remote API communication/authorization
32+
*
33+
* @throws APIException If the given string does not match the JSON Web Token format
34+
*/
35+
void init(@Nonnull String token) throws APIException;
36+
37+
/**
38+
* Returns the JSON Web Token used for authentication of all requests that are sent to the remote service by this API instance. If the current API has not yet been
39+
* initialized an empty <i>Optional</i> instance will be returned.
40+
*
41+
* @return The JWT used by this API or an empty <i>Optional</i> if the API has not been initialized
42+
*/
43+
Optional<String> getToken();
44+
2345
/**
2446
* Sets the preferred language to be used for communication with the remote service. Some of the API calls might use this setting in order to only return results that
2547
* match the given language. If available, the data returned by the remote API will be translated to the given language. The default language code is <b>"en"</b>. For a list

src/main/java/com/github/m0nk3y2k4/thetvdb/internal/api/impl/TheTVDBApiImpl.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.List;
44
import java.util.Map;
5+
import java.util.Optional;
56

67
import javax.annotation.Nonnull;
78

@@ -81,6 +82,16 @@ public void init() throws APIException {
8182
this.login();
8283
}
8384

85+
@Override
86+
public void init(@Nonnull String token) throws APIException {
87+
con.setToken(token);
88+
}
89+
90+
@Override
91+
public Optional<String> getToken() {
92+
return con.getToken();
93+
}
94+
8495
@Override
8596
public void setLanguage(String languageCode) {
8697
con.setLanguage(languageCode);

src/main/java/com/github/m0nk3y2k4/thetvdb/internal/connection/APIConnection.java

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ public JsonNode sendPUT(@Nonnull String resource) throws APIException {
6464
return sendRequest(new PutRequest(resource));
6565
}
6666

67+
public void setToken(@Nonnull String token) throws APIException {
68+
session.setToken(token);
69+
}
70+
71+
public void setStatus(Status status) {
72+
session.setStatus(status);
73+
}
74+
75+
public void setLanguage(String language) {
76+
session.setLanguage(language);
77+
}
78+
6779
public String getApiKey() {
6880
return session.getApiKey();
6981
}
@@ -76,20 +88,12 @@ public Optional<String> getUserName() {
7688
return session.getUserName();
7789
}
7890

79-
public boolean userAuthentication() {
80-
return session.userAuthentication();
91+
public Optional<String> getToken() {
92+
return session.getToken();
8193
}
8294

83-
public void setStatus(Status status) {
84-
session.setStatus(status);
85-
}
86-
87-
public void setToken(String token) {
88-
session.setToken(token);
89-
}
90-
91-
public void setLanguage(String language) {
92-
this.session.setLanguage(language);
95+
public boolean userAuthentication() {
96+
return session.userAuthentication();
9397
}
9498

9599
private synchronized JsonNode sendRequest(APIRequest request) throws APIException {
@@ -110,10 +114,8 @@ private synchronized JsonNode sendRequest(APIRequest request) throws APIExceptio
110114
private void authorizeSession() throws APIException {
111115
switch (session.getStatus()) {
112116
case NOT_AUTHORIZED:
113-
AuthenticationAPI.login(this); // Request a new token
114-
break;
115117
case AUTHORIZED:
116-
AuthenticationAPI.refreshSession(this); // Refresh the existing token
118+
AuthenticationAPI.login(this); // Not yet authorized or authorization expired: Request a new token
117119
break;
118120
default:
119121
// Authorization is already in progress but could not be completed. Do not retry to authorize this session
@@ -151,7 +153,7 @@ abstract class APIRequest {
151153
this.resource = resource;
152154
}
153155

154-
public void setSession(@Nonnull APISession session) {
156+
void setSession(@Nonnull APISession session) {
155157
this.session = session;
156158
}
157159

@@ -167,7 +169,7 @@ void openConnection(@Nonnull String resource, @Nonnull String requestMethod) thr
167169
con.setRequestProperty("User-Agent", USER_AGENT);
168170
if (session != null && session.isInitialized()) {
169171
// If session has already been initialized, add token information and language key to each request
170-
con.setRequestProperty("Authorization", "Bearer " + session.getToken());
172+
con.setRequestProperty("Authorization", "Bearer " + session.getToken().get());
171173
con.setRequestProperty("Accept-Language", session.getLanguage());
172174
}
173175
}
@@ -212,20 +214,20 @@ private String getError() throws IOException {
212214
return parseResponse(con.getErrorStream()).get(API_ERROR).asText("");
213215
}
214216

215-
public abstract JsonNode send() throws APIException;
217+
abstract JsonNode send() throws APIException;
216218
}
217219

218220
final class GetRequest extends APIRequest {
219221

220222
/** Messages for error/exception handling */
221223
private static final String ERR_GET = "An exception occurred while sending GET request to API";
222224

223-
public GetRequest(@Nonnull String resource) {
225+
GetRequest(@Nonnull String resource) {
224226
super(resource);
225227
}
226228

227229
@Override
228-
public JsonNode send() throws APIException {
230+
JsonNode send() throws APIException {
229231
try {
230232
openConnection(resource, "GET");
231233

@@ -244,13 +246,13 @@ final class PostRequest extends APIRequest {
244246

245247
private final String data;
246248

247-
public PostRequest(@Nonnull String resource, @Nonnull String data) {
249+
PostRequest(@Nonnull String resource, @Nonnull String data) {
248250
super(resource);
249251
this.data = data;
250252
}
251253

252254
@Override
253-
public JsonNode send() throws APIException {
255+
JsonNode send() throws APIException {
254256
try {
255257
openConnection(resource, "POST");
256258

@@ -279,12 +281,12 @@ final class HeadRequest extends APIRequest {
279281
/** Messages for error/exception handling */
280282
private static final String ERR_HEAD = "An exception occurred while sending HEAD request to API";
281283

282-
public HeadRequest(@Nonnull String resource) {
284+
HeadRequest(@Nonnull String resource) {
283285
super(resource);
284286
}
285287

286288
@Override
287-
public JsonNode send() throws APIException {
289+
JsonNode send() throws APIException {
288290
try {
289291
openConnection(resource, "HEAD");
290292

@@ -327,12 +329,12 @@ final class DeleteRequest extends APIRequest {
327329
/** Messages for error/exception handling */
328330
private static final String ERR_DELETE = "An exception occurred while sending DELETE request to API";
329331

330-
public DeleteRequest(@Nonnull String resource) {
332+
DeleteRequest(@Nonnull String resource) {
331333
super(resource);
332334
}
333335

334336
@Override
335-
public JsonNode send() throws APIException {
337+
JsonNode send() throws APIException {
336338
try {
337339
openConnection(resource, "DELETE");
338340

@@ -349,12 +351,12 @@ final class PutRequest extends APIRequest {
349351
/** Messages for error/exception handling */
350352
private static final String ERR_PUT = "An exception occurred while sending PUT request to API";
351353

352-
public PutRequest(@Nonnull String resource) {
354+
PutRequest(@Nonnull String resource) {
353355
super(resource);
354356
}
355357

356358
@Override
357-
public JsonNode send() throws APIException {
359+
JsonNode send() throws APIException {
358360
try {
359361
openConnection(resource, "PUT");
360362

src/main/java/com/github/m0nk3y2k4/thetvdb/internal/connection/APISession.java

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.github.m0nk3y2k4.thetvdb.internal.connection;
22

3+
import com.github.m0nk3y2k4.thetvdb.api.exception.APIException;
34
import com.github.m0nk3y2k4.thetvdb.internal.util.APIUtil;
45

56
import javax.annotation.Nonnull;
67
import java.util.Objects;
78
import java.util.Optional;
9+
import java.util.regex.Pattern;
810

911
/**
1012
* Session used for remote API communication. All connections to the TheTVDB API are backed by an instance of this class. These sessions
@@ -13,6 +15,13 @@
1315
*/
1416
public final class APISession {
1517

18+
/** Pattern for JSON Web Token validation */
19+
private static final Pattern JWT_PATTERN = Pattern.compile("^[A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*$");
20+
21+
/** Error messages */
22+
private static final String ERR_JWT_EMPTY = "Remote API authorization failed: Token must not be NULL or empty";
23+
private static final String ERR_JWT_INVALID = "Remote API authorization failed: Invalid token format [%s]";
24+
1625
/**
1726
* Represents the different states of a session. By default, sessions are not authorized for general API communication. Only
1827
* login/refresh requests may be allowed. During the execution of these kind of requests, the session authorization is in progress.
@@ -52,7 +61,7 @@ public enum Status {NOT_AUTHORIZED, AUTHORIZATION_IN_PROGRESS, AUTHORIZED}
5261
*
5362
* @param apiKey The API key used to request a session token
5463
*/
55-
public APISession(@Nonnull String apiKey) {
64+
APISession(@Nonnull String apiKey) {
5665
Objects.requireNonNull(apiKey, "API key must not be NULL or empty!");
5766

5867
this.apiKey = apiKey;
@@ -71,7 +80,7 @@ public APISession(@Nonnull String apiKey) {
7180
* @param userKey User key for authentication (also referred to as "Unique ID")
7281
* @param userName User name for authentication
7382
*/
74-
public APISession(@Nonnull String apiKey, @Nonnull String userKey, @Nonnull String userName) {
83+
APISession(@Nonnull String apiKey, @Nonnull String userKey, @Nonnull String userName) {
7584
Objects.requireNonNull(apiKey, "API key must not be NULL or empty!");
7685
Objects.requireNonNull(userKey, "User key must not be NULL or empty!");
7786
Objects.requireNonNull(userName, "User name must not be NULL or empty!");
@@ -86,7 +95,7 @@ public APISession(@Nonnull String apiKey, @Nonnull String userKey, @Nonnull Stri
8695
*
8796
* @return API key of the session
8897
*/
89-
public String getApiKey() {
98+
String getApiKey() {
9099
return this.apiKey;
91100
}
92101

@@ -95,7 +104,7 @@ public String getApiKey() {
95104
*
96105
* @return The user key
97106
*/
98-
public Optional<String> getUserKey() {
107+
Optional<String> getUserKey() {
99108
return Optional.ofNullable(userKey);
100109
}
101110

@@ -104,36 +113,38 @@ public Optional<String> getUserKey() {
104113
*
105114
* @return The user name
106115
*/
107-
public Optional<String> getUserName() {
116+
Optional<String> getUserName() {
108117
return Optional.ofNullable(userName);
109118
}
110119

111120
/**
112-
* Sets the token of this session
121+
* Returns the current session token. Might be empty if the session has not yet been initialized.
113122
*
114-
* @param token The new session token
123+
* @return Current API session token
115124
*/
116-
public void setToken(@Nonnull String token) {
117-
Objects.requireNonNull(token, "Token must not be NULL or empty!");
118-
119-
this.token = token;
125+
Optional<String> getToken() {
126+
return Optional.ofNullable(token);
120127
}
121128

122129
/**
123-
* Returns the current session token
130+
* Sets the token of this session
124131
*
125-
* @return Current API session token
132+
* @param token The new session token
126133
*/
127-
public String getToken() {
128-
return token;
134+
void setToken(@Nonnull String token) throws APIException {
135+
// Validate token - throws an exception if not a valid JWT
136+
validateJWT(token);
137+
138+
this.token = token;
139+
this.status = Status.AUTHORIZED;
129140
}
130141

131142
/**
132143
* Set the preferred language used for API communication. Search results will be based on this language.
133144
*
134145
* @param language The language for API communication
135146
*/
136-
public void setLanguage(String language) {
147+
void setLanguage(String language) {
137148
this.language = language;
138149
}
139150

@@ -142,7 +153,7 @@ public void setLanguage(String language) {
142153
*
143154
* @return The language used for API communication
144155
*/
145-
public String getLanguage() {
156+
String getLanguage() {
146157
return language;
147158
}
148159

@@ -151,7 +162,7 @@ public String getLanguage() {
151162
*
152163
* @param status The new session status
153164
*/
154-
public void setStatus(Status status) { this.status = status; }
165+
void setStatus(Status status) { this.status = status; }
155166
/**
156167
* Returns the current {@link Status} of this session. This status indicates that...
157168
* <p/>
@@ -161,14 +172,14 @@ public String getLanguage() {
161172
*
162173
* @return The current status of this session
163174
*/
164-
public Status getStatus() { return status; }
175+
Status getStatus() { return status; }
165176

166177
/**
167178
* Check if this session has already been initialized
168179
*
169180
* @return {@link Boolean#TRUE} if the session is initialized or {@link Boolean#FALSE} if the session is not yet initialized.
170181
*/
171-
public Boolean isInitialized() {
182+
Boolean isInitialized() {
172183
return getStatus() == Status.AUTHORIZED;
173184
}
174185

@@ -177,7 +188,24 @@ public Boolean isInitialized() {
177188
*
178189
* @return {@link Boolean#TRUE} if both, userKey and userName are not empty or {@link Boolean#FALSE} if not.
179190
*/
180-
public Boolean userAuthentication() {
191+
Boolean userAuthentication() {
181192
return APIUtil.hasValue(userKey) && APIUtil.hasValue(userName);
182193
}
194+
195+
/**
196+
* Checks if the given token is a valid JSON Web Token
197+
*
198+
* @param token The token to check
199+
*
200+
* @throws APIException If the given token is <code>null</code>, an empty character sequence or does not match the regular JWT format
201+
*/
202+
private static void validateJWT(String token) throws APIException {
203+
if (APIUtil.hasNoValue(token)) {
204+
throw new APIException(ERR_JWT_EMPTY);
205+
}
206+
207+
if (!JWT_PATTERN.matcher(token).matches()) {
208+
throw new APIException(String.format(ERR_JWT_INVALID, token));
209+
}
210+
}
183211
}

0 commit comments

Comments
 (0)