Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate JWT client-side tokens by default #266

Merged
merged 10 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading