Skip to content

Commit f93c1a5

Browse files
author
Madhav Chhura
committed
remove jwt from client token
1 parent b193615 commit f93c1a5

File tree

5 files changed

+278
-155
lines changed

5 files changed

+278
-155
lines changed

src/main/java/com/opentok/OpenTok.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.opentok.exception.InvalidArgumentException;
1414
import com.opentok.exception.OpenTokException;
1515
import com.opentok.exception.RequestException;
16+
import com.opentok.util.Crypto;
1617
import com.opentok.util.HttpClient;
1718
import com.opentok.util.TokenGenerator;
1819
import org.xml.sax.InputSource;
@@ -22,8 +23,10 @@
2223
import javax.xml.xpath.XPathFactory;
2324
import java.io.IOException;
2425
import java.io.StringReader;
26+
import java.io.UnsupportedEncodingException;
2527
import java.net.Proxy;
2628
import java.util.Collection;
29+
import java.util.List;
2730
import java.util.Map;
2831

2932
/**
@@ -117,9 +120,24 @@ private OpenTok(int apiKey, String apiSecret, HttpClient httpClient) {
117120
*
118121
* @return The token string.
119122
*/
120-
public String generateToken(String sessionId, TokenOptions tokenOptions)
121-
throws OpenTokException {
122-
return TokenGenerator.generateToken(sessionId, tokenOptions, apiKey, apiSecret);
123+
public String generateToken(String sessionId, TokenOptions tokenOptions) throws OpenTokException {
124+
List<String> sessionIdParts = null;
125+
if (sessionId == null || sessionId == "") {
126+
throw new InvalidArgumentException("Session not valid");
127+
}
128+
129+
try {
130+
sessionIdParts = Crypto.decodeSessionId(sessionId);
131+
} catch (UnsupportedEncodingException e) {
132+
throw new InvalidArgumentException("Session ID was not valid");
133+
}
134+
if (!sessionIdParts.contains(Integer.toString(this.apiKey))) {
135+
throw new InvalidArgumentException("Session ID was not valid");
136+
}
137+
138+
// NOTE: kind of wasteful of a Session instance
139+
Session session = new Session(sessionId, apiKey, apiSecret);
140+
return session.generateToken(tokenOptions);
123141
}
124142

125143
/**

src/main/java/com/opentok/Session.java

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,17 @@
88
package com.opentok;
99

1010
import com.opentok.exception.OpenTokException;
11-
import com.opentok.util.TokenGenerator;
11+
import java.io.UnsupportedEncodingException;
12+
import java.net.URLEncoder;
13+
import java.security.InvalidKeyException;
14+
import java.security.NoSuchAlgorithmException;
15+
import java.security.SignatureException;
16+
import java.util.Random;
17+
18+
import com.opentok.exception.InvalidArgumentException;
19+
import com.opentok.util.Crypto;
20+
import org.apache.commons.codec.binary.Base64;
21+
1222

1323
/**
1424
* Represents an OpenTok session. Use the {@link OpenTok#createSession(SessionProperties properties)}
@@ -93,6 +103,88 @@ public String generateToken() throws OpenTokException {
93103
* @return The token string.
94104
*/
95105
public String generateToken(TokenOptions tokenOptions) throws OpenTokException {
96-
return TokenGenerator.generateToken(sessionId, tokenOptions, apiKey, apiSecret);
106+
// Token format
107+
//
108+
// | ------------------------------ tokenStringBuilder ----------------------------- |
109+
// | "T1=="+Base64Encode(| --------------------- innerBuilder --------------------- |)|
110+
// | "partner_id={apiKey}&sig={sig}:| -- dataStringBuilder -- |
111+
112+
if (tokenOptions == null) {
113+
throw new InvalidArgumentException("Token options cannot be null");
114+
}
115+
116+
Role role = tokenOptions.getRole();
117+
double expireTime = tokenOptions.getExpireTime(); // will be 0 if nothing was explicitly set
118+
String data = tokenOptions.getData(); // will be null if nothing was explicitly set
119+
Long create_time = new Long(System.currentTimeMillis() / 1000).longValue();
120+
121+
StringBuilder dataStringBuilder = new StringBuilder();
122+
Random random = new Random();
123+
int nonce = random.nextInt();
124+
dataStringBuilder.append("session_id=");
125+
dataStringBuilder.append(sessionId);
126+
dataStringBuilder.append("&create_time=");
127+
dataStringBuilder.append(create_time);
128+
dataStringBuilder.append("&nonce=");
129+
dataStringBuilder.append(nonce);
130+
dataStringBuilder.append("&role=");
131+
dataStringBuilder.append(role);
132+
133+
double now = System.currentTimeMillis() / 1000L;
134+
if (expireTime == 0) {
135+
expireTime = now + (60*60*24); // 1 day
136+
} else if(expireTime < now-1) {
137+
throw new InvalidArgumentException(
138+
"Expire time must be in the future. relative time: "+ (expireTime - now));
139+
} else if(expireTime > (now + (60*60*24*30) /* 30 days */)) {
140+
throw new InvalidArgumentException(
141+
"Expire time must be in the next 30 days. too large by "+ (expireTime - (now + (60*60*24*30))));
142+
}
143+
// NOTE: Double.toString() would print the value with scientific notation
144+
dataStringBuilder.append(String.format("&expire_time=%.0f", expireTime));
145+
146+
if (data != null) {
147+
if(data.length() > 1000) {
148+
throw new InvalidArgumentException(
149+
"Connection data must be less than 1000 characters. length: " + data.length());
150+
}
151+
dataStringBuilder.append("&connection_data=");
152+
try {
153+
dataStringBuilder.append(URLEncoder.encode(data, "UTF-8"));
154+
} catch (UnsupportedEncodingException e) {
155+
throw new InvalidArgumentException(
156+
"Error during URL encode of your connection data: " + e.getMessage());
157+
}
158+
}
159+
160+
161+
StringBuilder tokenStringBuilder = new StringBuilder();
162+
try {
163+
tokenStringBuilder.append("T1==");
164+
165+
StringBuilder innerBuilder = new StringBuilder();
166+
innerBuilder.append("partner_id=");
167+
innerBuilder.append(this.apiKey);
168+
169+
innerBuilder.append("&sig=");
170+
171+
innerBuilder.append(Crypto.signData(dataStringBuilder.toString(), this.apiSecret));
172+
innerBuilder.append(":");
173+
innerBuilder.append(dataStringBuilder.toString());
174+
175+
tokenStringBuilder.append(
176+
Base64.encodeBase64String(
177+
innerBuilder.toString().getBytes("UTF-8")
178+
)
179+
.replace("+", "-")
180+
.replace("/", "_")
181+
);
182+
183+
} catch (SignatureException | NoSuchAlgorithmException
184+
| InvalidKeyException | UnsupportedEncodingException e) {
185+
throw new OpenTokException("Could not generate token, a signing error occurred.", e);
186+
}
187+
188+
return tokenStringBuilder.toString();
97189
}
98190
}

