Skip to content

Commit f7dd370

Browse files
authored
Generate JWT client-side tokens by default (#266)
* Make OpenTokException unchecked * JWT client-side generation * Bump version: v4.14.1 → 4.15.0 * Bump dependencies * Fix legacy token tests * Add JWT library for tests * Test default JWT * Test optional JWT params
1 parent dd165e8 commit f7dd370

File tree

10 files changed

+237
-181
lines changed

10 files changed

+237
-181
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[bumpversion]
22
commit = True
33
tag = False
4-
current_version = v4.14.1
4+
current_version = 4.15.0
55
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+)(?P<build>\d+))?
66
serialize =
77
{major}.{minor}.{patch}-{release}{build}

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ When you use Maven as your build tool, you can manage dependencies in the `pom.x
4848
<dependency>
4949
<groupId>com.tokbox</groupId>
5050
<artifactId>opentok-server-sdk</artifactId>
51-
<version>4.14.1</version>
51+
<version>4.15.0</version>
5252
</dependency>
5353
```
5454

@@ -58,7 +58,7 @@ When you use Gradle as your build tool, you can manage dependencies in the `buil
5858

5959
```groovy
6060
dependencies {
61-
compile group: 'com.tokbox', name: 'opentok-server-sdk', version: '4.14.1'
61+
compile group: 'com.tokbox', name: 'opentok-server-sdk', version: '4.15.0'
6262
}
6363
```
6464

build.gradle

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ plugins {
55
id 'jacoco'
66
id 'signing'
77
id 'maven-publish'
8-
id 'io.github.gradle-nexus.publish-plugin' version '1.3.0'
9-
id "com.github.hierynomus.license" version "0.16.1"
8+
id 'io.github.gradle-nexus.publish-plugin' version '2.0.0'
9+
id 'com.github.hierynomus.license' version '0.16.1'
10+
id 'com.github.ben-manes.versions' version '0.51.0'
1011
}
1112

1213
group = 'com.tokbox'
1314
archivesBaseName = 'opentok-server-sdk'
14-
version = '4.14.1'
15+
version = '4.15.0'
1516

1617
ext.githubPath = "opentok/$archivesBaseName"
1718

@@ -21,15 +22,18 @@ repositories {
2122

2223
dependencies {
2324
testImplementation 'junit:junit:4.13.2'
24-
testImplementation 'org.wiremock:wiremock:3.6.0'
25-
testImplementation 'com.google.guava:guava:33.2.1-jre'
25+
testImplementation 'org.wiremock:wiremock:3.9.2'
26+
testImplementation 'com.google.guava:guava:33.3.1-jre'
27+
testImplementation 'io.jsonwebtoken:jjwt-api:0.12.6'
28+
testImplementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
29+
testImplementation 'io.jsonwebtoken:jjwt-jackson:0.12.6'
2630

2731
implementation 'commons-lang:commons-lang:2.6'
28-
implementation 'commons-codec:commons-codec:1.17.0'
29-
implementation 'io.netty:netty-codec-http:4.1.111.Final'
30-
implementation 'io.netty:netty-handler:4.1.111.Final'
32+
implementation 'commons-codec:commons-codec:1.17.1'
33+
implementation 'io.netty:netty-codec-http:4.1.114.Final'
34+
implementation 'io.netty:netty-handler:4.1.114.Final'
3135
implementation 'org.asynchttpclient:async-http-client:2.12.3'
32-
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
36+
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.1'
3337
implementation 'org.bitbucket.b_c:jose4j:0.9.6'
3438
}
3539

@@ -68,7 +72,7 @@ javadoc {
6872
}
6973

7074
jacoco {
71-
toolVersion = "0.8.8"
75+
toolVersion = "0.8.12"
7276
}
7377
jacocoTestReport {
7478
reports {

bumpversion.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ then
1111
exit 1
1212
fi
1313

14-
python -m pip install --upgrade pip
14+
python3 -m pip install --upgrade pip
1515
pip install bump2version
1616
bump2version --new-version "$1" patch

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

Lines changed: 84 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
import com.opentok.exception.InvalidArgumentException;
2020
import com.opentok.util.Crypto;
21+
import com.opentok.util.TokenGenerator;
2122
import org.apache.commons.codec.binary.Base64;
23+
import org.jose4j.jwt.JwtClaims;
2224

2325

2426
/**
@@ -99,92 +101,106 @@ public String generateToken() throws OpenTokException {
99101
* @return The token string.
100102
*/
101103
public String generateToken(TokenOptions tokenOptions) throws OpenTokException {
102-
// Token format
103-
//
104-
// | ------------------------------ tokenStringBuilder ----------------------------- |
105-
// | "T1=="+Base64Encode(| --------------------- innerBuilder --------------------- |)|
106-
// | "partner_id={apiKey}&sig={sig}:| -- dataStringBuilder -- |
107104

108105
if (tokenOptions == null) {
109106
throw new InvalidArgumentException("Token options cannot be null");
110107
}
111108

112109
Role role = tokenOptions.getRole();
113-
double expireTime = tokenOptions.getExpireTime(); // will be 0 if nothing was explicitly set
114-
String data = tokenOptions.getData(); // will be null if nothing was explicitly set
115-
long create_time = System.currentTimeMillis() / 1000;
116-
117-
StringBuilder dataStringBuilder = new StringBuilder();
118-
Random random = new Random();
119-
int nonce = random.nextInt();
120-
dataStringBuilder
121-
.append("session_id=").append(sessionId)
122-
.append("&create_time=").append(create_time)
123-
.append("&nonce=").append(nonce)
124-
.append("&role=").append(role);
125-
126-
if (tokenOptions.getInitialLayoutClassList() != null ) {
127-
dataStringBuilder.append("&initial_layout_class_list=");
128-
dataStringBuilder.append(String.join(" ", tokenOptions.getInitialLayoutClassList()));
129-
}
130-
131-
long now = System.currentTimeMillis() / 1000L;
132-
if (expireTime == 0) {
133-
expireTime = now + (60*60*24); // 1 day
134-
}
135-
else if (expireTime < now) {
110+
String data = tokenOptions.getData();
111+
int nonce = new Random().nextInt();
112+
long iat = System.currentTimeMillis() / 1000;
113+
long exp = tokenOptions.getExpireTime();
114+
115+
if (exp == 0) {
116+
exp = iat + (60 * 60 * 24); // 1 day
117+
} else if (exp < iat) {
136118
throw new InvalidArgumentException(
137-
"Expire time must be in the future. Relative time: "+ (expireTime - now)
119+
"Expire time must be in the future. Relative time: " + (exp - iat)
138120
);
139-
}
140-
else if (expireTime > (now + (60*60*24*30) /* 30 days */)) {
121+
} else if (exp > (iat + (60 * 60 * 24 * 30) /* 30 days */)) {
141122
throw new InvalidArgumentException(
142-
"Expire time must be in the next 30 days. Too large by "+ (expireTime - (now + (60*60*24*30)))
123+
"Expire time must be in the next 30 days. Too large by " + (exp - (iat + (60 * 60 * 24 * 30)))
143124
);
144125
}
145-
// NOTE: Double.toString() would print the value with scientific notation
146-
dataStringBuilder.append(String.format("&expire_time=%.0f", expireTime));
147126

148-
if (data != null) {
149-
if (data.length() > 1000) {
150-
throw new InvalidArgumentException(
151-
"Connection data must be less than 1000 characters. length: " + data.length()
152-
);
127+
if (tokenOptions.isLegacyT1Token()) {
128+
// Token format
129+
//
130+
// | ------------------------------ tokenStringBuilder ----------------------------- |
131+
// | "T1=="+Base64Encode(| --------------------- innerBuilder --------------------- |)|
132+
// | "partner_id={apiKey}&sig={sig}:| -- dataStringBuilder -- |
133+
134+
StringBuilder dataStringBuilder = new StringBuilder()
135+
.append("session_id=").append(sessionId)
136+
.append("&create_time=").append(iat)
137+
.append("&nonce=").append(nonce)
138+
.append("&role=").append(role);
139+
140+
if (tokenOptions.getInitialLayoutClassList() != null) {
141+
dataStringBuilder
142+
.append("&initial_layout_class_list=")
143+
.append(String.join(" ", tokenOptions.getInitialLayoutClassList()));
153144
}
154-
dataStringBuilder.append("&connection_data=");
155-
try {
156-
dataStringBuilder.append(URLEncoder.encode(data, "UTF-8"));
157-
}
158-
catch (UnsupportedEncodingException e) {
159-
throw new InvalidArgumentException(
160-
"Error during URL encode of your connection data: " + e.getMessage()
161-
);
162-
}
163-
}
164-
165145

166-
StringBuilder tokenStringBuilder = new StringBuilder();
167-
try {
168-
tokenStringBuilder.append("T1==");
146+
dataStringBuilder.append("&expire_time=").append(exp);
147+
148+
if (data != null) {
149+
if (data.length() > 1000) {
150+
throw new InvalidArgumentException(
151+
"Connection data must be less than 1000 characters. length: " + data.length()
152+
);
153+
}
154+
dataStringBuilder.append("&connection_data=");
155+
try {
156+
dataStringBuilder.append(URLEncoder.encode(data, "UTF-8"));
157+
}
158+
catch (UnsupportedEncodingException e) {
159+
throw new InvalidArgumentException(
160+
"Error during URL encode of your connection data: " + e.getMessage()
161+
);
162+
}
163+
}
169164

170-
String innerBuilder = "partner_id=" +
171-
this.apiKey +
172-
"&sig=" +
173-
Crypto.signData(dataStringBuilder.toString(), this.apiSecret) +
174-
":" +
175-
dataStringBuilder;
176165

177-
tokenStringBuilder.append(
178-
Base64.encodeBase64String(innerBuilder.getBytes(StandardCharsets.UTF_8))
179-
.replace("+", "-")
180-
.replace("/", "_")
181-
);
166+
StringBuilder tokenStringBuilder = new StringBuilder();
167+
try {
168+
tokenStringBuilder.append("T1==");
169+
170+
String innerBuilder = "partner_id=" +
171+
this.apiKey +
172+
"&sig=" +
173+
Crypto.signData(dataStringBuilder.toString(), this.apiSecret) +
174+
":" +
175+
dataStringBuilder;
176+
177+
tokenStringBuilder.append(
178+
Base64.encodeBase64String(innerBuilder.getBytes(StandardCharsets.UTF_8))
179+
.replace("+", "-")
180+
.replace("/", "_")
181+
);
182182

183+
}
184+
catch (SignatureException | NoSuchAlgorithmException | InvalidKeyException e) {
185+
throw new OpenTokException("Could not generate token, a signing error occurred.", e);
186+
}
187+
return tokenStringBuilder.toString();
183188
}
184-
catch (SignatureException | NoSuchAlgorithmException | InvalidKeyException e) {
185-
throw new OpenTokException("Could not generate token, a signing error occurred.", e);
189+
else {
190+
JwtClaims claims = new JwtClaims();
191+
claims.setClaim("nonce", nonce);
192+
claims.setClaim("role", role.toString());
193+
claims.setClaim("session_id", sessionId);
194+
claims.setClaim("scope", "session.connect");
195+
if (tokenOptions.getInitialLayoutClassList() != null) {
196+
claims.setClaim("initial_layout_class_list",
197+
String.join(" ", tokenOptions.getInitialLayoutClassList())
198+
);
199+
}
200+
if (tokenOptions.getData() != null) {
201+
claims.setClaim("connection_data", tokenOptions.getData());
202+
}
203+
return TokenGenerator.generateToken(claims, exp, apiKey, apiSecret);
186204
}
187-
188-
return tokenStringBuilder.toString();
189205
}
190206
}

src/main/java/com/opentok/TokenOptions.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,20 @@ public class TokenOptions {
2424
private long expireTime;
2525
private String data;
2626
private List<String> initialLayoutClassList;
27+
private boolean legacyT1Token;
2728

2829
private TokenOptions(Builder builder) {
29-
this.role = builder.role != null ? builder.role : Role.PUBLISHER;
30+
role = builder.role != null ? builder.role : Role.PUBLISHER;
31+
legacyT1Token = builder.legacyT1Token;
3032

3133
// default value calculated at token generation time
32-
this.expireTime = builder.expireTime;
34+
expireTime = builder.expireTime;
3335

3436
// default value of null means to omit the key "connection_data" from the token
35-
this.data = builder.data;
37+
data = builder.data;
3638

3739
// default value of null means to omit the key "initialLayoutClassList" from the token
38-
this.initialLayoutClassList = builder.initialLayoutClassList;
40+
initialLayoutClassList = builder.initialLayoutClassList;
3941
}
4042

4143
/**
@@ -69,6 +71,16 @@ public List<String> getInitialLayoutClassList() {
6971
return initialLayoutClassList;
7072
}
7173

74+
/**
75+
* Returns whether the generated token will be in the old T1 format instead of JWT.
76+
*
77+
* @return {@code true} if the token will be in the old T1 format, {@code false} otherwise.
78+
* @since 4.15.0
79+
*/
80+
public boolean isLegacyT1Token() {
81+
return legacyT1Token;
82+
}
83+
7284
/**
7385
* Use this class to create a TokenOptions object.
7486
*
@@ -79,6 +91,7 @@ public static class Builder {
7991
private long expireTime = 0;
8092
private String data;
8193
private List<String> initialLayoutClassList;
94+
private boolean legacyT1Token = false;
8295

8396
/**
8497
* Sets the role for the token. Each role defines a set of permissions granted to the token.
@@ -148,6 +161,18 @@ public Builder initialLayoutClassList (List<String> initialLayoutClassList) {
148161
return this;
149162
}
150163

164+
/**
165+
* Use this method to generate a legacy T1 token instead of a JWT.
166+
*
167+
* @return This builder.
168+
*
169+
* @since 4.15.0
170+
*/
171+
public Builder useLegacyT1Token() {
172+
legacyT1Token = true;
173+
return this;
174+
}
175+
151176
/**
152177
* Builds the TokenOptions object.
153178
*

src/main/java/com/opentok/constants/Version.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
package com.opentok.constants;
99

1010
public class Version {
11-
public static final String VERSION = "4.14.1";
11+
public static final String VERSION = "4.15.0";
1212
}

src/main/java/com/opentok/exception/OpenTokException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
/**
1111
* Defines exceptions in the OpenTok SDK.
1212
*/
13-
public class OpenTokException extends Exception {
13+
public class OpenTokException extends RuntimeException {
1414
private static final long serialVersionUID = 6059658348908505724L;
1515

1616
/**

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

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import org.jose4j.jwt.JwtClaims;
1414
import org.jose4j.jwt.NumericDate;
1515
import org.jose4j.lang.JoseException;
16-
1716
import javax.crypto.spec.SecretKeySpec;
18-
import java.util.concurrent.TimeUnit;
17+
import java.time.Instant;
18+
import java.time.temporal.ChronoUnit;
1919

2020
public class TokenGenerator {
2121

@@ -30,22 +30,20 @@ public class TokenGenerator {
3030
public static String generateToken(final Integer apiKey, final String apiSecret)
3131
throws OpenTokException {
3232

33-
//This is the default expire time we use for rest endpoints.
34-
final long defaultExpireTime = System.currentTimeMillis() / 1000L
35-
+ TimeUnit.MINUTES.toSeconds(3);
3633
final JwtClaims claims = new JwtClaims();
37-
claims.setIssuer(apiKey.toString());
38-
claims.setStringClaim(ISSUER_TYPE, PROJECT_ISSUER_TYPE);
39-
claims.setGeneratedJwtId(); // JTI a unique identifier for the JWT.
40-
41-
return getToken(claims, defaultExpireTime, apiSecret);
34+
//This is the default expire time we use for rest endpoints.
35+
final long defaultExpireTime = Instant.now().plus(3, ChronoUnit.MINUTES).getEpochSecond();
36+
return generateToken(claims, defaultExpireTime, apiKey, apiSecret);
4237
}
4338

44-
private static String getToken(final JwtClaims claims, final long expireTime,
45-
final String apiSecret) throws OpenTokException {
39+
public static String generateToken(final JwtClaims claims, final long expireTime,
40+
final int apiKey, final String apiSecret) throws OpenTokException {
4641
final SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(),
4742
AlgorithmIdentifiers.HMAC_SHA256);
4843

44+
claims.setStringClaim(ISSUER_TYPE, PROJECT_ISSUER_TYPE);
45+
claims.setIssuer(apiKey + "");
46+
claims.setGeneratedJwtId(); // JTI a unique identifier for the JWT.
4947
claims.setExpirationTime(NumericDate.fromSeconds(expireTime));
5048
claims.setIssuedAtToNow();
5149

0 commit comments

Comments
 (0)