Hey Donte,
Sure thing, I actually went and created a ticket of my own after posting here as my issue is a bit different in that i’m only getting the signature invalid error about 3/10 times I reload the page - it’s really random and makes no sense. In other words, 7/10 times i reload the page and attempt to join the webinar, it WORKS. And there’s no difference in the signatures being passed each time as they’re all being generated from the same source.
Here’s my open ticket: #15610152
In short - i’ve completed all of the steps to upgrade the SDK to latest (now using SDK App type creds with SDK key and secret leveraged in the new signature generation style. One odd observation is that when running the app locally (we use a node server when working locally to handle the signature generation), it works 100% of the time, as in i’m able to enter the meeting with no signature invalid errors. However, in production environments, we use Java servlets to handle the signature generation, so implementation is a bit different. This is the scenario where we start getting only about 70% successful joins and 30% of attempts give “signature invalid”.
Here’s the node signature generation code:
require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const KJUR = require("jsrsasign");
const cors = require("cors");
const app = express();
const port = process.env.PORT || 4000;
app.use(bodyParser.json(), cors());
app.options("*", cors());
app.post("/", (req, res) => {
const iat = Math.round((new Date().getTime() - 30000) / 1000);
const exp = iat + 60 * 60 * 2;
const oHeader = { alg: "HS256", typ: "JWT" };
const oPayload = {
sdkKey: process.env.ZOOM_SDK_API_KEY, // Our SDK app "SDK Key"
mn: req.body.meetingNumber,
role: req.body.role,
iat,
exp,
appKey: process.env.ZOOM_SDK_API_KEY,
tokenExp: iat + 60 * 60 * 2,
};
console.log(oPayload);
const sHeader = JSON.stringify(oHeader);
const sPayload = JSON.stringify(oPayload);
const sdkJWT = KJUR.jws.JWS.sign("HS256", sHeader, sPayload, process.env.ZOOM_SDK_API_SECRET); // Our SDK app "SDK Secret"
res.json({
sdkKey: process.env.ZOOM_SDK_API_KEY,
signature: sdkJWT,
});
});
app.listen(port, () =>
console.log(`Zoom Web Meeting SDK Sample Signature Node.js on port ${port}!`)
);
…and here’s the Java implementation:
package com.dropbox.cms.dmep.servlets;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Date;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import com.day.cq.search.QueryBuilder;
import com.dropbox.aem.common.services.DropboxLinkHelper;
import com.dropbox.cms.dmep.service.WebinarConfigService;
import com.adobe.granite.crypto.CryptoSupport;
import org.apache.commons.lang.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.json.JSONException;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(
service = Servlet.class,
property = {
"sling.servlet.extensions=" + WebinarServlet.EXTENSION,
"sling.servlet.paths=" + WebinarServlet.ENDPOINT,
"sling.servlet.methods=get"
})
public class WebinarServlet extends SlingSafeMethodsServlet {
@Inject
private WebinarConfigService webinarConfigService;
private static final Logger LOGGER = LoggerFactory.getLogger(WebinarServlet.class);
public static final String ENDPOINT = "/bin/dropbox/dmep/webinar";
public static final String EXTENSION = "json";
private static final String JWT_HEADER = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
private static final int EXPIRY_DAYS = 90;
private String signature = StringUtils.EMPTY;
private String sdkSecret = StringUtils.EMPTY;
private String sdkKey = StringUtils.EMPTY;
private String meetingNumber = StringUtils.EMPTY;
private String role = StringUtils.EMPTY;
private String encodedHeader = StringUtils.EMPTY;
private String encodedPayload = StringUtils.EMPTY;
private int iat = 0;
private int exp = 0;
private QueryBuilder queryBuilder;
private DropboxLinkHelper linkService;
@Reference
public void bindWebinarConfigService(WebinarConfigService webinarConfigService) {
this.webinarConfigService = webinarConfigService;
}
public void unbindWebinarConfigService(WebinarConfigService webinarConfigService) {
this.webinarConfigService = webinarConfigService;
}
@Reference
private CryptoSupport cryptoSupport;
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
try {
response.setStatus(HttpServletResponse.SC_OK);
sdkKey = webinarConfigService.getSdkKey();
if (this.cryptoSupport.isProtected(sdkKey)) {
sdkKey = this.cryptoSupport.unprotect(sdkKey);
}
sdkSecret = webinarConfigService.getSdkSecret();
if (this.cryptoSupport.isProtected(sdkSecret)) {
sdkSecret = this.cryptoSupport.unprotect(sdkSecret);
}
meetingNumber = request.getParameter("meetingNumber");
role = request.getParameter("role");
encodedHeader = encode(JWT_HEADER.getBytes());
if (sdkKey != null && meetingNumber != null && role != null) {
encodedPayload = encode(generatePayload(sdkKey,meetingNumber,role).getBytes());
}
if (sdkSecret != null) {
signature = hmacSha256(encodedHeader + "." + encodedPayload,sdkSecret);
}
if (!encodedPayload.isEmpty() && !signature.isEmpty()) {
signature = encodedHeader + "." + encodedPayload + "." + signature;
}
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
out.print("{\"signature\": \"" + signature
+ "\"," + "\"sdkKey\": \"" + sdkKey + "\"}");
out.flush();
} catch (Exception e) {
response.getWriter().write(e.getMessage());
LOGGER.error(e.getMessage(), e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* Generates hmacSha256 signature.
* CMS-6253 : For zoom sdk update provided
* proper Json Web Token signature
* @return String
*/
private String hmacSha256(String data, String secret) {
try {
byte[] hash = secret.getBytes(StandardCharsets.UTF_8);
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(hash, "HmacSHA256");
sha256Hmac.init(secretKey);
byte[] signedBytes = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return encode(signedBytes);
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
LOGGER.error("Exception while generating hmacSha256 signature {}", ex.getMessage());
return null;
}
}
/**
* Generates payload for jwt token using sdkkey,meeting number and role.
*
* @return String
*/
public String generatePayload(String sdkKey,
String meetingNumber,String role) {
iat = Math.round((new Date().getTime() - 30000) / 1000);
exp = iat + 60 * 60 * 2;
JSONObject jwtPayload = new JSONObject();
try {
jwtPayload.put("appKey", sdkKey);
jwtPayload.put("sdkKey", sdkKey);
jwtPayload.put("mn", meetingNumber);
jwtPayload.put("role", Integer.parseInt(role));
jwtPayload.put("iat", iat);
jwtPayload.put("exp", exp);
jwtPayload.put("tokenExp", exp);
return jwtPayload.toString();
} catch (JSONException e) {
LOGGER.error("Exception while generating payload {}", e.getMessage());
return null;
}
}
private static String encode(byte[] bytes) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
@Reference
public void bindQueryBuilder(QueryBuilder queryBuilder) {
this.queryBuilder = queryBuilder;
}
@Reference
public void bindLinkService(DropboxLinkHelper linkService) {
this.linkService = linkService;
}
}
Again, we’ve verified from both endpoints that the resulting signature is the exact same, yet we get this “signature invalid” error on ZmMtg.join on the front end ONLY when using the Java servlet. Very odd and leading me to believe it’s got to have something to do with the backend implementation (there were no docs on this, so we had to improv). But implementation shouldn’t matter given we’ve verified signatures from both node and java servlet are exact same structure. We’ve even cross-checked the iat and exp timestamps to see if they were off somehow…nope! Let me know if there’s anything else i can provide!