src/main/java/com/opentok/util/TokenGenerator.java

Lines changed: 8 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,90 +7,40 @@
77
*/
88
package com.opentok.util;
99

10-
import com.opentok.TokenOptions;
11-
import com.opentok.exception.InvalidArgumentException;
1210
import com.opentok.exception.OpenTokException;
13-
import org.apache.commons.lang.StringUtils;
1411
import org.jose4j.jws.AlgorithmIdentifiers;
1512
import org.jose4j.jws.JsonWebSignature;
1613
import org.jose4j.jwt.JwtClaims;
1714
import org.jose4j.jwt.NumericDate;
1815
import org.jose4j.lang.JoseException;
1916

2017
import javax.crypto.spec.SecretKeySpec;
21-
import java.io.UnsupportedEncodingException;
22-
import java.util.List;
18+
import java.util.concurrent.TimeUnit;
2319

2420
public class TokenGenerator {
2521

2622
public static final String ISSUER = "iss";
23+
public static final String ISSUER_TYPE = "ist";
2724
public static final String ISSUED_AT = "iat";
2825
public static final String EXP = "exp";
29-
public static final String SID = "sid";
30-
public static final String CONNECTION_DATA = "connectionData";
31-
public static final String ROLE = "role";
32-
public static final String INITIAL_LAYOUT_CLASS_LIST = "initialLayoutClassList";
26+
public static final String PROJECT_ISSUER_TYPE = "project";
27+
3328

3429
// Used by the REST Endpoints
3530
public static String generateToken(final Integer apiKey, final String apiSecret)
3631
throws OpenTokException {
3732

3833
//This is the default expire time we use for rest endpoints.
39-
final long defaultExpireTime = System.currentTimeMillis() / 1000L + 300; // 5 minutes
34+
final long defaultExpireTime = System.currentTimeMillis() / 1000L
35+
+ TimeUnit.MINUTES.toSeconds(3);
4036
final JwtClaims claims = new JwtClaims();
4137
claims.setIssuer(apiKey.toString());
38+
claims.setStringClaim(ISSUER_TYPE, PROJECT_ISSUER_TYPE);
39+
claims.setGeneratedJwtId(); // JTI a unique identifier for the JWT.
4240

4341
return getToken(claims, defaultExpireTime, apiSecret);
4442
}
4543

46-
public static String generateToken(final String sessionId, final TokenOptions tokenOptions,
47-
final Integer apiKey, final String apiSecret)
48-
throws OpenTokException {
49-
50-
List<String> sessionIdParts;
51-
if(sessionId == null || sessionId.isEmpty()) {
52-
throw new InvalidArgumentException("Session ID not valid");
53-
}
54-
55-
try {
56-
sessionIdParts = Crypto.decodeSessionId(sessionId);
57-
} catch (UnsupportedEncodingException e) {
58-
throw new InvalidArgumentException("Session ID was not valid");
59-
}
60-
61-
if (!sessionIdParts.contains(Integer.toString(apiKey))) {
62-
throw new InvalidArgumentException("Session ID was not valid");
63-
}
64-
65-
if (tokenOptions == null) {
66-
throw new InvalidArgumentException("Token options cannot be null");
67-
}
68-
69-
long expireTime = tokenOptions.getExpireTime();
70-
final long now = System.currentTimeMillis() / 1000L;
71-
if (expireTime == 0) {
72-
expireTime = now + (60 * 60 * 24); // 1 day
73-
} else if(expireTime < now - 1) {
74-
throw new InvalidArgumentException(
75-
"Expire time must be in the future. relative time: " + (expireTime - now));
76-
} else if(expireTime > (now + (60 * 60 * 24 * 30) /* 30 days */)) {
77-
throw new InvalidArgumentException(
78-
"Expire time must be in the next 30 days. too large by " +
79-
(expireTime - (now + (60 * 60 * 24 * 30))));
80-
}
81-
82-
final JwtClaims claims = new JwtClaims();
83-
claims.setIssuer(apiKey.toString());
84-
claims.setClaim(SID, sessionId);
85-
claims.setClaim(CONNECTION_DATA, tokenOptions.getData());
86-
claims.setClaim(ROLE, tokenOptions.getRole());
87-
claims.setClaim(INITIAL_LAYOUT_CLASS_LIST,
88-
StringUtils.join(tokenOptions.getInitialLayoutClassList(), " "));
89-
90-
return getToken(claims, expireTime, apiSecret);
91-
92-
}
93-
9444
private static String getToken(final JwtClaims claims, final long expireTime,
9545
final String apiSecret) throws OpenTokException {
9646
final SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(),

src/test/java/com/opentok/test/Helpers.java

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,65 @@
1111
import com.github.tomakehurst.wiremock.http.Request;
1212
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
1313
import com.opentok.constants.Version;
14+
import org.apache.commons.codec.binary.Base64;
1415
import org.jose4j.jws.AlgorithmIdentifiers;
1516
import org.jose4j.jwt.consumer.InvalidJwtException;
1617
import org.jose4j.jwt.consumer.JwtConsumer;
1718
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
1819

1920
import javax.crypto.spec.SecretKeySpec;
2021
import java.io.UnsupportedEncodingException;
22+
import java.net.URLDecoder;
23+
import java.security.InvalidKeyException;
24+
import java.security.NoSuchAlgorithmException;
25+
import java.security.SignatureException;
2126
import java.util.Formatter;
27+
import java.util.HashMap;
2228
import java.util.List;
2329
import java.util.Map;
2430

2531
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
2632
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
33+
import static com.opentok.util.Crypto.signData;
34+
import static com.opentok.util.TokenGenerator.ISSUED_AT;
35+
import static com.opentok.util.TokenGenerator.ISSUER;
36+
import static com.opentok.util.TokenGenerator.ISSUER_TYPE;
37+
import static com.opentok.util.TokenGenerator.PROJECT_ISSUER_TYPE;
2738
import static org.junit.Assert.assertTrue;
2839

2940
public class Helpers {
3041

31-
public static Map<String, Object> decodeToken(String token, Integer apiKey, String apiSecret)
32-
throws UnsupportedEncodingException, InvalidJwtException {
33-
return getClaims(token, apiKey, apiSecret);
34-
}
35-
36-
public static boolean verifyTokenSignature(String token, Integer apiKey, String apiSecret) {
42+
public static final String JTI = "jti";
3743

38-
try {
39-
getClaims(token, apiKey, apiSecret);
40-
return true;
41-
} catch (InvalidJwtException e) {
42-
return false;
44+
public static Map<String, String> decodeToken(String token) throws UnsupportedEncodingException {
45+
Map<String, String> tokenData = new HashMap<String, String>();
46+
token = token.substring(4);
47+
byte[] buffer = Base64.decodeBase64(token);
48+
String decoded = new String(buffer, "UTF-8");
49+
String[] decodedParts = decoded.split(":");
50+
for (String part : decodedParts) {
51+
tokenData.putAll(decodeFormData(part));
4352
}
53+
return tokenData;
4454
}
4555

46-
public static void verifyTokenAuth(int apiKey, String apiSecret, List<LoggedRequest> requests) {
56+
public static boolean verifyTokenSignature(String token, String apiSecret) throws
57+
UnsupportedEncodingException, NoSuchAlgorithmException, SignatureException, InvalidKeyException {
58+
token = token.substring(4);
59+
byte[] buffer = Base64.decodeBase64(token);
60+
String decoded = new String(buffer, "UTF-8");
61+
String[] decodedParts = decoded.split(":");
62+
String signature = decodeToken(token).get("sig");
63+
return (signature.equals(signData(decodedParts[1], apiSecret)));
64+
}
65+
66+
public static boolean verifyTokenAuth(Integer apiKey, String apiSecret, List<LoggedRequest> requests) {
4767
for (Request request: requests) {
48-
assertTrue(verifyTokenSignature(request.getHeader("X-OPENTOK-AUTH"), apiKey, apiSecret));
68+
if (!verifyJWTClaims(request.getHeader("X-OPENTOK-AUTH"), apiKey, apiSecret)) {
69+
return false;
70+
}
4971
}
72+
return true;
5073
}
5174

5275
public static void verifyUserAgent() {
@@ -55,6 +78,18 @@ public static void verifyUserAgent() {
5578
+ Version.VERSION + ".*JRE/" + System.getProperty("java.version") + ".*")));
5679
}
5780

81+
private static Map<String, String> decodeFormData(String formData) throws UnsupportedEncodingException {
82+
Map<String, String> decodedFormData = new HashMap<String, String>();
83+
String[] pairs = formData.split("\\&");
84+
for (int i = 0; i < pairs.length; i++) {
85+
String[] fields = pairs[i].split("=");
86+
String name = URLDecoder.decode(fields[0], "UTF-8");
87+
String value = URLDecoder.decode(fields[1], "UTF-8");
88+
decodedFormData.put(name, value);
89+
}
90+
return decodedFormData;
91+
}
92+
5893
private static Map<String, Object> getClaims(final String token, final Integer apiKey,
5994
final String apiSecret)
6095
throws InvalidJwtException {
@@ -79,4 +114,17 @@ private static String toHexString(byte[] bytes) {
79114
}
80115
return formatter.toString();
81116
}
117+
118+
public static boolean verifyJWTClaims(String token, Integer apiKey, String apiSecret) {
119+
try {
120+
Map<String, Object> tokenData = getClaims(token, apiKey, apiSecret);
121+
return apiKey.toString().equals(tokenData.get(ISSUER))
122+
&& PROJECT_ISSUER_TYPE.equals(tokenData.get(ISSUER_TYPE))
123+
&& System.currentTimeMillis() / 1000 >= (long) tokenData.get(ISSUED_AT)
124+
&& null != tokenData.get(JTI);
125+
} catch (InvalidJwtException e) {
126+
return false;
127+
}
128+
}
129+
82130
}

0 commit comments

Comments
 (0)