Skip to main content

Java (Spring Boot) Implementation

Notice

This document is a machine-translated draft and is currently undergoing review. Some content may be inaccurate or differ from the original Korean version. For the most precise information, refer to the Korean documentation.

Configuration​

Configure the security key and custom key required for generating callback responses to be injected via the application-local.yaml configuration file rather than hardcoded in the source code. Copy the repository's application-local.yaml.example file and replace the placeholders with your actual issued key values before use.

kollus:
security-key: "YOUR_SECURITY_KEY" # Kollus VOD Console > [Settings] > [Security Key]
custom-key: "YOUR_CUSTOM_KEY" # Kollus VOD Console > [Settings] > [Custom Key]

JWT signing implementation​

This service signs callback response JWTs based on HS256 (HMAC-SHA256) using standard Java security APIs without requiring external JWT libraries. (Methods unrelated to callbacks, such as player token generation, have been omitted.)

package com.example.kollus.service;

import org.springframework.stereotype.Service;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Map;

/**
* Kollus player JWT token generation and signing utility.
* Encodes and signs Kollus callback response payloads directly using the HS256 (HMAC-SHA256) algorithm without external JWT libraries.
*/
@Service
public class KollusTokenService {

/** HMAC-SHA256 signature algorithm identifier */
private static final String HMAC_ALGORITHM = "HmacSHA256";
/** Fixed JWT header specification (typ: JWT, alg: HS256) */
private static final Map<String, Object> JWT_HEADER = Map.of("typ", "JWT", "alg", "HS256");

private final ObjectMapper objectMapper;
private final Base64.Encoder base64Url = Base64.getUrlEncoder().withoutPadding();

public KollusTokenService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

/**
* Signs the JWT for Kollus callback responses using the HS256 algorithm.
*
* @param payload JWT payload object
* @param securityKey Security key used for HMAC signing
* @return Base64URL-encoded signed JWT string
*/
public String signCallbackJwt(Object payload, String securityKey) {
return encodeJwt(payload, securityKey);
}

/**
* Generates a standard JWT string in the format {@code header.payload.signature}.
*/
private String encodeJwt(Object payload, String securityKey) {
String header = base64UrlJson(JWT_HEADER);
String body = base64UrlJson(payload);
String content = header + "." + body;
String signature = sign(content, securityKey);
return content + "." + signature;
}

/**
* Serializes an object into JSON and encodes it into Base64URL format (without padding).
*/
private String base64UrlJson(Object value) {
try {
byte[] json = objectMapper.writeValueAsBytes(value);
return base64Url.encodeToString(json);
} catch (JacksonException e) {
// Serialization failure is a runtime defect; rethrow wrapped as an unchecked exception (IllegalStateException)
throw new IllegalStateException("JWT payload serialization failed", e);
}
}

/**
* Signs the content using the HMAC-SHA256 algorithm and returns it as a Base64URL string.
*/
private String sign(String content, String securityKey) {
try {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(new SecretKeySpec(securityKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM));
byte[] raw = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
return base64Url.encodeToString(raw);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
// HmacSHA256 is mandatory across standard JVMs; this should not occur in a normal runtime environment
throw new IllegalStateException("JWT signing failed", e);
}
}
}

DRM download callback controller implementation​

This controller receives the POST /callback/drm request with the items parameter (a JSON array string) from the Kollus server, constructs authorization response data for each item, and returns it as a signed JWT wrapped in a data array. (Other callback handling methods have been omitted.)

package com.example.kollus.controller;

import com.example.kollus.config.KollusProperties;
import com.example.kollus.service.KollusTokenService;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import tools.jackson.core.JacksonException;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;

