Skip to content

Commit

Permalink
Generate JWT client-side tokens by default (#266)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
SMadani authored Nov 11, 2024
1 parent dd165e8 commit f7dd370
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 181 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[bumpversion]
commit = True
tag = False
current_version = v4.14.1
current_version = 4.15.0
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+)(?P<build>\d+))?
serialize =
{major}.{minor}.{patch}-{release}{build}
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ When you use Maven as your build tool, you can manage dependencies in the `pom.x
<dependency>
<groupId>com.tokbox</groupId>
<artifactId>opentok-server-sdk</artifactId>
<version>4.14.1</version>
<version>4.15.0</version>
</dependency>
```

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

```groovy
dependencies {
compile group: 'com.tokbox', name: 'opentok-server-sdk', version: '4.14.1'
compile group: 'com.tokbox', name: 'opentok-server-sdk', version: '4.15.0'
}
```

Expand Down
24 changes: 14 additions & 10 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ plugins {
id 'jacoco'
id 'signing'
id 'maven-publish'
id 'io.github.gradle-nexus.publish-plugin' version '1.3.0'
id "com.github.hierynomus.license" version "0.16.1"
id 'io.github.gradle-nexus.publish-plugin' version '2.0.0'
id 'com.github.hierynomus.license' version '0.16.1'
id 'com.github.ben-manes.versions' version '0.51.0'
}

group = 'com.tokbox'
archivesBaseName = 'opentok-server-sdk'
version = '4.14.1'
version = '4.15.0'

ext.githubPath = "opentok/$archivesBaseName"

Expand All @@ -21,15 +22,18 @@ repositories {

dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.wiremock:wiremock:3.6.0'
testImplementation 'com.google.guava:guava:33.2.1-jre'
testImplementation 'org.wiremock:wiremock:3.9.2'
testImplementation 'com.google.guava:guava:33.3.1-jre'
testImplementation 'io.jsonwebtoken:jjwt-api:0.12.6'
testImplementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
testImplementation 'io.jsonwebtoken:jjwt-jackson:0.12.6'

implementation 'commons-lang:commons-lang:2.6'
implementation 'commons-codec:commons-codec:1.17.0'
implementation 'io.netty:netty-codec-http:4.1.111.Final'
implementation 'io.netty:netty-handler:4.1.111.Final'
implementation 'commons-codec:commons-codec:1.17.1'
implementation 'io.netty:netty-codec-http:4.1.114.Final'
implementation 'io.netty:netty-handler:4.1.114.Final'
implementation 'org.asynchttpclient:async-http-client:2.12.3'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.1'
implementation 'org.bitbucket.b_c:jose4j:0.9.6'
}

Expand Down Expand Up @@ -68,7 +72,7 @@ javadoc {
}

jacoco {
toolVersion = "0.8.8"
toolVersion = "0.8.12"
}
jacocoTestReport {
reports {
Expand Down
2 changes: 1 addition & 1 deletion bumpversion.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ then
exit 1
fi

python -m pip install --upgrade pip
python3 -m pip install --upgrade pip
pip install bump2version
bump2version --new-version "$1" patch
152 changes: 84 additions & 68 deletions src/main/java/com/opentok/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

import com.opentok.exception.InvalidArgumentException;
import com.opentok.util.Crypto;
import com.opentok.util.TokenGenerator;
import org.apache.commons.codec.binary.Base64;
import org.jose4j.jwt.JwtClaims;


/**
Expand Down Expand Up @@ -99,92 +101,106 @@ public String generateToken() throws OpenTokException {
* @return The token string.
*/
public String generateToken(TokenOptions tokenOptions) throws OpenTokException {
// Token format
//
// | ------------------------------ tokenStringBuilder ----------------------------- |
// | "T1=="+Base64Encode(| --------------------- innerBuilder --------------------- |)|
// | "partner_id={apiKey}&sig={sig}:| -- dataStringBuilder -- |

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

Role role = tokenOptions.getRole();
double expireTime = tokenOptions.getExpireTime(); // will be 0 if nothing was explicitly set
String data = tokenOptions.getData(); // will be null if nothing was explicitly set
long create_time = System.currentTimeMillis() / 1000;

StringBuilder dataStringBuilder = new StringBuilder();
Random random = new Random();
int nonce = random.nextInt();
dataStringBuilder
.append("session_id=").append(sessionId)
.append("&create_time=").append(create_time)
.append("&nonce=").append(nonce)
.append("&role=").append(role);

if (tokenOptions.getInitialLayoutClassList() != null ) {
dataStringBuilder.append("&initial_layout_class_list=");
dataStringBuilder.append(String.join(" ", tokenOptions.getInitialLayoutClassList()));
}

long now = System.currentTimeMillis() / 1000L;
if (expireTime == 0) {
expireTime = now + (60*60*24); // 1 day
}
else if (expireTime < now) {
String data = tokenOptions.getData();
int nonce = new Random().nextInt();
long iat = System.currentTimeMillis() / 1000;
long exp = tokenOptions.getExpireTime();

if (exp == 0) {
exp = iat + (60 * 60 * 24); // 1 day
} else if (exp < iat) {
throw new InvalidArgumentException(
"Expire time must be in the future. Relative time: "+ (expireTime - now)
"Expire time must be in the future. Relative time: " + (exp - iat)
);
}
else if (expireTime > (now + (60*60*24*30) /* 30 days */)) {
} else if (exp > (iat + (60 * 60 * 24 * 30) /* 30 days */)) {
throw new InvalidArgumentException(
"Expire time must be in the next 30 days. Too large by "+ (expireTime - (now + (60*60*24*30)))
"Expire time must be in the next 30 days. Too large by " + (exp - (iat + (60 * 60 * 24 * 30)))
);
}
// NOTE: Double.toString() would print the value with scientific notation
dataStringBuilder.append(String.format("&expire_time=%.0f", expireTime));

if (data != null) {
if (data.length() > 1000) {
throw new InvalidArgumentException(
"Connection data must be less than 1000 characters. length: " + data.length()
);
if (tokenOptions.isLegacyT1Token()) {
// Token format
//
// | ------------------------------ tokenStringBuilder ----------------------------- |
// | "T1=="+Base64Encode(| --------------------- innerBuilder --------------------- |)|
// | "partner_id={apiKey}&sig={sig}:| -- dataStringBuilder -- |

StringBuilder dataStringBuilder = new StringBuilder()
.append("session_id=").append(sessionId)
.append("&create_time=").append(iat)
.append("&nonce=").append(nonce)
.append("&role=").append(role);

if (tokenOptions.getInitialLayoutClassList() != null) {
dataStringBuilder
.append("&initial_layout_class_list=")
.append(String.join(" ", tokenOptions.getInitialLayoutClassList()));
}
dataStringBuilder.append("&connection_data=");
try {
dataStringBuilder.append(URLEncoder.encode(data, "UTF-8"));
}
catch (UnsupportedEncodingException e) {
throw new InvalidArgumentException(
"Error during URL encode of your connection data: " + e.getMessage()
);
}
}


StringBuilder tokenStringBuilder = new StringBuilder();
try {
tokenStringBuilder.append("T1==");
dataStringBuilder.append("&expire_time=").append(exp);

if (data != null) {
if (data.length() > 1000) {
throw new InvalidArgumentException(
"Connection data must be less than 1000 characters. length: " + data.length()
);
}
dataStringBuilder.append("&connection_data=");
try {
dataStringBuilder.append(URLEncoder.encode(data, "UTF-8"));
}
catch (UnsupportedEncodingException e) {
throw new InvalidArgumentException(
"Error during URL encode of your connection data: " + e.getMessage()
);
}
}

String innerBuilder = "partner_id=" +
this.apiKey +
"&sig=" +
Crypto.signData(dataStringBuilder.toString(), this.apiSecret) +
":" +
dataStringBuilder;

tokenStringBuilder.append(
Base64.encodeBase64String(innerBuilder.getBytes(StandardCharsets.UTF_8))
.replace("+", "-")
.replace("/", "_")
);
StringBuilder tokenStringBuilder = new StringBuilder();
try {
tokenStringBuilder.append("T1==");

String innerBuilder = "partner_id=" +
this.apiKey +
"&sig=" +
Crypto.signData(dataStringBuilder.toString(), this.apiSecret) +
":" +
dataStringBuilder;

tokenStringBuilder.append(
Base64.encodeBase64String(innerBuilder.getBytes(StandardCharsets.UTF_8))
.replace("+", "-")
.replace("/", "_")
);

}
catch (SignatureException | NoSuchAlgorithmException | InvalidKeyException e) {
throw new OpenTokException("Could not generate token, a signing error occurred.", e);
}
return tokenStringBuilder.toString();
}
catch (SignatureException | NoSuchAlgorithmException | InvalidKeyException e) {
throw new OpenTokException("Could not generate token, a signing error occurred.", e);
else {
JwtClaims claims = new JwtClaims();
claims.setClaim("nonce", nonce);
claims.setClaim("role", role.toString());
claims.setClaim("session_id", sessionId);
claims.setClaim("scope", "session.connect");
if (tokenOptions.getInitialLayoutClassList() != null) {
claims.setClaim("initial_layout_class_list",
String.join(" ", tokenOptions.getInitialLayoutClassList())
);
}
if (tokenOptions.getData() != null) {
claims.setClaim("connection_data", tokenOptions.getData());
}
return TokenGenerator.generateToken(claims, exp, apiKey, apiSecret);
}

return tokenStringBuilder.toString();
}
}
33 changes: 29 additions & 4 deletions src/main/java/com/opentok/TokenOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ public class TokenOptions {
private long expireTime;
private String data;
private List<String> initialLayoutClassList;
private boolean legacyT1Token;

private TokenOptions(Builder builder) {
this.role = builder.role != null ? builder.role : Role.PUBLISHER;
role = builder.role != null ? builder.role : Role.PUBLISHER;
legacyT1Token = builder.legacyT1Token;

// default value calculated at token generation time
this.expireTime = builder.expireTime;
expireTime = builder.expireTime;

// default value of null means to omit the key "connection_data" from the token
this.data = builder.data;
data = builder.data;

// default value of null means to omit the key "initialLayoutClassList" from the token
this.initialLayoutClassList = builder.initialLayoutClassList;
initialLayoutClassList = builder.initialLayoutClassList;
}

/**
Expand Down Expand Up @@ -69,6 +71,16 @@ public List<String> getInitialLayoutClassList() {
return initialLayoutClassList;
}

/**
* Returns whether the generated token will be in the old T1 format instead of JWT.
*
* @return {@code true} if the token will be in the old T1 format, {@code false} otherwise.
* @since 4.15.0
*/
public boolean isLegacyT1Token() {
return legacyT1Token;
}

/**
* Use this class to create a TokenOptions object.
*
Expand All @@ -79,6 +91,7 @@ public static class Builder {
private long expireTime = 0;
private String data;
private List<String> initialLayoutClassList;
private boolean legacyT1Token = false;

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

/**
* Use this method to generate a legacy T1 token instead of a JWT.
*
* @return This builder.
*
* @since 4.15.0
*/
public Builder useLegacyT1Token() {
legacyT1Token = true;
return this;
}

/**
* Builds the TokenOptions object.
*
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/opentok/constants/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
package com.opentok.constants;

public class Version {
public static final String VERSION = "4.14.1";
public static final String VERSION = "4.15.0";
}
2 changes: 1 addition & 1 deletion src/main/java/com/opentok/exception/OpenTokException.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
/**
* Defines exceptions in the OpenTok SDK.
*/
public class OpenTokException extends Exception {
public class OpenTokException extends RuntimeException {
private static final long serialVersionUID = 6059658348908505724L;

/**
Expand Down
22 changes: 10 additions & 12 deletions src/main/java/com/opentok/util/TokenGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.NumericDate;
import org.jose4j.lang.JoseException;

import javax.crypto.spec.SecretKeySpec;
import java.util.concurrent.TimeUnit;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class TokenGenerator {

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

//This is the default expire time we use for rest endpoints.
final long defaultExpireTime = System.currentTimeMillis() / 1000L
+ TimeUnit.MINUTES.toSeconds(3);
final JwtClaims claims = new JwtClaims();
claims.setIssuer(apiKey.toString());
claims.setStringClaim(ISSUER_TYPE, PROJECT_ISSUER_TYPE);
claims.setGeneratedJwtId(); // JTI a unique identifier for the JWT.

return getToken(claims, defaultExpireTime, apiSecret);
//This is the default expire time we use for rest endpoints.
final long defaultExpireTime = Instant.now().plus(3, ChronoUnit.MINUTES).getEpochSecond();
return generateToken(claims, defaultExpireTime, apiKey, apiSecret);
}

private static String getToken(final JwtClaims claims, final long expireTime,
final String apiSecret) throws OpenTokException {
public static String generateToken(final JwtClaims claims, final long expireTime,
final int apiKey, final String apiSecret) throws OpenTokException {
final SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(),
AlgorithmIdentifiers.HMAC_SHA256);

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

Expand Down
Loading

0 comments on commit f7dd370

Please sign in to comment.