import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Controller for handling Kollus callbacks.
* Receives Play, DRM, Next Episode, and LMS callback requests and returns compliant responses.
*/
@RestController
@RequestMapping("/callback")
public class CallbackController {

/** DRM content expiration period (30 days, in seconds) */
private static final long CONTENT_EXPIRE_SEC = 60L * 60 * 24 * 30;
/** Kollus callback response header name (for passing custom key) */
private static final String USERKEY_HEADER = "X-KOLLUS-USERKEY";
/** Callback kind 1: Grant & set expiration (DRM callback: download approval & expiration setup) */
private static final int KIND_GRANT_WITH_EXPIRATION = 1;
/** Callback kind 2: Download completed notification (DRM callback exclusive) */
private static final int KIND_DOWNLOAD_COMPLETED = 2;
/** Callback kind 3: Final confirmation of offline playback permission */
private static final int KIND_PLAYBACK_APPROVAL = 3;

private final KollusTokenService tokenService;
private final KollusProperties properties;
private final ObjectMapper objectMapper;

public CallbackController(KollusTokenService tokenService,
KollusProperties properties,
ObjectMapper objectMapper) {
this.tokenService = tokenService;
this.properties = properties;
this.objectMapper = objectMapper;
}

/**
* POST /callback/drm — Handles DRM download and offline playback permission callback requests.
* Parses the items parameter (JSON array string) in the request body to construct response data for each content item, returning it as a JWT.
*
* @param itemsJson items parameter (JSON array string containing kind, media_content_key, start_at, etc.)
* @return X-KOLLUS-USERKEY header and signed JWT body (text/plain) containing the data array; 400 Bad Request on parse failure
*/
@PostMapping("/drm")
public ResponseEntity<String> drm(@RequestParam(value = "items", required = false) String itemsJson) {
List> items;
try {
items = objectMapper.readValue(itemsJson, new TypeReference<>() {
});
} catch (JacksonException | IllegalArgumentException e) {
// Return 400 Bad Request if items parameter deserialization fails
return ResponseEntity.badRequest().body("Invalid items");
}

long now = Instant.now().getEpochSecond();
List> data = new ArrayList<>();

for (Map<String, Object> item : items) {
Integer kind = parseIntOrNull(String.valueOf(item.get("kind")));
Object mediaContentKey = item.get("media_content_key");

if (kind == null) {
continue;
}
Map<String, Object> entry = new LinkedHashMap<>();
if (kind == KIND_GRANT_WITH_EXPIRATION) {
// Callback kind 1: Approve download and set playback expiration timestamp
entry.put("kind", KIND_GRANT_WITH_EXPIRATION);
entry.put("media_content_key", mediaContentKey);
entry.put("expiration_date", now + CONTENT_EXPIRE_SEC);
entry.put("result", 1);
data.add(entry);
} else if (kind == KIND_DOWNLOAD_COMPLETED) {
// Callback kind 2: Acknowledge download completion notification
entry.put("kind", KIND_DOWNLOAD_COMPLETED);
entry.put("media_content_key", mediaContentKey);
entry.put("result", 1);
data.add(entry);
} else if (kind == KIND_PLAYBACK_APPROVAL) {
// Callback kind 3: Final confirmation of offline playback permission
entry.put("kind", KIND_PLAYBACK_APPROVAL);
entry.put("media_content_key", mediaContentKey);
entry.put("start_at", item.get("start_at"));
entry.put("result", 1);
data.add(entry);
}
}

return callbackJwt(Map.of("data", data));
}

/** Returns a response consisting of the X-KOLLUS-USERKEY header and a signed JWT body according to Kollus callback specifications. */
private ResponseEntity<String> callbackJwt(Object payload) {
String jwt = tokenService.signCallbackJwt(payload, properties.securityKey());
return ResponseEntity.ok()
.header(USERKEY_HEADER, properties.customKey())
.contentType(MediaType.TEXT_PLAIN)
.body(jwt);
}

/** Converts a string to an integer, returning null if the format is invalid or conversion fails. */
private Integer parseIntOrNull(String value) {
if (value == null) {
return null;
}
try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException e) {
// Return null on numeric parsing failure to safely skip processing this item
return null;
}
}
}