From fc28eed8896035ec7715daf4ae5c5b306a5c608b Mon Sep 17 00:00:00 2001 From: lastpeony Date: Thu, 23 Feb 2023 16:24:54 +0300 Subject: [PATCH 1/5] issue #4809 jwt blacklist implementation and stop play/ publish using jwt api call --- .../antmedia/AntMediaApplicationAdapter.java | 2 + src/main/java/io/antmedia/AppSettings.java | 10 + .../io/antmedia/datastore/db/DataStore.java | 36 +++- .../datastore/db/InMemoryDataStore.java | 41 +++- .../datastore/db/MapBasedDataStore.java | 90 +++++++++ .../io/antmedia/datastore/db/MapDBStore.java | 16 +- .../io/antmedia/datastore/db/MongoStore.java | 37 +++- .../io/antmedia/datastore/db/RedisStore.java | 17 +- .../antmedia/rest/BroadcastRestService.java | 135 ++++++++++++- .../io/antmedia/rest/RestServiceBase.java | 8 + .../io/antmedia/security/ITokenService.java | 1 + .../antmedia/security/MockTokenService.java | 5 + .../integration/RestServiceV2Test.java | 154 +++++++++++++++ .../io/antmedia/test/AppSettingsUnitTest.java | 3 +- .../io/antmedia/test/db/DBStoresUnitTest.java | 103 +++++++++- .../rest/BroadcastRestServiceV2UnitTest.java | 177 +++++++++++++++++- 16 files changed, 807 insertions(+), 28 deletions(-) diff --git a/src/main/java/io/antmedia/AntMediaApplicationAdapter.java b/src/main/java/io/antmedia/AntMediaApplicationAdapter.java index 4662c54a5..7dd582c85 100644 --- a/src/main/java/io/antmedia/AntMediaApplicationAdapter.java +++ b/src/main/java/io/antmedia/AntMediaApplicationAdapter.java @@ -1508,6 +1508,8 @@ public static boolean updateAppSettingsFile(String appName, AppSettings newAppse store.put(AppSettings.SETTINGS_PUBLISH_JWT_CONTROL_ENABLED, String.valueOf(newAppsettings.isPublishJwtControlEnabled())); store.put(AppSettings.SETTINGS_PLAY_JWT_CONTROL_ENABLED, String.valueOf(newAppsettings.isPlayJwtControlEnabled())); store.put(AppSettings.SETTINGS_JWT_STREAM_SECRET_KEY, newAppsettings.getJwtStreamSecretKey() != null ? newAppsettings.getJwtStreamSecretKey() : ""); + store.put(AppSettings.SETTINGS_JWT_BLACKLIST_ENABLED, String.valueOf(newAppsettings.isJwtBlacklistEnabled())); + store.put(AppSettings.SETTINGS_WEBRTC_ENABLED, String.valueOf(newAppsettings.isWebRTCEnabled())); store.put(AppSettings.SETTINGS_WEBRTC_FRAME_RATE, String.valueOf(newAppsettings.getWebRTCFrameRate())); diff --git a/src/main/java/io/antmedia/AppSettings.java b/src/main/java/io/antmedia/AppSettings.java index 6d9dda2bc..1b1417e27 100644 --- a/src/main/java/io/antmedia/AppSettings.java +++ b/src/main/java/io/antmedia/AppSettings.java @@ -265,6 +265,7 @@ public class AppSettings implements Serializable{ public static final String SETTINGS_JWT_SECRET_KEY = "settings.jwtSecretKey"; public static final String SETTINGS_JWT_CONTROL_ENABLED = "settings.jwtControlEnabled"; + public static final String SETTINGS_JWT_BLACKLIST_ENABLED = "settings.jwtBlacklistEnabled"; public static final String SETTINGS_IP_FILTER_ENABLED = "settings.ipFilterEnabled"; @@ -1283,6 +1284,8 @@ public class AppSettings implements Serializable{ @Value( "${"+SETTINGS_JWT_CONTROL_ENABLED+":false}" ) private boolean jwtControlEnabled; + @Value( "${"+SETTINGS_JWT_BLACKLIST_ENABLED+":false}" ) + private boolean jwtBlacklistEnabled; /** * Application IP Filter Enabled */ @@ -2600,6 +2603,13 @@ public boolean isJwtControlEnabled() { return jwtControlEnabled; } + public void setJwtBlacklistEnabled(boolean jwtBlacklistEnabled){ + this.jwtBlacklistEnabled = jwtBlacklistEnabled; + } + + public boolean isJwtBlacklistEnabled() { + return jwtBlacklistEnabled; + } public void setJwtControlEnabled(boolean jwtControlEnabled) { this.jwtControlEnabled = jwtControlEnabled; } diff --git a/src/main/java/io/antmedia/datastore/db/DataStore.java b/src/main/java/io/antmedia/datastore/db/DataStore.java index 37234eab1..8a1b59cf8 100644 --- a/src/main/java/io/antmedia/datastore/db/DataStore.java +++ b/src/main/java/io/antmedia/datastore/db/DataStore.java @@ -10,6 +10,8 @@ import java.util.List; import java.util.Map; +import io.antmedia.rest.model.Result; +import io.antmedia.security.ITokenService; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; @@ -424,6 +426,27 @@ public List listAllTokens (Map tokenMap, String streamId, public abstract boolean deleteToken (String tokenId); + /** + * Delete specific token from blacklist. + * @param tokenId id of the token + */ + public abstract boolean deleteTokenFromBlacklist (String tokenId); + + /** + * Get all tokens from jwt blacklist. + */ + public abstract List getJwtBlacklist(); + + /** + * Delete all expired tokens from jwt blacklist. + */ + public abstract Result deleteAllExpiredJwtFromBlacklist(ITokenService tokenService); + + /** + * Delete all tokens from jwt blacklist. + */ + public abstract void clearJwtBlacklist(); + /** * retrieve specific token * @param tokenId id of the token @@ -1352,7 +1375,18 @@ public List getWebRTCViewerList(Map webRTCView * @param metaData new meta data */ public abstract boolean updateStreamMetaData(String streamId, String metaData); - + + /** + * Add jwt token to black list. + * @param token which will be added to black list. + */ + public abstract boolean addTokenToBlacklist(Token token); + + /** + * Get token from black list. + * @param tokenId id of the token. + */ + public abstract Token getTokenFromBlacklist(String tokenId); //************************************** //ATTENTION: Write function descriptions while adding new functions diff --git a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java index 400efc1de..5ea91a003 100644 --- a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java +++ b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java @@ -2,15 +2,12 @@ import java.io.File; import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; import java.util.regex.Pattern; + +import io.antmedia.rest.model.Result; +import io.antmedia.security.ITokenService; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -900,6 +897,26 @@ public boolean deleteToken(String tokenId) { } + @Override + public boolean deleteTokenFromBlacklist(String tokenId) { + return false; + } + + @Override + public List getJwtBlacklist() { + return Collections.emptyList(); + } + + @Override + public Result deleteAllExpiredJwtFromBlacklist(ITokenService tokenService) { + return null; + } + + @Override + public void clearJwtBlacklist() { + throw new UnsupportedOperationException("JWT blacklist must be stored as map based db on disk, not in memory."); + } + @Override public Token getToken(String tokenId) { @@ -1022,4 +1039,14 @@ public boolean updateStreamMetaData(String streamId, String metaData) { } return result; } + + @Override + public boolean addTokenToBlacklist(Token token) { + return false; + } + + @Override + public Token getTokenFromBlacklist(String tokenId) { + return null; + } } \ No newline at end of file diff --git a/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java b/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java index 44065e25a..1d25414df 100644 --- a/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java +++ b/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java @@ -9,8 +9,11 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; +import io.antmedia.rest.model.Result; +import io.antmedia.security.ITokenService; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -41,6 +44,8 @@ public abstract class MapBasedDataStore extends DataStore { protected Map vodMap; protected Map detectionMap; protected Map tokenMap; + protected Map tokenBlacklistMap; + protected Map subscriberMap; protected Map conferenceRoomMap; protected Map webRTCViewerMap; @@ -943,6 +948,65 @@ public boolean deleteToken(String tokenId) { return result; } + @Override + public boolean deleteTokenFromBlacklist(String tokenId) { + boolean result; + + synchronized (this) { + result = tokenBlacklistMap.remove(tokenId) != null; + } + return result; + } + + @Override + public List getJwtBlacklist(){ + + synchronized (this){ + return new ArrayList<>(tokenBlacklistMap.keySet()); + + } + + } + + @Override + public Result deleteAllExpiredJwtFromBlacklist(ITokenService tokenService){ + logger.info("Deleting all expired JWTs from black list."); + AtomicInteger deletedTokenCount = new AtomicInteger(); + + synchronized (this){ + tokenBlacklistMap.forEach((key, value) -> { + Token token = gson.fromJson(value,Token.class); + String tokenId = token.getTokenId(); + if(!tokenService.verifyJwt(tokenId,token.getStreamId(),token.getType())){ + if(deleteTokenFromBlacklist(tokenId)){ + deletedTokenCount.getAndIncrement(); + }else{ + logger.warn("Couldn't delete JWT:{}", tokenId); + } + } + }); + } + + if(deletedTokenCount.get() > 0){ + final String successMsg = deletedTokenCount+" JWT deleted successfully from black list."; + logger.info(successMsg); + return new Result(true, successMsg); + }else{ + final String failMsg = "No JWT deleted from black list."; + logger.warn(failMsg); + return new Result(false, failMsg); + + } + + } + + @Override + public void clearJwtBlacklist(){ + synchronized (this) { + tokenBlacklistMap.clear(); + } + } + @Override public Token getToken(String tokenId) { return super.getToken(tokenMap, tokenId, gson); @@ -1055,4 +1119,30 @@ public Broadcast getBroadcastFromMap(String streamId) return null; } + @Override + public boolean addTokenToBlacklist(Token token) { + boolean result = false; + + synchronized (this) { + + if (token.getStreamId() != null && token.getTokenId() != null) { + + try { + tokenBlacklistMap.put(token.getTokenId(), gson.toJson(token)); + result = true; + } catch (Exception e) { + logger.error(ExceptionUtils.getStackTrace(e)); + } + } + } + return result; + + } + + @Override + public Token getTokenFromBlacklist(String tokenId) { + return super.getToken(tokenBlacklistMap, tokenId, gson); + + } + } diff --git a/src/main/java/io/antmedia/datastore/db/MapDBStore.java b/src/main/java/io/antmedia/datastore/db/MapDBStore.java index e3de68fe0..87eced277 100644 --- a/src/main/java/io/antmedia/datastore/db/MapDBStore.java +++ b/src/main/java/io/antmedia/datastore/db/MapDBStore.java @@ -4,10 +4,7 @@ import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import java.util.Set; -import java.util.Map.Entry; import org.apache.commons.lang3.exception.ExceptionUtils; import org.mapdb.DB; @@ -16,9 +13,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.antmedia.datastore.db.types.Broadcast; import io.antmedia.datastore.db.types.StreamInfo; -import io.antmedia.muxer.IAntMediaStreamHandler; import io.vertx.core.Vertx; @@ -33,6 +28,8 @@ public class MapDBStore extends MapBasedDataStore { private static final String VOD_MAP_NAME = "VOD"; private static final String DETECTION_MAP_NAME = "DETECTION"; private static final String TOKEN = "TOKEN"; + private static final String TOKEN_BLACKLIST = "TOKEN_BLACKLIST"; + private static final String SUBSCRIBER = "SUBSCRIBER"; private static final String CONFERENCE_ROOM_MAP_NAME = "CONFERENCE_ROOM"; private static final String WEBRTC_VIEWER = "WEBRTC_VIEWER"; @@ -73,6 +70,10 @@ public MapDBStore(String dbName, Vertx vertx) { webRTCViewerMap = db.treeMap(WEBRTC_VIEWER).keySerializer(Serializer.STRING).valueSerializer(Serializer.STRING) .counterEnable().createOrOpen(); + tokenBlacklistMap = db.treeMap(TOKEN_BLACKLIST).keySerializer(Serializer.STRING).valueSerializer(Serializer.STRING) + .counterEnable().createOrOpen(); + + timerId = vertx.setPeriodic(5000, id -> vertx.executeBlocking(b -> { @@ -124,7 +125,10 @@ public void close(boolean deleteDB) { public void clearStreamInfoList(String streamId) { //used in mongo for cluster mode. useless here. } - + + + + @Override public List getStreamInfoList(String streamId) { return new ArrayList<>(); diff --git a/src/main/java/io/antmedia/datastore/db/MongoStore.java b/src/main/java/io/antmedia/datastore/db/MongoStore.java index 4c87a848e..f555b8e85 100644 --- a/src/main/java/io/antmedia/datastore/db/MongoStore.java +++ b/src/main/java/io/antmedia/datastore/db/MongoStore.java @@ -9,9 +9,12 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.regex.Pattern; +import io.antmedia.rest.model.Result; +import io.antmedia.security.ITokenService; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -94,7 +97,6 @@ public MongoStore(String host, String username, String password, String dbName) subscriberDatastore = Morphia.createDatastore(mongoClient, dbName + "_subscriber"); detectionMap = Morphia.createDatastore(mongoClient, dbName + "detection"); conferenceRoomDatastore = Morphia.createDatastore(mongoClient, dbName + "room"); - //************************************************* //do not create data store for each type as we do above //************************************************* @@ -107,7 +109,7 @@ public MongoStore(String host, String username, String password, String dbName) vodDatastore.getMapper().mapPackage("io.antmedia.datastore.db.types"); detectionMap.getMapper().mapPackage("io.antmedia.datastore.db.types"); conferenceRoomDatastore.getMapper().mapPackage("io.antmedia.datastore.db.types"); - + tokenDatastore.ensureIndexes(); subscriberDatastore.ensureIndexes(); datastore.ensureIndexes(); @@ -1196,6 +1198,26 @@ public boolean deleteToken(String tokenId) { return result; } + @Override + public boolean deleteTokenFromBlacklist(String tokenId) { + return false; + } + + @Override + public List getJwtBlacklist() { + return Collections.emptyList(); + } + + @Override + public Result deleteAllExpiredJwtFromBlacklist(ITokenService tokenService) { + return null; + } + + @Override + public void clearJwtBlacklist() { + throw new UnsupportedOperationException("JWT blacklist must be stored as map based db on disk, not in mongodb."); + } + @Override public Token getToken(String tokenId) { Token token = null; @@ -1429,4 +1451,15 @@ public boolean updateStreamMetaData(String streamId, String metaData) { } return false; } + + @Override + public boolean addTokenToBlacklist(Token token) { + + return false; + } + + @Override + public Token getTokenFromBlacklist(String tokenId) { + return null; + } } diff --git a/src/main/java/io/antmedia/datastore/db/RedisStore.java b/src/main/java/io/antmedia/datastore/db/RedisStore.java index 4208e7fbb..0213d06f4 100644 --- a/src/main/java/io/antmedia/datastore/db/RedisStore.java +++ b/src/main/java/io/antmedia/datastore/db/RedisStore.java @@ -6,9 +6,8 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; -import java.util.Set; -import java.util.Map.Entry; +import io.antmedia.datastore.db.types.Token; import org.apache.commons.lang3.exception.ExceptionUtils; import org.redisson.Redisson; import org.redisson.api.RMap; @@ -17,10 +16,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.antmedia.datastore.db.types.Broadcast; import io.antmedia.datastore.db.types.P2PConnection; import io.antmedia.datastore.db.types.StreamInfo; -import io.antmedia.muxer.IAntMediaStreamHandler; public class RedisStore extends MapBasedDataStore { @@ -123,6 +120,18 @@ public int resetBroadcasts(String hostAddress) { } } + @Override + public boolean addTokenToBlacklist(Token token) { + + return false; + } + + @Override + public Token getTokenFromBlacklist(String tokenId) { + + return null; + } + @Override public List getStreamInfoList(String streamId) { diff --git a/src/main/java/io/antmedia/rest/BroadcastRestService.java b/src/main/java/io/antmedia/rest/BroadcastRestService.java index 9ca381d0b..74645cb0b 100644 --- a/src/main/java/io/antmedia/rest/BroadcastRestService.java +++ b/src/main/java/io/antmedia/rest/BroadcastRestService.java @@ -1,8 +1,16 @@ package io.antmedia.rest; +import java.nio.charset.Charset; +import java.util.Base64; import java.util.List; +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.interfaces.DecodedJWT; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; import org.springframework.stereotype.Component; import io.antmedia.AntMediaApplicationAdapter; @@ -40,6 +48,7 @@ import io.swagger.annotations.Info; import io.swagger.annotations.License; import io.swagger.annotations.SwaggerDefinition; + import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -78,6 +87,9 @@ public class BroadcastRestService extends RestServiceBase{ private static final String ABSOLUTE_MOVE = "absolute"; private static final String CONTINUOUS_MOVE = "continuous"; + private final String blacklistNotEnabledMsg = "JWT black list is not enabled for this application."; + + @ApiModel(value="SimpleStat", description="Simple generic statistics class to return single values") public static class SimpleStat { @ApiModelProperty(value = "the stat value") @@ -594,8 +606,127 @@ public Result validateTokenV2(@ApiParam(value = "Token to be validated", require return new Result(result); } + @ApiOperation(value = "Add jwt token to black list. If added, success field is true, " + + "if not added success field false", response = Result.class) + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("/jwt-black-list") + @Produces(MediaType.APPLICATION_JSON) + public Result addJwtToBlacklist(@ApiParam(value = "jwt to be added to black list.", required = true) @QueryParam("jwt") String jwt) + { + if(getAppSettings().isJwtBlacklistEnabled()){ + try{ + final DecodedJWT decodedJWT = JWT.decode(jwt); + final String payload = new String(Base64.getDecoder().decode(decodedJWT.getPayload()), Charset.defaultCharset()); + final JSONParser parser = new JSONParser(); + try{ + final JSONObject jwtPayload = (JSONObject) parser.parse(payload); + final String tokenType = jwtPayload.get("type").toString(); + final String streamId = jwtPayload.get("streamId").toString(); + final long exp = (Long) jwtPayload.get("exp"); + + final Token token = new Token(); + token.setTokenId(jwt); + token.setType(tokenType); + token.setStreamId(streamId); + token.setExpireDate(exp); + + if(!super.verifyJwt(jwt, streamId, tokenType)){ + return new Result(false,"JWT is not valid."); + }else if(getDataStore().getTokenFromBlacklist(token.getTokenId()) != null){ + return new Result(false, "JWT is already in blacklist."); + }else if(getDataStore().addTokenToBlacklist(token)){ + return new Result(true, "JWT successfully added to blacklist."); + } + + }catch (ParseException e) { + return new Result(false,"Invalid JWT"); + } + }catch (JWTDecodeException e){ + return new Result(false,"Invalid JWT"); + } + + }else{ + logger.warn(blacklistNotEnabledMsg); + return new Result(false, blacklistNotEnabledMsg); + } + + return new Result(false); + } - @ApiOperation(value = " Removes all tokens related with requested stream", notes = "", response = Result.class) + @ApiOperation(value = "Delete jwt from black list. If deleted, success field is true, " + + "if not deleted success field false.", response = Result.class) + @DELETE + @Consumes(MediaType.APPLICATION_JSON) + @Path("/jwt-black-list") + @Produces(MediaType.APPLICATION_JSON) + public Result deleteJwtFromBlacklist(@ApiParam(value = "jwt to be deleted from black list.", required = true) @QueryParam("jwt") String jwt) + { + if(getAppSettings().isJwtBlacklistEnabled()){ + if(getDataStore().getTokenFromBlacklist(jwt) == null){ + return new Result(false, "JWT does not exist in black list."); + + }else if(getDataStore().deleteTokenFromBlacklist(jwt)){ + return new Result(true, "JWT successfully deleted from black list."); + + }else{ + return new Result(false, "JWT cannot be deleted from black list."); + } + }else{ + logger.warn(blacklistNotEnabledMsg); + return new Result(false, blacklistNotEnabledMsg); + } + } + + @ApiOperation(value = "Get all JWTs from blacklist.", response = Result.class) + @GET + @Path("/jwt-black-list") + @Produces(MediaType.APPLICATION_JSON) + public List getJwtBlacklist() + { + if(getAppSettings().isJwtBlacklistEnabled()) { + return getDataStore().getJwtBlacklist(); + }else{ + logger.warn(blacklistNotEnabledMsg); + return null; + } + } + + @ApiOperation(value = "Delete all expired JWTs from black list.", response = Result.class) + @DELETE + @Path("/jwt-black-list-delete-expired") + @Produces(MediaType.APPLICATION_JSON) + public Result deleteAllExpiredJwtFromBlacklist() + { + if(getAppSettings().isJwtBlacklistEnabled()) { + return getDataStore().deleteAllExpiredJwtFromBlacklist(getTokenService()); + }else{ + logger.warn(blacklistNotEnabledMsg); + return new Result(false, blacklistNotEnabledMsg); + } + } + + @ApiOperation(value = "Delete all JWTs from black list.", response = Result.class) + @DELETE + @Path("/jwt-black-list-clear") + @Produces(MediaType.APPLICATION_JSON) + public Result clearJwtBlacklist() + { + if(getAppSettings().isJwtBlacklistEnabled()) { + getDataStore().clearJwtBlacklist(); + if(getDataStore().getJwtBlacklist().isEmpty()){ + return new Result(true, "JWT black list cleared successfully"); + }else{ + return new Result(false, "JWT black list clear failed."); + } + }else{ + logger.warn(blacklistNotEnabledMsg); + return new Result(false, blacklistNotEnabledMsg); + } + + } + + @ApiOperation(value = "Removes all tokens related with requested stream", notes = "", response = Result.class) @DELETE @Consumes(MediaType.APPLICATION_JSON) @Path("/{id}/tokens") @@ -604,7 +735,6 @@ public Result revokeTokensV2(@ApiParam(value = "the id of the stream", required return super.revokeTokens(streamId); } - @ApiOperation(value = "Get the all tokens of requested stream", notes = "",responseContainer = "List", response = Token.class) @GET @Path("/{id}/tokens/list/{offset}/{size}") @@ -1133,6 +1263,5 @@ public Result stopPlaying(@ApiParam(value = "the id of the webrtc viewer.", requ boolean result = getApplication().stopPlaying(viewerId); return new Result(result); } - } diff --git a/src/main/java/io/antmedia/rest/RestServiceBase.java b/src/main/java/io/antmedia/rest/RestServiceBase.java index 633802ddf..0668d94b0 100644 --- a/src/main/java/io/antmedia/rest/RestServiceBase.java +++ b/src/main/java/io/antmedia/rest/RestServiceBase.java @@ -1616,6 +1616,14 @@ protected Object getJwtToken (String streamId, long expireDate, String type, Str return new Result(false, message); } + protected boolean verifyJwt(String jwt, String streamId, String type){ + return getTokenService().verifyJwt(jwt, streamId, type); + } + + protected ITokenService getTokenService(){ + return (ITokenService) getAppContext().getBean(ITokenService.BeanName.TOKEN_SERVICE.toString()); + } + protected Token validateToken (Token token) { Token validatedToken = null; diff --git a/src/main/java/io/antmedia/security/ITokenService.java b/src/main/java/io/antmedia/security/ITokenService.java index e7c36b3ae..26390d128 100644 --- a/src/main/java/io/antmedia/security/ITokenService.java +++ b/src/main/java/io/antmedia/security/ITokenService.java @@ -100,4 +100,5 @@ public String toString() { Map getSubscriberAuthenticatedMap(); + boolean verifyJwt(String jwtTokenId, String streamId, String type); } diff --git a/src/main/java/io/antmedia/security/MockTokenService.java b/src/main/java/io/antmedia/security/MockTokenService.java index 416192ba4..f22eb9bfa 100644 --- a/src/main/java/io/antmedia/security/MockTokenService.java +++ b/src/main/java/io/antmedia/security/MockTokenService.java @@ -43,6 +43,11 @@ public Map getSubscriberAuthenticatedMap() { return subscriberAuthenticatedMap; } + @Override + public boolean verifyJwt(String jwtTokenId, String streamId, String type) { + return false; + } + @Override public boolean checkJwtToken(String jwtTokenId, String streamId, String type) { return true; diff --git a/src/test/java/io/antmedia/integration/RestServiceV2Test.java b/src/test/java/io/antmedia/integration/RestServiceV2Test.java index 83157d4eb..4d846c57b 100644 --- a/src/test/java/io/antmedia/integration/RestServiceV2Test.java +++ b/src/test/java/io/antmedia/integration/RestServiceV2Test.java @@ -42,6 +42,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntityBuilder; @@ -1997,4 +1998,157 @@ public Result callIsEnterpriseEdition() throws Exception { } + @Test + public void testJwtBlacklist(){ + ConsoleAppRestServiceTest.resetCookieStore(); + + try { + Result result = ConsoleAppRestServiceTest.callisFirstLogin(); + + if (result.isSuccess()) { + Result createInitialUser = ConsoleAppRestServiceTest.createDefaultInitialUser(); + assertTrue(createInitialUser.isSuccess()); + } + + result = ConsoleAppRestServiceTest.authenticateDefaultUser(); + assertTrue(result.isSuccess()); + + + boolean isEnterprise = callIsEnterpriseEdition().getMessage().contains("Enterprise"); + if(!isEnterprise) { + logger.info("This is not enterprise edition so skipping this test"); + return; + } + + final AppSettings appSettingsModel = ConsoleAppRestServiceTest.callGetAppSettings("LiveApp"); + appSettingsModel.setJwtStreamSecretKey("testtesttesttesttesttesttesttest"); + appSettingsModel.setJwtBlacklistEnabled(true); + + result = ConsoleAppRestServiceTest.callSetAppSettings("LiveApp", appSettingsModel); + assertTrue(result.isSuccess()); + + final String clearJwtBlacklistUrl = ROOT_SERVICE_URL + "/v2/broadcasts/jwt-black-list-clear"; + final String jwtBlacklistUrl = ROOT_SERVICE_URL + "/v2/broadcasts/jwt-black-list"; + + CloseableHttpClient client = HttpClients.custom().setRedirectStrategy(new LaxRedirectStrategy()).build(); + + HttpUriRequest clearJwtBlacklistRequest = RequestBuilder.delete().setUri(clearJwtBlacklistUrl).build(); + HttpResponse clearJwtBlacklistResponse = client.execute(clearJwtBlacklistRequest); + StringBuffer clearJwtBlacklistResult = readResponse(clearJwtBlacklistResponse); + + if (clearJwtBlacklistResponse.getStatusLine().getStatusCode() != 200) { + throw new Exception(clearJwtBlacklistResult.toString()); + } + result = gson.fromJson(clearJwtBlacklistResult.toString(), Result.class); + + assertTrue(result.isSuccess()); + + final String validJwt1 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicHVibGlzaCIsImV4cCI6OTg4NzUwNzUwMH0.aZRIBC6zHDPw3od9tBCn9gGg3Taab8RpuPUxGr46YM8"; + final String validJwt2 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicHVibGlzaCIsImV4cCI6OTg4NzUwNzUwMX0.f4YTJUOmO7yuGpD7W4i_fffv2IVi1JB3mZVxNv8LSdI"; + final String validJwt3 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicHVibGlzaCIsImV4cCI6OTg4NzUwNzUwMn0.bSbIumeAeM-k5zLndtII49z_458L8Lqg3eVweahvpb4"; + + HttpUriRequest addJwtRequest1 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt1).build().toString()) + .setHeader(HttpHeaders.CONTENT_TYPE, "application/json").build(); + + HttpUriRequest addJwtRequest2 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt2).build().toString()) + .setHeader(HttpHeaders.CONTENT_TYPE, "application/json").build(); + + HttpUriRequest addJwtRequest3 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt3).build().toString()) + .setHeader(HttpHeaders.CONTENT_TYPE, "application/json").build(); + + HttpResponse addJwtResponse1 = client.execute(addJwtRequest1); + + StringBuffer addJwtResult1 = readResponse(addJwtResponse1); + + if (addJwtResponse1.getStatusLine().getStatusCode() != 200) { + throw new Exception(addJwtResult1.toString()); + } + + result = gson.fromJson(addJwtResult1.toString(), Result.class); + assertTrue(result.isSuccess()); + + HttpResponse addJwtResponse2 = client.execute(addJwtRequest2); + + StringBuffer addJwtResult2 = readResponse(addJwtResponse2); + + if (addJwtResponse2.getStatusLine().getStatusCode() != 200) { + throw new Exception(addJwtResult2.toString()); + } + + result = gson.fromJson(addJwtResult2.toString(), Result.class); + assertTrue(result.isSuccess()); + + HttpResponse addJwtResponse3 = client.execute(addJwtRequest3); + + StringBuffer addJwtResult3 = readResponse(addJwtResponse3); + + if (addJwtResponse3.getStatusLine().getStatusCode() != 200) { + throw new Exception(addJwtResult3.toString()); + } + + result = gson.fromJson(addJwtResult3.toString(), Result.class); + assertTrue(result.isSuccess()); + + HttpUriRequest getJwtBlacklistRequest = RequestBuilder.get().setUri(jwtBlacklistUrl) + .setHeader(HttpHeaders.CONTENT_TYPE, "application/json").build(); + + HttpResponse getJwtBlacklistRequestResponse = client.execute(getJwtBlacklistRequest); + + StringBuffer getJwtBlacklistRequestResult = readResponse(getJwtBlacklistRequestResponse); + + ArrayList jwtBlacklist = gson.fromJson(getJwtBlacklistRequestResult.toString(), ArrayList.class); + + int expectedJwtCount = 3; + + assertEquals(expectedJwtCount, jwtBlacklist.size()); + + + HttpUriRequest deleteJwtFromBlacklistRequest = RequestBuilder.delete().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt1).build()).build(); + HttpResponse deleteJwtFromBlacklistResponse = client.execute(deleteJwtFromBlacklistRequest); + StringBuffer deleteJwtFromBlacklistResult = readResponse(deleteJwtFromBlacklistResponse); + + if (deleteJwtFromBlacklistResponse.getStatusLine().getStatusCode() != 200) { + throw new Exception(deleteJwtFromBlacklistResult.toString()); + } + result = gson.fromJson(deleteJwtFromBlacklistResult.toString(), Result.class); + + assertTrue(result.isSuccess()); + + getJwtBlacklistRequestResponse = client.execute(getJwtBlacklistRequest); + + getJwtBlacklistRequestResult = readResponse(getJwtBlacklistRequestResponse); + + jwtBlacklist = gson.fromJson(getJwtBlacklistRequestResult.toString(), ArrayList.class); + + expectedJwtCount = 2; + + assertEquals(expectedJwtCount, jwtBlacklist.size()); + + clearJwtBlacklistResponse = client.execute(clearJwtBlacklistRequest); + clearJwtBlacklistResult = readResponse(clearJwtBlacklistResponse); + + if (clearJwtBlacklistResponse.getStatusLine().getStatusCode() != 200) { + throw new Exception(clearJwtBlacklistResult.toString()); + } + result = gson.fromJson(clearJwtBlacklistResult.toString(), Result.class); + + assertTrue(result.isSuccess()); + + getJwtBlacklistRequestResponse = client.execute(getJwtBlacklistRequest); + + getJwtBlacklistRequestResult = readResponse(getJwtBlacklistRequestResponse); + + jwtBlacklist = gson.fromJson(getJwtBlacklistRequestResult.toString(), ArrayList.class); + + expectedJwtCount = 0; + + assertEquals(expectedJwtCount, jwtBlacklist.size()); + + + } catch (Exception e){ + logger.error(e.toString()); + } + + } + } diff --git a/src/test/java/io/antmedia/test/AppSettingsUnitTest.java b/src/test/java/io/antmedia/test/AppSettingsUnitTest.java index 61ae76bf1..2a84dd296 100644 --- a/src/test/java/io/antmedia/test/AppSettingsUnitTest.java +++ b/src/test/java/io/antmedia/test/AppSettingsUnitTest.java @@ -479,6 +479,7 @@ public void testUnsetAppSettings() { assertEquals(-1, appSettings.getMaxVideoTrackCount()); assertEquals(2, appSettings.getOriginEdgeIdleTimeout()); assertEquals(false, appSettings.isAddDateTimeToHlsFileName()); + assertEquals(false, appSettings.isJwtControlEnabled()); @@ -486,7 +487,7 @@ public void testUnsetAppSettings() { //When a new field is added or removed please update the number of fields and make this test pass //by also checking its default value. assertEquals("New field is added to settings. PAY ATTENTION: Please CHECK ITS DEFAULT VALUE and fix the number of fields.", - 157, numberOfFields); + 158, numberOfFields); } diff --git a/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java b/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java index c8b74800d..b506358d3 100644 --- a/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java +++ b/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java @@ -7,6 +7,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.File; import java.io.IOException; @@ -19,6 +21,8 @@ import java.util.Random; import java.util.concurrent.TimeUnit; +import io.antmedia.rest.model.Result; +import io.antmedia.security.ITokenService; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; import org.awaitility.Awaitility; @@ -134,6 +138,8 @@ public void testMapDBStore() { testWebRTCViewerOperations(dataStore); testUpdateMetaData(dataStore); testStreamSourceList(dataStore); + testDeleteAllExpiredJwtFromBlacklist(dataStore); + testClearJwtBlacklist(dataStore); } @@ -2100,9 +2106,9 @@ private DataStore createDB(String type, boolean writeStats) { dsf.setDbType(type); dsf.setDbName("testdb"); dsf.setDbHost("localhost"); - ApplicationContext context = Mockito.mock(ApplicationContext.class); - Mockito.when(context.getBean(IAntMediaStreamHandler.VERTX_BEAN_NAME)).thenReturn(vertx); - Mockito.when(context.getBean(ServerSettings.BEAN_NAME)).thenReturn(new ServerSettings()); + ApplicationContext context = mock(ApplicationContext.class); + when(context.getBean(IAntMediaStreamHandler.VERTX_BEAN_NAME)).thenReturn(vertx); + when(context.getBean(ServerSettings.BEAN_NAME)).thenReturn(new ServerSettings()); dsf.setApplicationContext(context); return dsf.getDataStore(); } @@ -2834,4 +2840,95 @@ public void testUpdateMetaData(DataStore dataStore) { assertFalse(dataStore.updateStreamMetaData("someDummyStream"+RandomStringUtils.randomAlphanumeric(8), UPDATED_DATA)); } + + public void testDeleteAllExpiredJwtFromBlacklist(DataStore dataStore){ + + Token token1 = new Token(); + Token token2 = new Token(); + Token token3 = new Token(); + + String jwt1 = "jwt1"; + String jwt2 = "jwt2"; + String jwt3 = "jwt3"; + + String tokenType = "publish"; + String streamId = "test-stream"; + + token1.setTokenId(jwt1); + token2.setTokenId(jwt2); + token3.setTokenId(jwt3); + + token1.setType(tokenType); + token2.setType(tokenType); + token3.setType(tokenType); + + token1.setStreamId(streamId); + token2.setStreamId(streamId); + token3.setStreamId(streamId); + ITokenService tokenService = mock(ITokenService.class); + + Result res = dataStore.deleteAllExpiredJwtFromBlacklist(tokenService); + assertFalse(res.isSuccess()); + + dataStore.addTokenToBlacklist(token1); + dataStore.addTokenToBlacklist(token2); + dataStore.addTokenToBlacklist(token3); + + Token jwt = dataStore.getTokenFromBlacklist(token1.getTokenId()); + assertNotNull(jwt); + + when(tokenService.verifyJwt(jwt1,streamId,tokenType)).thenReturn(false); + when(tokenService.verifyJwt(jwt2,streamId,tokenType)).thenReturn(false); + when(tokenService.verifyJwt(jwt3,streamId,tokenType)).thenReturn(false); + + + res = dataStore.deleteAllExpiredJwtFromBlacklist(tokenService); + assertTrue(res.isSuccess()); + + } + + public void testClearJwtBlacklist(DataStore dataStore){ + addJwtsToBlacklist(dataStore); + + + List jwtBlacklist = dataStore.getJwtBlacklist(); + assertEquals(3, jwtBlacklist.size()); + + dataStore.clearJwtBlacklist(); + jwtBlacklist = dataStore.getJwtBlacklist(); + + assertEquals(0, jwtBlacklist.size()); + + + } + + private void addJwtsToBlacklist(DataStore dataStore){ + Token token1 = new Token(); + Token token2 = new Token(); + Token token3 = new Token(); + + String jwt1 = "jwt1"; + String jwt2 = "jwt2"; + String jwt3 = "jwt3"; + + String tokenType = "publish"; + String streamId = "test-stream"; + + token1.setTokenId(jwt1); + token2.setTokenId(jwt2); + token3.setTokenId(jwt3); + + token1.setType(tokenType); + token2.setType(tokenType); + token3.setType(tokenType); + + token1.setStreamId(streamId); + token2.setStreamId(streamId); + token3.setStreamId(streamId); + + dataStore.addTokenToBlacklist(token1); + dataStore.addTokenToBlacklist(token2); + dataStore.addTokenToBlacklist(token3); + } + } diff --git a/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java b/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java index 45685da5c..db8dec72e 100644 --- a/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java +++ b/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java @@ -31,6 +31,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import io.antmedia.datastore.db.MapDBStore; import org.apache.commons.lang3.RandomStringUtils; import org.awaitility.Awaitility; import org.bytedeco.ffmpeg.global.avformat; @@ -3040,5 +3041,179 @@ public void testGetCameraProfiles() { assertNull(streamSourceRest.getOnvifDeviceProfiles("invalid id")); } - + + @Test + public void testAddJwtToBlacklist() { + + AppSettings appSettings = mock(AppSettings.class); + ApplicationContext appContext = mock(ApplicationContext.class); + + ITokenService tokenService = mock(ITokenService.class); + + when(appContext.getBean(ITokenService.BeanName.TOKEN_SERVICE.toString())).thenReturn(tokenService); + + when(appSettings.isJwtBlacklistEnabled()).thenReturn(false); + + BroadcastRestService restServiceSpy = Mockito.spy(restServiceReal); + restServiceSpy.setAppCtx(appContext); + + restServiceSpy.setAppSettings(appSettings); + String jwt = "test-jwt"; + + DataStore store = mock(MapDBStore.class); + + restServiceSpy.setDataStore(store); + + + Result result = restServiceSpy.addJwtToBlacklist(jwt); + assertFalse(result.isSuccess()); + when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); + Token token = mock(Token.class); + + when(token.getTokenId()).thenReturn(jwt); + when(token.getStreamId()).thenReturn("test-stream"); + + + when(store.addTokenToBlacklist(token)).thenReturn(true); + result = restServiceSpy.addJwtToBlacklist(jwt); + assertFalse(result.isSuccess()); + String invalidJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdHJlYW1JZCI6InRlc3Qtc3RyZWFtIiwidHlwZSI6InF3ZSIsImV4cCI6OTk5OTk5OTk5OTk5OX0.DqfFkRJgKPVXgAkIzucuQtfwP2Oj-Qf9dhUuO_-04bU"; + + result = restServiceSpy.addJwtToBlacklist(invalidJwt); + assertFalse(result.isSuccess()); + + String validJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdHJlYW1JZCI6InRlc3Qtc3RyZWFtIiwidHlwZSI6InB1Ymxpc2giLCJleHAiOjk5OTk5OTk5OTk5OTl9.ichno9utOYwVv1qoQWtUpDap7PGYze-zfXRZU31CMnQ"; + + when(tokenService.verifyJwt(validJwt,"test-stream","publish")).thenReturn(true); + + when(store.addTokenToBlacklist(any())).thenReturn(true); + + result = restServiceSpy.addJwtToBlacklist(validJwt); + assertTrue(result.isSuccess()); + + when(store.getTokenFromBlacklist(validJwt)).thenReturn(token); + + result = restServiceSpy.addJwtToBlacklist(validJwt); + assertFalse(result.isSuccess()); + + } + + @Test + public void testDeleteJwtFromBlacklist() { + + AppSettings appSettings = mock(AppSettings.class); + ApplicationContext appContext = mock(ApplicationContext.class); + + ITokenService tokenService = mock(ITokenService.class); + + when(appContext.getBean(ITokenService.BeanName.TOKEN_SERVICE.toString())).thenReturn(tokenService); + + when(appSettings.isJwtBlacklistEnabled()).thenReturn(false); + + BroadcastRestService restServiceSpy = Mockito.spy(restServiceReal); + restServiceSpy.setAppCtx(appContext); + + restServiceSpy.setAppSettings(appSettings); + String jwt = "test-jwt"; + + DataStore store = mock(MapDBStore.class); + + restServiceSpy.setDataStore(store); + + Result result1 = restServiceSpy.deleteJwtFromBlacklist(jwt); + assertFalse(result1.isSuccess()); + + when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); + + when(store.getTokenFromBlacklist(jwt)).thenReturn(null); + Result result2 = restServiceSpy.deleteJwtFromBlacklist(jwt); + assertFalse(result2.isSuccess()); + + Token token = mock(Token.class); + when(store.getTokenFromBlacklist(jwt)).thenReturn(token); + Result result3 = restServiceSpy.deleteJwtFromBlacklist(jwt); + assertFalse(result3.isSuccess()); + + when(store.deleteTokenFromBlacklist(jwt)).thenReturn(true); + Result result4 = restServiceSpy.deleteJwtFromBlacklist(jwt); + assertTrue(result4.isSuccess()); + + } + + @Test + public void testGetJwtBlacklist(){ + AppSettings appSettings = mock(AppSettings.class); + ApplicationContext appContext = mock(ApplicationContext.class); + when(appSettings.isJwtBlacklistEnabled()).thenReturn(false); + + String jwt = "test-jwt"; + + DataStore store = mock(MapDBStore.class); + Token token = new Token(); + token.setTokenId(jwt); + token.setType("publish"); + + BroadcastRestService restServiceSpy = Mockito.spy(restServiceReal); + restServiceSpy.setAppCtx(appContext); + store.addTokenToBlacklist(token); + restServiceSpy.setDataStore(store); + + when(restServiceSpy.getDataStore()).thenReturn(store); + ArrayList tokenList = new ArrayList<>(); + tokenList.add(jwt); + when(restServiceSpy.getAppSettings()).thenReturn(appSettings); + when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); + when(store.getJwtBlacklist()).thenReturn(tokenList); + + List jwtBlacklist = restServiceSpy.getJwtBlacklist(); + + assertTrue(jwtBlacklist.size() > 0); + + } + + @Test + public void testClearJwtBlacklist() { + AppSettings appSettings = mock(AppSettings.class); + when(appSettings.isJwtBlacklistEnabled()).thenReturn(false); + + BroadcastRestService restServiceSpy = Mockito.spy(restServiceReal); + restServiceSpy.setAppSettings(appSettings); + assertFalse(restServiceSpy.clearJwtBlacklist().isSuccess()); + + String jwt = "test-jwt"; + + DataStore store = mock(MapDBStore.class); + Token token = new Token(); + token.setTokenId(jwt); + token.setType("publish"); + store.addTokenToBlacklist(token); + restServiceSpy.setDataStore(store); + when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); + assertTrue(restServiceSpy.clearJwtBlacklist().isSuccess()); + } + + @Test + public void testDeleteAllExpiredJwtFromBlacklist(){ + AppSettings appSettings = mock(AppSettings.class); + when(appSettings.isJwtBlacklistEnabled()).thenReturn(false); + ApplicationContext appContext = mock(ApplicationContext.class); + + BroadcastRestService restServiceSpy = Mockito.spy(restServiceReal); + restServiceSpy.setAppCtx(appContext); + restServiceSpy.setAppSettings(appSettings); + assertFalse(restServiceSpy.deleteAllExpiredJwtFromBlacklist().isSuccess()); + DataStore store = mock(MapDBStore.class); + Token token = new Token(); + token.setTokenId("token"); + token.setType("publish"); + store.addTokenToBlacklist(token); + restServiceSpy.setDataStore(store); + when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); + ITokenService tokenService = mock(ITokenService.class); + + when(appContext.getBean(ITokenService.BeanName.TOKEN_SERVICE.toString())).thenReturn(tokenService); + when(store.deleteAllExpiredJwtFromBlacklist(tokenService)).thenReturn(new Result(true)); + assertTrue(restServiceSpy.deleteAllExpiredJwtFromBlacklist().isSuccess()); + + } } From 7543ea116051b19b1eaf99f3980c25ec791bb164 Mon Sep 17 00:00:00 2001 From: lastpeony Date: Thu, 2 Mar 2023 14:39:35 +0300 Subject: [PATCH 2/5] issue #4809 jwt blacklist implementation and stop play/ publish using jwt. blacklist using already existed token db with flag --- .../io/antmedia/datastore/db/DataStore.java | 28 +++-- .../datastore/db/InMemoryDataStore.java | 14 +-- .../datastore/db/MapBasedDataStore.java | 84 +++++++------ .../io/antmedia/datastore/db/MapDBStore.java | 5 - .../io/antmedia/datastore/db/MongoStore.java | 117 ++++++++++++++---- .../io/antmedia/datastore/db/RedisStore.java | 4 +- .../io/antmedia/datastore/db/types/Token.java | 19 ++- .../antmedia/rest/BroadcastRestService.java | 64 ++++------ .../integration/RestServiceV2Test.java | 18 +-- .../io/antmedia/test/db/DBStoresUnitTest.java | 50 +++++--- .../rest/BroadcastRestServiceV2UnitTest.java | 42 +++---- 11 files changed, 271 insertions(+), 174 deletions(-) diff --git a/src/main/java/io/antmedia/datastore/db/DataStore.java b/src/main/java/io/antmedia/datastore/db/DataStore.java index 8a1b59cf8..62f0872b2 100644 --- a/src/main/java/io/antmedia/datastore/db/DataStore.java +++ b/src/main/java/io/antmedia/datastore/db/DataStore.java @@ -427,25 +427,27 @@ public List listAllTokens (Map tokenMap, String streamId, public abstract boolean deleteToken (String tokenId); /** - * Delete specific token from blacklist. + * Whitelist specific token. * @param tokenId id of the token */ - public abstract boolean deleteTokenFromBlacklist (String tokenId); + public abstract boolean whiteListToken(String tokenId); /** - * Get all tokens from jwt blacklist. + * Get all blacklisted tokens. */ - public abstract List getJwtBlacklist(); + public abstract List getBlackListedTokens(); /** - * Delete all expired tokens from jwt blacklist. + * Delete all blacklisted expired tokens. */ - public abstract Result deleteAllExpiredJwtFromBlacklist(ITokenService tokenService); + public abstract Result deleteAllBlacklistedExpiredTokens(ITokenService tokenService); /** - * Delete all tokens from jwt blacklist. + * Whitelist all blacklisted tokens. + * + * @return */ - public abstract void clearJwtBlacklist(); + public abstract boolean whiteListAllTokens(); /** * retrieve specific token @@ -1377,16 +1379,16 @@ public List getWebRTCViewerList(Map webRTCView public abstract boolean updateStreamMetaData(String streamId, String metaData); /** - * Add jwt token to black list. - * @param token which will be added to black list. + * Blacklist token. + * @param token which will be blacklisted. */ - public abstract boolean addTokenToBlacklist(Token token); + public abstract boolean blackListToken(Token token); /** - * Get token from black list. + * Get token from blacklist. * @param tokenId id of the token. */ - public abstract Token getTokenFromBlacklist(String tokenId); + public abstract Token getBlackListedToken(String tokenId); //************************************** //ATTENTION: Write function descriptions while adding new functions diff --git a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java index 5ea91a003..d65c56c1b 100644 --- a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java +++ b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java @@ -898,23 +898,23 @@ public boolean deleteToken(String tokenId) { } @Override - public boolean deleteTokenFromBlacklist(String tokenId) { + public boolean whiteListToken(String tokenId) { return false; } @Override - public List getJwtBlacklist() { + public List getBlackListedTokens() { return Collections.emptyList(); } @Override - public Result deleteAllExpiredJwtFromBlacklist(ITokenService tokenService) { + public Result deleteAllBlacklistedExpiredTokens(ITokenService tokenService) { return null; } @Override - public void clearJwtBlacklist() { - throw new UnsupportedOperationException("JWT blacklist must be stored as map based db on disk, not in memory."); + public boolean whiteListAllTokens() { + throw new UnsupportedOperationException(""); } @Override @@ -1041,12 +1041,12 @@ public boolean updateStreamMetaData(String streamId, String metaData) { } @Override - public boolean addTokenToBlacklist(Token token) { + public boolean blackListToken(Token token) { return false; } @Override - public Token getTokenFromBlacklist(String tokenId) { + public Token getBlackListedToken(String tokenId) { return null; } } \ No newline at end of file diff --git a/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java b/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java index 1d25414df..fa25222e3 100644 --- a/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java +++ b/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java @@ -44,8 +44,6 @@ public abstract class MapBasedDataStore extends DataStore { protected Map vodMap; protected Map detectionMap; protected Map tokenMap; - protected Map tokenBlacklistMap; - protected Map subscriberMap; protected Map conferenceRoomMap; protected Map webRTCViewerMap; @@ -949,36 +947,43 @@ public boolean deleteToken(String tokenId) { } @Override - public boolean deleteTokenFromBlacklist(String tokenId) { - boolean result; - - synchronized (this) { - result = tokenBlacklistMap.remove(tokenId) != null; + public boolean whiteListToken(String tokenId) { + synchronized (this){ + Token token = getToken(tokenId); + if(token != null && token.isBlackListed()){ + token.setBlackListed(false); + return saveToken(token); + } } - return result; + + return false; } @Override - public List getJwtBlacklist(){ - + public List getBlackListedTokens(){ + ArrayList tokenBlacklist = new ArrayList<>(); synchronized (this){ - return new ArrayList<>(tokenBlacklistMap.keySet()); - + tokenMap.forEach((tokenId, tokenAsJson) -> { + Token token = gson.fromJson(tokenAsJson,Token.class); + if(token.isBlackListed()){ + tokenBlacklist.add(tokenId); + } + }); + return tokenBlacklist; } - } @Override - public Result deleteAllExpiredJwtFromBlacklist(ITokenService tokenService){ - logger.info("Deleting all expired JWTs from black list."); + public Result deleteAllBlacklistedExpiredTokens(ITokenService tokenService){ + logger.info("Deleting all expired JWTs from token storage."); AtomicInteger deletedTokenCount = new AtomicInteger(); - synchronized (this){ - tokenBlacklistMap.forEach((key, value) -> { - Token token = gson.fromJson(value,Token.class); - String tokenId = token.getTokenId(); - if(!tokenService.verifyJwt(tokenId,token.getStreamId(),token.getType())){ - if(deleteTokenFromBlacklist(tokenId)){ + synchronized (this) { + + tokenMap.forEach((tokenId, tokenAsJson) -> { + Token token = gson.fromJson(tokenAsJson,Token.class); + if(token.isBlackListed() && !tokenService.verifyJwt(tokenId,token.getStreamId(),token.getType())){ + if(deleteToken(tokenId)){ deletedTokenCount.getAndIncrement(); }else{ logger.warn("Couldn't delete JWT:{}", tokenId); @@ -988,23 +993,30 @@ public Result deleteAllExpiredJwtFromBlacklist(ITokenService tokenService){ } if(deletedTokenCount.get() > 0){ - final String successMsg = deletedTokenCount+" JWT deleted successfully from black list."; + final String successMsg = deletedTokenCount+" JWT deleted successfully from storage."; logger.info(successMsg); return new Result(true, successMsg); }else{ - final String failMsg = "No JWT deleted from black list."; + final String failMsg = "No JWT deleted from storage."; logger.warn(failMsg); return new Result(false, failMsg); - } } @Override - public void clearJwtBlacklist(){ + public boolean whiteListAllTokens(){ + synchronized (this) { - tokenBlacklistMap.clear(); + tokenMap.forEach((tokenId, tokenAsJson) -> { + Token token = gson.fromJson(tokenAsJson,Token.class); + if(token.isBlackListed()){ + whiteListToken(tokenId); + } + }); } + return true; + } @Override @@ -1120,19 +1132,14 @@ public Broadcast getBroadcastFromMap(String streamId) } @Override - public boolean addTokenToBlacklist(Token token) { + public boolean blackListToken(Token token) { boolean result = false; synchronized (this) { if (token.getStreamId() != null && token.getTokenId() != null) { - - try { - tokenBlacklistMap.put(token.getTokenId(), gson.toJson(token)); - result = true; - } catch (Exception e) { - logger.error(ExceptionUtils.getStackTrace(e)); - } + token.setBlackListed(true); + return saveToken(token); } } return result; @@ -1140,9 +1147,12 @@ public boolean addTokenToBlacklist(Token token) { } @Override - public Token getTokenFromBlacklist(String tokenId) { - return super.getToken(tokenBlacklistMap, tokenId, gson); - + public Token getBlackListedToken(String tokenId) { + Token token = getToken(tokenId); + if(token != null && token.isBlackListed()){ + return token; + } + return null; } } diff --git a/src/main/java/io/antmedia/datastore/db/MapDBStore.java b/src/main/java/io/antmedia/datastore/db/MapDBStore.java index 87eced277..291e0d246 100644 --- a/src/main/java/io/antmedia/datastore/db/MapDBStore.java +++ b/src/main/java/io/antmedia/datastore/db/MapDBStore.java @@ -28,8 +28,6 @@ public class MapDBStore extends MapBasedDataStore { private static final String VOD_MAP_NAME = "VOD"; private static final String DETECTION_MAP_NAME = "DETECTION"; private static final String TOKEN = "TOKEN"; - private static final String TOKEN_BLACKLIST = "TOKEN_BLACKLIST"; - private static final String SUBSCRIBER = "SUBSCRIBER"; private static final String CONFERENCE_ROOM_MAP_NAME = "CONFERENCE_ROOM"; private static final String WEBRTC_VIEWER = "WEBRTC_VIEWER"; @@ -70,9 +68,6 @@ public MapDBStore(String dbName, Vertx vertx) { webRTCViewerMap = db.treeMap(WEBRTC_VIEWER).keySerializer(Serializer.STRING).valueSerializer(Serializer.STRING) .counterEnable().createOrOpen(); - tokenBlacklistMap = db.treeMap(TOKEN_BLACKLIST).keySerializer(Serializer.STRING).valueSerializer(Serializer.STRING) - .counterEnable().createOrOpen(); - timerId = vertx.setPeriodic(5000, id -> diff --git a/src/main/java/io/antmedia/datastore/db/MongoStore.java b/src/main/java/io/antmedia/datastore/db/MongoStore.java index f555b8e85..917000ee1 100644 --- a/src/main/java/io/antmedia/datastore/db/MongoStore.java +++ b/src/main/java/io/antmedia/datastore/db/MongoStore.java @@ -9,8 +9,8 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import io.antmedia.rest.model.Result; @@ -59,6 +59,7 @@ public class MongoStore extends DataStore { public static final String VOD_ID = "vodId"; private static final String VIEWER_ID = "viewerId"; private static final String TOKEN_ID = "tokenId"; + private static final String BLACKLISTED = "blackListed"; public static final String STREAM_ID = "streamId"; private Datastore datastore; private Datastore vodDatastore; @@ -1198,26 +1199,6 @@ public boolean deleteToken(String tokenId) { return result; } - @Override - public boolean deleteTokenFromBlacklist(String tokenId) { - return false; - } - - @Override - public List getJwtBlacklist() { - return Collections.emptyList(); - } - - @Override - public Result deleteAllExpiredJwtFromBlacklist(ITokenService tokenService) { - return null; - } - - @Override - public void clearJwtBlacklist() { - throw new UnsupportedOperationException("JWT blacklist must be stored as map based db on disk, not in mongodb."); - } - @Override public Token getToken(String tokenId) { Token token = null; @@ -1453,13 +1434,101 @@ public boolean updateStreamMetaData(String streamId, String metaData) { } @Override - public boolean addTokenToBlacklist(Token token) { + public boolean blackListToken(Token token) { + boolean result = false; + //update if exists, else insert + synchronized (this) { + if (token.getStreamId() != null && token.getTokenId() != null) { + Query query = tokenDatastore.find(Token.class).filter(Filters.eq(TOKEN_ID, token.getTokenId())); + + if(query.first() != null){ + final UpdateResult results = query.update(new UpdateOptions().multi(false), set(BLACKLISTED, true)); + if(results.getModifiedCount() == 1){ + result = true; + } + }else{ + token.setBlackListed(true); + result = saveToken(token); + } + } + } + + return result; - return false; } @Override - public Token getTokenFromBlacklist(String tokenId) { + public Token getBlackListedToken(String tokenId) { + synchronized (this){ + Query query = tokenDatastore.find(Token.class).filter(Filters.eq(TOKEN_ID, tokenId)); + Token fetchedToken = query.first(); + if(fetchedToken != null && fetchedToken.isBlackListed()){ + return fetchedToken; + } + } return null; } + + @Override + public boolean whiteListToken(String tokenId) { + synchronized (this){ + Query query = tokenDatastore.find(Token.class).filter(Filters.eq(TOKEN_ID, tokenId)); + final UpdateResult results = query.update(new UpdateOptions().multi(false), set(BLACKLISTED, false)); + return results.wasAcknowledged(); + } + } + + @Override + public List getBlackListedTokens() { + List tokenBlacklist = new ArrayList<>(); + synchronized (this){ + Query query = tokenDatastore.find(Token.class).filter(Filters.eq(BLACKLISTED, true)); + for (Token token : query) { + tokenBlacklist.add(token.getTokenId()); + } + return tokenBlacklist; + } + } + + @Override + public Result deleteAllBlacklistedExpiredTokens(ITokenService tokenService) { + AtomicInteger deletedTokenCount = new AtomicInteger(); + + synchronized (this){ + List tokenBlacklist = getBlackListedTokens(); + tokenBlacklist.forEach(tokenId ->{ + + Token token = getToken(tokenId); + if(!tokenService.verifyJwt(tokenId,token.getStreamId(),token.getType())){ + if(whiteListToken(tokenId)){ + deletedTokenCount.getAndIncrement(); + }else{ + logger.warn("Couldn't delete JWT:{}", tokenId); + } + } + + }); + + } + if(deletedTokenCount.get() > 0){ + final String successMsg = deletedTokenCount+" JWT deleted successfully from blacklist."; + logger.info(successMsg); + return new Result(true, successMsg); + }else{ + final String failMsg = "No JWT deleted from blacklist."; + logger.warn(failMsg); + return new Result(false, failMsg); + } + + } + + @Override + public boolean whiteListAllTokens() { + synchronized (this) { + Query query = tokenDatastore.find(Token.class).filter(Filters.eq(BLACKLISTED, true)); + final UpdateResult results = query.update(new UpdateOptions().multi(true), set(BLACKLISTED, false)); + return results.wasAcknowledged(); + } + } + } diff --git a/src/main/java/io/antmedia/datastore/db/RedisStore.java b/src/main/java/io/antmedia/datastore/db/RedisStore.java index 0213d06f4..dd8e5fd4f 100644 --- a/src/main/java/io/antmedia/datastore/db/RedisStore.java +++ b/src/main/java/io/antmedia/datastore/db/RedisStore.java @@ -121,13 +121,13 @@ public int resetBroadcasts(String hostAddress) { } @Override - public boolean addTokenToBlacklist(Token token) { + public boolean blackListToken(Token token) { return false; } @Override - public Token getTokenFromBlacklist(String tokenId) { + public Token getBlackListedToken(String tokenId) { return null; } diff --git a/src/main/java/io/antmedia/datastore/db/types/Token.java b/src/main/java/io/antmedia/datastore/db/types/Token.java index a4cf8bdef..d38b79b48 100644 --- a/src/main/java/io/antmedia/datastore/db/types/Token.java +++ b/src/main/java/io/antmedia/datastore/db/types/Token.java @@ -51,6 +51,16 @@ public class Token { */ @ApiModelProperty(value = "the type of the token") private String type; + + public ObjectId getDbId() { + return dbId; + } + + public void setDbId(ObjectId dbId) { + this.dbId = dbId; + } + + private boolean blackListed; /** * the id of the conference room which requested streams belongs to. @@ -98,6 +108,13 @@ public long getExpireDate() { public void setExpireDate(long expireDate) { this.expireDate = expireDate; } - + + public boolean isBlackListed() { + return blackListed; + } + + public void setBlackListed(boolean blackListed) { + this.blackListed = blackListed; + } } diff --git a/src/main/java/io/antmedia/rest/BroadcastRestService.java b/src/main/java/io/antmedia/rest/BroadcastRestService.java index 74645cb0b..397e5fd21 100644 --- a/src/main/java/io/antmedia/rest/BroadcastRestService.java +++ b/src/main/java/io/antmedia/rest/BroadcastRestService.java @@ -87,7 +87,7 @@ public class BroadcastRestService extends RestServiceBase{ private static final String ABSOLUTE_MOVE = "absolute"; private static final String CONTINUOUS_MOVE = "continuous"; - private final String blacklistNotEnabledMsg = "JWT black list is not enabled for this application."; + private final String blacklistNotEnabledMsg = "JWT blacklist is not enabled for this application."; @ApiModel(value="SimpleStat", description="Simple generic statistics class to return single values") @@ -606,15 +606,27 @@ public Result validateTokenV2(@ApiParam(value = "Token to be validated", require return new Result(result); } - @ApiOperation(value = "Add jwt token to black list. If added, success field is true, " + @ApiOperation(value = "Add jwt token to blacklist. If added, success field is true, " + "if not added success field false", response = Result.class) @POST @Consumes(MediaType.APPLICATION_JSON) @Path("/jwt-black-list") @Produces(MediaType.APPLICATION_JSON) - public Result addJwtToBlacklist(@ApiParam(value = "jwt to be added to black list.", required = true) @QueryParam("jwt") String jwt) + public Result blackListOrWhitelistJwt(@ApiParam(value = "jwt to be added to blacklist.", required = true) @QueryParam("jwt") String jwt, + @ApiParam(value = "if true whitelist jwt (unblacklist)", required = false) @QueryParam("whiteList") boolean whiteList) { if(getAppSettings().isJwtBlacklistEnabled()){ + if(whiteList){ + if(getDataStore().getBlackListedToken(jwt) == null){ + return new Result(false, "JWT does not exist in blacklist."); + + }else if(getDataStore().whiteListToken(jwt)){ + return new Result(true, "JWT successfully whitelisted."); + + }else{ + return new Result(false, "JWT cannot be whitelisted."); + } + } try{ final DecodedJWT decodedJWT = JWT.decode(jwt); final String payload = new String(Base64.getDecoder().decode(decodedJWT.getPayload()), Charset.defaultCharset()); @@ -633,9 +645,9 @@ public Result addJwtToBlacklist(@ApiParam(value = "jwt to be added to black list if(!super.verifyJwt(jwt, streamId, tokenType)){ return new Result(false,"JWT is not valid."); - }else if(getDataStore().getTokenFromBlacklist(token.getTokenId()) != null){ + }else if(getDataStore().getBlackListedToken(jwt) != null){ return new Result(false, "JWT is already in blacklist."); - }else if(getDataStore().addTokenToBlacklist(token)){ + }else if(getDataStore().blackListToken(token)){ return new Result(true, "JWT successfully added to blacklist."); } @@ -654,70 +666,46 @@ public Result addJwtToBlacklist(@ApiParam(value = "jwt to be added to black list return new Result(false); } - @ApiOperation(value = "Delete jwt from black list. If deleted, success field is true, " - + "if not deleted success field false.", response = Result.class) - @DELETE - @Consumes(MediaType.APPLICATION_JSON) - @Path("/jwt-black-list") - @Produces(MediaType.APPLICATION_JSON) - public Result deleteJwtFromBlacklist(@ApiParam(value = "jwt to be deleted from black list.", required = true) @QueryParam("jwt") String jwt) - { - if(getAppSettings().isJwtBlacklistEnabled()){ - if(getDataStore().getTokenFromBlacklist(jwt) == null){ - return new Result(false, "JWT does not exist in black list."); - - }else if(getDataStore().deleteTokenFromBlacklist(jwt)){ - return new Result(true, "JWT successfully deleted from black list."); - - }else{ - return new Result(false, "JWT cannot be deleted from black list."); - } - }else{ - logger.warn(blacklistNotEnabledMsg); - return new Result(false, blacklistNotEnabledMsg); - } - } - - @ApiOperation(value = "Get all JWTs from blacklist.", response = Result.class) + @ApiOperation(value = "Get all blacklisted JWTs.", response = Result.class) @GET @Path("/jwt-black-list") @Produces(MediaType.APPLICATION_JSON) public List getJwtBlacklist() { if(getAppSettings().isJwtBlacklistEnabled()) { - return getDataStore().getJwtBlacklist(); + return getDataStore().getBlackListedTokens(); }else{ logger.warn(blacklistNotEnabledMsg); return null; } } - @ApiOperation(value = "Delete all expired JWTs from black list.", response = Result.class) + @ApiOperation(value = "Delete all expired blacklisted JWTs.", response = Result.class) @DELETE @Path("/jwt-black-list-delete-expired") @Produces(MediaType.APPLICATION_JSON) public Result deleteAllExpiredJwtFromBlacklist() { if(getAppSettings().isJwtBlacklistEnabled()) { - return getDataStore().deleteAllExpiredJwtFromBlacklist(getTokenService()); + return getDataStore().deleteAllBlacklistedExpiredTokens(getTokenService()); }else{ logger.warn(blacklistNotEnabledMsg); return new Result(false, blacklistNotEnabledMsg); } } - @ApiOperation(value = "Delete all JWTs from black list.", response = Result.class) + @ApiOperation(value = "White list all blacklisted JWTs.", response = Result.class) @DELETE @Path("/jwt-black-list-clear") @Produces(MediaType.APPLICATION_JSON) public Result clearJwtBlacklist() { if(getAppSettings().isJwtBlacklistEnabled()) { - getDataStore().clearJwtBlacklist(); - if(getDataStore().getJwtBlacklist().isEmpty()){ - return new Result(true, "JWT black list cleared successfully"); + getDataStore().whiteListAllTokens(); + if(getDataStore().getBlackListedTokens().isEmpty()){ + return new Result(true, "All blacklisted tokens are whitelisted successfully."); }else{ - return new Result(false, "JWT black list clear failed."); + return new Result(false, "JWT blacklist clear failed."); } }else{ logger.warn(blacklistNotEnabledMsg); diff --git a/src/test/java/io/antmedia/integration/RestServiceV2Test.java b/src/test/java/io/antmedia/integration/RestServiceV2Test.java index 4d846c57b..fa240a1af 100644 --- a/src/test/java/io/antmedia/integration/RestServiceV2Test.java +++ b/src/test/java/io/antmedia/integration/RestServiceV2Test.java @@ -2047,13 +2047,13 @@ public void testJwtBlacklist(){ final String validJwt2 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicHVibGlzaCIsImV4cCI6OTg4NzUwNzUwMX0.f4YTJUOmO7yuGpD7W4i_fffv2IVi1JB3mZVxNv8LSdI"; final String validJwt3 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicHVibGlzaCIsImV4cCI6OTg4NzUwNzUwMn0.bSbIumeAeM-k5zLndtII49z_458L8Lqg3eVweahvpb4"; - HttpUriRequest addJwtRequest1 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt1).build().toString()) + HttpUriRequest addJwtRequest1 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt1).addParameter("whiteList","false").build().toString()) .setHeader(HttpHeaders.CONTENT_TYPE, "application/json").build(); - HttpUriRequest addJwtRequest2 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt2).build().toString()) + HttpUriRequest addJwtRequest2 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt2).addParameter("whiteList","false").build().toString()) .setHeader(HttpHeaders.CONTENT_TYPE, "application/json").build(); - HttpUriRequest addJwtRequest3 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt3).build().toString()) + HttpUriRequest addJwtRequest3 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt3).addParameter("whiteList","false").build().toString()) .setHeader(HttpHeaders.CONTENT_TYPE, "application/json").build(); HttpResponse addJwtResponse1 = client.execute(addJwtRequest1); @@ -2103,14 +2103,14 @@ public void testJwtBlacklist(){ assertEquals(expectedJwtCount, jwtBlacklist.size()); - HttpUriRequest deleteJwtFromBlacklistRequest = RequestBuilder.delete().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt1).build()).build(); - HttpResponse deleteJwtFromBlacklistResponse = client.execute(deleteJwtFromBlacklistRequest); - StringBuffer deleteJwtFromBlacklistResult = readResponse(deleteJwtFromBlacklistResponse); + HttpUriRequest whiteListJwtRequest = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt1).addParameter("whiteList", "true").build()).build(); + HttpResponse whiteListJwtResponse = client.execute(whiteListJwtRequest); + StringBuffer whiteListJwtResult = readResponse(whiteListJwtResponse); - if (deleteJwtFromBlacklistResponse.getStatusLine().getStatusCode() != 200) { - throw new Exception(deleteJwtFromBlacklistResult.toString()); + if (whiteListJwtResponse.getStatusLine().getStatusCode() != 200) { + throw new Exception(whiteListJwtResult.toString()); } - result = gson.fromJson(deleteJwtFromBlacklistResult.toString(), Result.class); + result = gson.fromJson(whiteListJwtResult.toString(), Result.class); assertTrue(result.isSuccess()); diff --git a/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java b/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java index b506358d3..0b3e68ea0 100644 --- a/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java +++ b/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java @@ -29,7 +29,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; @@ -138,8 +137,8 @@ public void testMapDBStore() { testWebRTCViewerOperations(dataStore); testUpdateMetaData(dataStore); testStreamSourceList(dataStore); - testDeleteAllExpiredJwtFromBlacklist(dataStore); - testClearJwtBlacklist(dataStore); + testWhitelistAllExpiredTokens(dataStore); + testWhitelistAllTokens(dataStore); } @@ -271,6 +270,8 @@ public void testMongoStore() { testUpdateEndpointStatus(dataStore); testWebRTCViewerOperations(dataStore); testUpdateMetaData(dataStore); + testWhitelistAllExpiredTokens(dataStore); + testWhitelistAllTokens(dataStore); } @Test @@ -2254,6 +2255,18 @@ public void testMongoDBSaveStreamInfo() { deleteStreamInfos(dataStore); } + @Test + public void testMongoDBJwtBlacklist(){ + MongoStore dataStore = new MongoStore("localhost", "", "", "testdb"); + dataStore.whiteListToken(""); + dataStore.getBlackListedTokens(); + dataStore.deleteAllBlacklistedExpiredTokens(null); + dataStore.whiteListAllTokens(); + dataStore.blackListToken(new Token()); + dataStore.getBlackListedToken(""); + + } + public void deleteStreamInfos(MongoStore datastore) { datastore.getDataStore().find(StreamInfo.class).delete(new DeleteOptions() .multi(true)); @@ -2841,7 +2854,7 @@ public void testUpdateMetaData(DataStore dataStore) { } - public void testDeleteAllExpiredJwtFromBlacklist(DataStore dataStore){ + public void testWhitelistAllExpiredTokens(DataStore dataStore){ Token token1 = new Token(); Token token2 = new Token(); @@ -2867,14 +2880,14 @@ public void testDeleteAllExpiredJwtFromBlacklist(DataStore dataStore){ token3.setStreamId(streamId); ITokenService tokenService = mock(ITokenService.class); - Result res = dataStore.deleteAllExpiredJwtFromBlacklist(tokenService); + Result res = dataStore.deleteAllBlacklistedExpiredTokens(tokenService); assertFalse(res.isSuccess()); - dataStore.addTokenToBlacklist(token1); - dataStore.addTokenToBlacklist(token2); - dataStore.addTokenToBlacklist(token3); + dataStore.blackListToken(token1); + dataStore.blackListToken(token2); + dataStore.blackListToken(token3); - Token jwt = dataStore.getTokenFromBlacklist(token1.getTokenId()); + Token jwt = dataStore.getBlackListedToken(token1.getTokenId()); assertNotNull(jwt); when(tokenService.verifyJwt(jwt1,streamId,tokenType)).thenReturn(false); @@ -2882,20 +2895,22 @@ public void testDeleteAllExpiredJwtFromBlacklist(DataStore dataStore){ when(tokenService.verifyJwt(jwt3,streamId,tokenType)).thenReturn(false); - res = dataStore.deleteAllExpiredJwtFromBlacklist(tokenService); + res = dataStore.deleteAllBlacklistedExpiredTokens(tokenService); assertTrue(res.isSuccess()); } - public void testClearJwtBlacklist(DataStore dataStore){ + public void testWhitelistAllTokens(DataStore dataStore){ + addJwtsToBlacklist(dataStore); - List jwtBlacklist = dataStore.getJwtBlacklist(); + List jwtBlacklist = dataStore.getBlackListedTokens(); assertEquals(3, jwtBlacklist.size()); - dataStore.clearJwtBlacklist(); - jwtBlacklist = dataStore.getJwtBlacklist(); + Token token = dataStore.getBlackListedToken("jwt1"); + dataStore.whiteListAllTokens(); + jwtBlacklist = dataStore.getBlackListedTokens(); assertEquals(0, jwtBlacklist.size()); @@ -2918,6 +2933,7 @@ private void addJwtsToBlacklist(DataStore dataStore){ token2.setTokenId(jwt2); token3.setTokenId(jwt3); + token1.setType(tokenType); token2.setType(tokenType); token3.setType(tokenType); @@ -2926,9 +2942,9 @@ private void addJwtsToBlacklist(DataStore dataStore){ token2.setStreamId(streamId); token3.setStreamId(streamId); - dataStore.addTokenToBlacklist(token1); - dataStore.addTokenToBlacklist(token2); - dataStore.addTokenToBlacklist(token3); + dataStore.blackListToken(token1); + dataStore.blackListToken(token2); + dataStore.blackListToken(token3); } } diff --git a/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java b/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java index db8dec72e..1084977c0 100644 --- a/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java +++ b/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java @@ -3065,7 +3065,7 @@ public void testAddJwtToBlacklist() { restServiceSpy.setDataStore(store); - Result result = restServiceSpy.addJwtToBlacklist(jwt); + Result result = restServiceSpy.blackListOrWhitelistJwt(jwt, false); assertFalse(result.isSuccess()); when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); Token token = mock(Token.class); @@ -3074,32 +3074,32 @@ public void testAddJwtToBlacklist() { when(token.getStreamId()).thenReturn("test-stream"); - when(store.addTokenToBlacklist(token)).thenReturn(true); - result = restServiceSpy.addJwtToBlacklist(jwt); + when(store.blackListToken(token)).thenReturn(true); + result = restServiceSpy.blackListOrWhitelistJwt(jwt,false); assertFalse(result.isSuccess()); String invalidJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdHJlYW1JZCI6InRlc3Qtc3RyZWFtIiwidHlwZSI6InF3ZSIsImV4cCI6OTk5OTk5OTk5OTk5OX0.DqfFkRJgKPVXgAkIzucuQtfwP2Oj-Qf9dhUuO_-04bU"; - result = restServiceSpy.addJwtToBlacklist(invalidJwt); + result = restServiceSpy.blackListOrWhitelistJwt(invalidJwt, false); assertFalse(result.isSuccess()); String validJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdHJlYW1JZCI6InRlc3Qtc3RyZWFtIiwidHlwZSI6InB1Ymxpc2giLCJleHAiOjk5OTk5OTk5OTk5OTl9.ichno9utOYwVv1qoQWtUpDap7PGYze-zfXRZU31CMnQ"; when(tokenService.verifyJwt(validJwt,"test-stream","publish")).thenReturn(true); - when(store.addTokenToBlacklist(any())).thenReturn(true); + when(store.blackListToken(any())).thenReturn(true); - result = restServiceSpy.addJwtToBlacklist(validJwt); + result = restServiceSpy.blackListOrWhitelistJwt(validJwt, false); assertTrue(result.isSuccess()); - when(store.getTokenFromBlacklist(validJwt)).thenReturn(token); + when(store.getBlackListedToken(validJwt)).thenReturn(token); - result = restServiceSpy.addJwtToBlacklist(validJwt); + result = restServiceSpy.blackListOrWhitelistJwt(validJwt, false); assertFalse(result.isSuccess()); } @Test - public void testDeleteJwtFromBlacklist() { + public void testWhitelistJwt() { AppSettings appSettings = mock(AppSettings.class); ApplicationContext appContext = mock(ApplicationContext.class); @@ -3120,22 +3120,22 @@ public void testDeleteJwtFromBlacklist() { restServiceSpy.setDataStore(store); - Result result1 = restServiceSpy.deleteJwtFromBlacklist(jwt); + Result result1 = restServiceSpy.blackListOrWhitelistJwt(jwt, true); assertFalse(result1.isSuccess()); when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); - when(store.getTokenFromBlacklist(jwt)).thenReturn(null); - Result result2 = restServiceSpy.deleteJwtFromBlacklist(jwt); + when(store.getBlackListedToken(jwt)).thenReturn(null); + Result result2 = restServiceSpy.blackListOrWhitelistJwt(jwt, true); assertFalse(result2.isSuccess()); Token token = mock(Token.class); - when(store.getTokenFromBlacklist(jwt)).thenReturn(token); - Result result3 = restServiceSpy.deleteJwtFromBlacklist(jwt); + when(store.getBlackListedToken(jwt)).thenReturn(token); + Result result3 = restServiceSpy.blackListOrWhitelistJwt(jwt, true); assertFalse(result3.isSuccess()); - when(store.deleteTokenFromBlacklist(jwt)).thenReturn(true); - Result result4 = restServiceSpy.deleteJwtFromBlacklist(jwt); + when(store.whiteListToken(jwt)).thenReturn(true); + Result result4 = restServiceSpy.blackListOrWhitelistJwt(jwt, true); assertTrue(result4.isSuccess()); } @@ -3155,7 +3155,7 @@ public void testGetJwtBlacklist(){ BroadcastRestService restServiceSpy = Mockito.spy(restServiceReal); restServiceSpy.setAppCtx(appContext); - store.addTokenToBlacklist(token); + store.blackListToken(token); restServiceSpy.setDataStore(store); when(restServiceSpy.getDataStore()).thenReturn(store); @@ -3163,7 +3163,7 @@ public void testGetJwtBlacklist(){ tokenList.add(jwt); when(restServiceSpy.getAppSettings()).thenReturn(appSettings); when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); - when(store.getJwtBlacklist()).thenReturn(tokenList); + when(store.getBlackListedTokens()).thenReturn(tokenList); List jwtBlacklist = restServiceSpy.getJwtBlacklist(); @@ -3186,7 +3186,7 @@ public void testClearJwtBlacklist() { Token token = new Token(); token.setTokenId(jwt); token.setType("publish"); - store.addTokenToBlacklist(token); + store.blackListToken(token); restServiceSpy.setDataStore(store); when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); assertTrue(restServiceSpy.clearJwtBlacklist().isSuccess()); @@ -3206,13 +3206,13 @@ public void testDeleteAllExpiredJwtFromBlacklist(){ Token token = new Token(); token.setTokenId("token"); token.setType("publish"); - store.addTokenToBlacklist(token); + store.blackListToken(token); restServiceSpy.setDataStore(store); when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); ITokenService tokenService = mock(ITokenService.class); when(appContext.getBean(ITokenService.BeanName.TOKEN_SERVICE.toString())).thenReturn(tokenService); - when(store.deleteAllExpiredJwtFromBlacklist(tokenService)).thenReturn(new Result(true)); + when(store.deleteAllBlacklistedExpiredTokens(tokenService)).thenReturn(new Result(true)); assertTrue(restServiceSpy.deleteAllExpiredJwtFromBlacklist().isSuccess()); } From 2f5971d8b09cdf9d31e9b4a3d75b6ebb39c8106a Mon Sep 17 00:00:00 2001 From: lastpeony Date: Tue, 13 Jun 2023 19:53:34 +0300 Subject: [PATCH 3/5] refactor jwt blacklist rest --- .../antmedia/rest/BroadcastRestService.java | 49 +++++++++++++------ src/main/java/io/antmedia/rest/model/Jwt.java | 23 +++++++++ .../antmedia/security/MockTokenService.java | 2 +- .../integration/RestServiceV2Test.java | 27 +++++----- .../rest/BroadcastRestServiceV2UnitTest.java | 47 ++++++++++-------- 5 files changed, 99 insertions(+), 49 deletions(-) create mode 100644 src/main/java/io/antmedia/rest/model/Jwt.java diff --git a/src/main/java/io/antmedia/rest/BroadcastRestService.java b/src/main/java/io/antmedia/rest/BroadcastRestService.java index 59f706eae..0b44e5744 100644 --- a/src/main/java/io/antmedia/rest/BroadcastRestService.java +++ b/src/main/java/io/antmedia/rest/BroadcastRestService.java @@ -7,6 +7,7 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.DecodedJWT; +import io.antmedia.rest.model.Jwt; import org.apache.commons.lang3.exception.ExceptionUtils; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; @@ -48,6 +49,7 @@ import io.swagger.annotations.Info; import io.swagger.annotations.License; import io.swagger.annotations.SwaggerDefinition; +import org.springframework.web.bind.annotation.RequestBody; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -611,23 +613,12 @@ public Result validateTokenV2(@ApiParam(value = "Token to be validated", require @Consumes(MediaType.APPLICATION_JSON) @Path("/jwt-black-list") @Produces(MediaType.APPLICATION_JSON) - public Result blackListOrWhitelistJwt(@ApiParam(value = "jwt to be added to blacklist.", required = true) @QueryParam("jwt") String jwt, - @ApiParam(value = "if true whitelist jwt (unblacklist)", required = false) @QueryParam("whiteList") boolean whiteList) + public Result blackListJwt(@ApiParam(value = "jwt to be added to blacklist.", required = true) @RequestBody Jwt jwt) { if(getAppSettings().isJwtBlacklistEnabled()){ - if(whiteList){ - if(getDataStore().getBlackListedToken(jwt) == null){ - return new Result(false, "JWT does not exist in blacklist."); - }else if(getDataStore().whiteListToken(jwt)){ - return new Result(true, "JWT successfully whitelisted."); - - }else{ - return new Result(false, "JWT cannot be whitelisted."); - } - } try{ - final DecodedJWT decodedJWT = JWT.decode(jwt); + final DecodedJWT decodedJWT = JWT.decode(jwt.getJwt()); final String payload = new String(Base64.getDecoder().decode(decodedJWT.getPayload()), Charset.defaultCharset()); final JSONParser parser = new JSONParser(); try{ @@ -637,14 +628,14 @@ public Result blackListOrWhitelistJwt(@ApiParam(value = "jwt to be added to blac final long exp = (Long) jwtPayload.get("exp"); final Token token = new Token(); - token.setTokenId(jwt); + token.setTokenId(jwt.getJwt()); token.setType(tokenType); token.setStreamId(streamId); token.setExpireDate(exp); - if(!super.verifyJwt(jwt, streamId, tokenType)){ + if(!super.verifyJwt(jwt.getJwt(), streamId, tokenType)){ return new Result(false,"JWT is not valid."); - }else if(getDataStore().getBlackListedToken(jwt) != null){ + }else if(getDataStore().getBlackListedToken(jwt.getJwt()) != null){ return new Result(false, "JWT is already in blacklist."); }else if(getDataStore().blackListToken(token)){ return new Result(true, "JWT successfully added to blacklist."); @@ -665,6 +656,32 @@ public Result blackListOrWhitelistJwt(@ApiParam(value = "jwt to be added to blac return new Result(false); } + @ApiOperation(value = "Remove jwt from blacklist. If removed, success field is true, " + + "if not removed success field false", response = Result.class) + @DELETE + @Consumes(MediaType.APPLICATION_JSON) + @Path("/jwt-black-list") + @Produces(MediaType.APPLICATION_JSON) + public Result whiteListJwt(@ApiParam(value = "Jwt to be removed from blacklist.", required = true) @QueryParam("jwt") String jwt) + { + if(getAppSettings().isJwtBlacklistEnabled()){ + + if(getDataStore().getBlackListedToken(jwt) == null){ + return new Result(false, "JWT does not exist in blacklist."); + + }else if(getDataStore().whiteListToken(jwt)){ + return new Result(true, "JWT successfully removed from blacklist."); + + }else{ + return new Result(false, "JWT cannot be removed from blacklist."); + } + }else{ + logger.warn(blacklistNotEnabledMsg); + return new Result(false, blacklistNotEnabledMsg); + } + + } + @ApiOperation(value = "Get all blacklisted JWTs.", response = Result.class) @GET @Path("/jwt-black-list") diff --git a/src/main/java/io/antmedia/rest/model/Jwt.java b/src/main/java/io/antmedia/rest/model/Jwt.java new file mode 100644 index 000000000..a30daca63 --- /dev/null +++ b/src/main/java/io/antmedia/rest/model/Jwt.java @@ -0,0 +1,23 @@ +package io.antmedia.rest.model; + +public class Jwt { + private String jwt; + + // Default constructor + public Jwt() { + } + + // Constructor with jwt parameter + public Jwt(String jwt) { + this.jwt = jwt; + } + + // Getter and setter for jwt + public String getJwt() { + return jwt; + } + + public void setJwt(String jwt) { + this.jwt = jwt; + } +} diff --git a/src/main/java/io/antmedia/security/MockTokenService.java b/src/main/java/io/antmedia/security/MockTokenService.java index 309c12df7..ce5a4d5ec 100644 --- a/src/main/java/io/antmedia/security/MockTokenService.java +++ b/src/main/java/io/antmedia/security/MockTokenService.java @@ -47,7 +47,7 @@ public Map getSubscriberAuthenticatedMap() { public boolean verifyJwt(String jwtTokenId, String streamId, String type) { return false; } - + @Override public boolean checkHash(String hash, String streamId, String sessionId, String type) { return true; diff --git a/src/test/java/io/antmedia/integration/RestServiceV2Test.java b/src/test/java/io/antmedia/integration/RestServiceV2Test.java index fa240a1af..0f081d530 100644 --- a/src/test/java/io/antmedia/integration/RestServiceV2Test.java +++ b/src/test/java/io/antmedia/integration/RestServiceV2Test.java @@ -38,10 +38,7 @@ import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.methods.*; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.HttpMultipartMode; @@ -56,6 +53,7 @@ import org.awaitility.Awaitility; import org.bytedeco.ffmpeg.global.avformat; import org.bytedeco.ffmpeg.global.avutil; +import org.json.simple.JSONObject; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -2047,14 +2045,20 @@ public void testJwtBlacklist(){ final String validJwt2 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicHVibGlzaCIsImV4cCI6OTg4NzUwNzUwMX0.f4YTJUOmO7yuGpD7W4i_fffv2IVi1JB3mZVxNv8LSdI"; final String validJwt3 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicHVibGlzaCIsImV4cCI6OTg4NzUwNzUwMn0.bSbIumeAeM-k5zLndtII49z_458L8Lqg3eVweahvpb4"; - HttpUriRequest addJwtRequest1 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt1).addParameter("whiteList","false").build().toString()) - .setHeader(HttpHeaders.CONTENT_TYPE, "application/json").build(); + String jwtRequestBody1 = new JSONObject().put("jwt", validJwt1).toString(); + HttpPost addJwtRequest1 = new HttpPost(jwtBlacklistUrl); + addJwtRequest1.setHeader("Content-Type", "application/json"); + addJwtRequest1.setEntity(new StringEntity(jwtRequestBody1)); - HttpUriRequest addJwtRequest2 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt2).addParameter("whiteList","false").build().toString()) - .setHeader(HttpHeaders.CONTENT_TYPE, "application/json").build(); + String jwtRequestBody2 = new JSONObject().put("jwt", validJwt2).toString(); + HttpPost addJwtRequest2 = new HttpPost(jwtBlacklistUrl); + addJwtRequest2.setHeader("Content-Type", "application/json"); + addJwtRequest2.setEntity(new StringEntity(jwtRequestBody2)); - HttpUriRequest addJwtRequest3 = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt3).addParameter("whiteList","false").build().toString()) - .setHeader(HttpHeaders.CONTENT_TYPE, "application/json").build(); + String jwtRequestBody3 = new JSONObject().put("jwt", validJwt3).toString(); + HttpPost addJwtRequest3 = new HttpPost(jwtBlacklistUrl); + addJwtRequest3.setHeader("Content-Type", "application/json"); + addJwtRequest3.setEntity(new StringEntity(jwtRequestBody3)); HttpResponse addJwtResponse1 = client.execute(addJwtRequest1); @@ -2103,10 +2107,11 @@ public void testJwtBlacklist(){ assertEquals(expectedJwtCount, jwtBlacklist.size()); - HttpUriRequest whiteListJwtRequest = RequestBuilder.post().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt1).addParameter("whiteList", "true").build()).build(); + HttpUriRequest whiteListJwtRequest = RequestBuilder.delete().setUri(new URIBuilder(jwtBlacklistUrl).addParameter("jwt",validJwt1).build()).build(); HttpResponse whiteListJwtResponse = client.execute(whiteListJwtRequest); StringBuffer whiteListJwtResult = readResponse(whiteListJwtResponse); + if (whiteListJwtResponse.getStatusLine().getStatusCode() != 200) { throw new Exception(whiteListJwtResult.toString()); } diff --git a/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java b/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java index b6fa6a9a2..2714aa48f 100644 --- a/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java +++ b/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java @@ -34,6 +34,7 @@ import javax.ws.rs.core.Response.Status; import io.antmedia.datastore.db.MapDBStore; +import io.antmedia.rest.model.Jwt; import org.apache.commons.lang3.RandomStringUtils; import org.awaitility.Awaitility; import org.bytedeco.ffmpeg.global.avformat; @@ -3130,42 +3131,46 @@ public void testAddJwtToBlacklist() { restServiceSpy.setAppCtx(appContext); restServiceSpy.setAppSettings(appSettings); - String jwt = "test-jwt"; + String jwtStr = "test-jwt"; + Jwt jwt = new Jwt(); + jwt.setJwt(jwtStr); DataStore store = mock(MapDBStore.class); restServiceSpy.setDataStore(store); - Result result = restServiceSpy.blackListOrWhitelistJwt(jwt, false); + Result result = restServiceSpy.blackListJwt(jwt); assertFalse(result.isSuccess()); when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); Token token = mock(Token.class); - when(token.getTokenId()).thenReturn(jwt); + when(token.getTokenId()).thenReturn(jwt.getJwt()); when(token.getStreamId()).thenReturn("test-stream"); when(store.blackListToken(token)).thenReturn(true); - result = restServiceSpy.blackListOrWhitelistJwt(jwt,false); + result = restServiceSpy.blackListJwt(jwt); assertFalse(result.isSuccess()); - String invalidJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdHJlYW1JZCI6InRlc3Qtc3RyZWFtIiwidHlwZSI6InF3ZSIsImV4cCI6OTk5OTk5OTk5OTk5OX0.DqfFkRJgKPVXgAkIzucuQtfwP2Oj-Qf9dhUuO_-04bU"; - - result = restServiceSpy.blackListOrWhitelistJwt(invalidJwt, false); + String invalidJwtStr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdHJlYW1JZCI6InRlc3Qtc3RyZWFtIiwidHlwZSI6InF3ZSIsImV4cCI6OTk5OTk5OTk5OTk5OX0.DqfFkRJgKPVXgAkIzucuQtfwP2Oj-Qf9dhUuO_-04bU"; + Jwt invalidJwt = new Jwt(); + invalidJwt.setJwt(invalidJwtStr); + result = restServiceSpy.blackListJwt(invalidJwt); assertFalse(result.isSuccess()); - String validJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdHJlYW1JZCI6InRlc3Qtc3RyZWFtIiwidHlwZSI6InB1Ymxpc2giLCJleHAiOjk5OTk5OTk5OTk5OTl9.ichno9utOYwVv1qoQWtUpDap7PGYze-zfXRZU31CMnQ"; - - when(tokenService.verifyJwt(validJwt,"test-stream","publish")).thenReturn(true); + String validJwtStr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdHJlYW1JZCI6InRlc3Qtc3RyZWFtIiwidHlwZSI6InB1Ymxpc2giLCJleHAiOjk5OTk5OTk5OTk5OTl9.ichno9utOYwVv1qoQWtUpDap7PGYze-zfXRZU31CMnQ"; + Jwt validJwt = new Jwt(); + validJwt.setJwt(validJwtStr); + when(tokenService.verifyJwt(validJwt.getJwt(),"test-stream","publish")).thenReturn(true); when(store.blackListToken(any())).thenReturn(true); - result = restServiceSpy.blackListOrWhitelistJwt(validJwt, false); + result = restServiceSpy.blackListJwt(validJwt); assertTrue(result.isSuccess()); - when(store.getBlackListedToken(validJwt)).thenReturn(token); + when(store.getBlackListedToken(validJwt.getJwt())).thenReturn(token); - result = restServiceSpy.blackListOrWhitelistJwt(validJwt, false); + result = restServiceSpy.blackListJwt(validJwt); assertFalse(result.isSuccess()); } @@ -3186,28 +3191,28 @@ public void testWhitelistJwt() { restServiceSpy.setAppCtx(appContext); restServiceSpy.setAppSettings(appSettings); - String jwt = "test-jwt"; + String jwtStr = "test-jwt"; DataStore store = mock(MapDBStore.class); restServiceSpy.setDataStore(store); - Result result1 = restServiceSpy.blackListOrWhitelistJwt(jwt, true); + Result result1 = restServiceSpy.whiteListJwt(jwtStr); assertFalse(result1.isSuccess()); when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); - when(store.getBlackListedToken(jwt)).thenReturn(null); - Result result2 = restServiceSpy.blackListOrWhitelistJwt(jwt, true); + when(store.getBlackListedToken(jwtStr)).thenReturn(null); + Result result2 = restServiceSpy.whiteListJwt(jwtStr); assertFalse(result2.isSuccess()); Token token = mock(Token.class); - when(store.getBlackListedToken(jwt)).thenReturn(token); - Result result3 = restServiceSpy.blackListOrWhitelistJwt(jwt, true); + when(store.getBlackListedToken(jwtStr)).thenReturn(token); + Result result3 = restServiceSpy.whiteListJwt(jwtStr); assertFalse(result3.isSuccess()); - when(store.whiteListToken(jwt)).thenReturn(true); - Result result4 = restServiceSpy.blackListOrWhitelistJwt(jwt, true); + when(store.whiteListToken(jwtStr)).thenReturn(true); + Result result4 = restServiceSpy.whiteListJwt(jwtStr); assertTrue(result4.isSuccess()); } From 9002d6cdcf2193407c0c0e26431bed2d7767225d Mon Sep 17 00:00:00 2001 From: lastpeony Date: Tue, 13 Jun 2023 21:40:49 +0300 Subject: [PATCH 4/5] refactor integration test --- .../integration/RestServiceV2Test.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/test/java/io/antmedia/integration/RestServiceV2Test.java b/src/test/java/io/antmedia/integration/RestServiceV2Test.java index 0f081d530..eb3264011 100644 --- a/src/test/java/io/antmedia/integration/RestServiceV2Test.java +++ b/src/test/java/io/antmedia/integration/RestServiceV2Test.java @@ -28,6 +28,7 @@ import javax.servlet.ServletContext; import javax.ws.rs.core.Context; +import com.google.gson.JsonObject; import io.antmedia.AppSettings; import io.antmedia.EncoderSettings; @@ -2018,11 +2019,11 @@ public void testJwtBlacklist(){ return; } - final AppSettings appSettingsModel = ConsoleAppRestServiceTest.callGetAppSettings("LiveApp"); + final AppSettings appSettingsModel = ConsoleAppRestServiceTest.callGetAppSettings("cleanapp"); appSettingsModel.setJwtStreamSecretKey("testtesttesttesttesttesttesttest"); appSettingsModel.setJwtBlacklistEnabled(true); - result = ConsoleAppRestServiceTest.callSetAppSettings("LiveApp", appSettingsModel); + result = ConsoleAppRestServiceTest.callSetAppSettings("cleanapp", appSettingsModel); assertTrue(result.isSuccess()); final String clearJwtBlacklistUrl = ROOT_SERVICE_URL + "/v2/broadcasts/jwt-black-list-clear"; @@ -2045,17 +2046,27 @@ public void testJwtBlacklist(){ final String validJwt2 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicHVibGlzaCIsImV4cCI6OTg4NzUwNzUwMX0.f4YTJUOmO7yuGpD7W4i_fffv2IVi1JB3mZVxNv8LSdI"; final String validJwt3 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicHVibGlzaCIsImV4cCI6OTg4NzUwNzUwMn0.bSbIumeAeM-k5zLndtII49z_458L8Lqg3eVweahvpb4"; - String jwtRequestBody1 = new JSONObject().put("jwt", validJwt1).toString(); + + JsonObject jwtRequest1 = new JsonObject(); + jwtRequest1.addProperty("jwt",validJwt1); + + JsonObject jwtRequest2 = new JsonObject(); + jwtRequest2.addProperty("jwt",validJwt2); + + JsonObject jwtRequest3 = new JsonObject(); + jwtRequest3.addProperty("jwt",validJwt3); + + String jwtRequestBody1 = gson.toJson(jwtRequest1); HttpPost addJwtRequest1 = new HttpPost(jwtBlacklistUrl); addJwtRequest1.setHeader("Content-Type", "application/json"); addJwtRequest1.setEntity(new StringEntity(jwtRequestBody1)); - String jwtRequestBody2 = new JSONObject().put("jwt", validJwt2).toString(); + String jwtRequestBody2 = gson.toJson(jwtRequest2); HttpPost addJwtRequest2 = new HttpPost(jwtBlacklistUrl); addJwtRequest2.setHeader("Content-Type", "application/json"); addJwtRequest2.setEntity(new StringEntity(jwtRequestBody2)); - String jwtRequestBody3 = new JSONObject().put("jwt", validJwt3).toString(); + String jwtRequestBody3 = gson.toJson(jwtRequest3); HttpPost addJwtRequest3 = new HttpPost(jwtBlacklistUrl); addJwtRequest3.setHeader("Content-Type", "application/json"); addJwtRequest3.setEntity(new StringEntity(jwtRequestBody3)); @@ -2151,7 +2162,10 @@ public void testJwtBlacklist(){ } catch (Exception e){ - logger.error(e.toString()); + e.printStackTrace(); + + fail(e.getMessage()); + } } From 2fc0ea8cf2ff5f03de92b7e1c9656ea7673b9c5f Mon Sep 17 00:00:00 2001 From: lastpeony Date: Mon, 19 Jun 2023 01:48:58 +0300 Subject: [PATCH 5/5] jwt blacklist for inmemory db --- .../datastore/db/InMemoryDataStore.java | 68 +++++++++++++++++-- .../antmedia/rest/BroadcastRestService.java | 10 +-- src/main/java/io/antmedia/rest/model/Jwt.java | 7 ++ .../io/antmedia/test/AppSettingsUnitTest.java | 3 +- .../rest/BroadcastRestServiceV2UnitTest.java | 10 +-- 5 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java index ea02f1477..79632d5de 100644 --- a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java +++ b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java @@ -4,8 +4,11 @@ import java.time.Instant; import java.util.*; import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import io.antmedia.rest.model.Result; import io.antmedia.security.ITokenService; import org.apache.commons.io.FilenameUtils; @@ -38,8 +41,11 @@ public class InMemoryDataStore extends DataStore { private Map roomMap = new LinkedHashMap<>(); private Map webRTCViewerMap = new LinkedHashMap<>(); + private Gson gson; public InMemoryDataStore(String dbName) { + GsonBuilder builder = new GsonBuilder(); + gson = builder.create(); available = true; } @@ -899,22 +905,65 @@ public boolean deleteToken(String tokenId) { @Override public boolean whiteListToken(String tokenId) { + Token token = getToken(tokenId); + if(token != null && token.isBlackListed()){ + token.setBlackListed(false); + return saveToken(token); + } + + return false; } @Override public List getBlackListedTokens() { - return Collections.emptyList(); + + ArrayList tokenBlacklist = new ArrayList<>(); + tokenMap.forEach((tokenId, token) -> { + if(token.isBlackListed()){ + tokenBlacklist.add(tokenId); + } + }); + return tokenBlacklist; + } @Override public Result deleteAllBlacklistedExpiredTokens(ITokenService tokenService) { - return null; + logger.info("Deleting all expired JWTs from token storage."); + AtomicInteger deletedTokenCount = new AtomicInteger(); + + tokenMap.forEach((tokenId, token) -> { + if(token.isBlackListed() && !tokenService.verifyJwt(tokenId,token.getStreamId(),token.getType())){ + if(deleteToken(tokenId)){ + deletedTokenCount.getAndIncrement(); + }else{ + logger.warn("Couldn't delete JWT:{}", tokenId); + } + } + }); + + + if(deletedTokenCount.get() > 0){ + final String successMsg = deletedTokenCount+" JWT deleted successfully from storage."; + logger.info(successMsg); + return new Result(true, successMsg); + }else{ + final String failMsg = "No JWT deleted from storage."; + logger.warn(failMsg); + return new Result(false, failMsg); + } } @Override public boolean whiteListAllTokens() { - throw new UnsupportedOperationException(""); + tokenMap.forEach((tokenId, token) -> { + if(token.isBlackListed()){ + whiteListToken(tokenId); + } + }); + + return true; } @Override @@ -1060,11 +1109,22 @@ public boolean updateStreamMetaData(String streamId, String metaData) { @Override public boolean blackListToken(Token token) { - return false; + boolean result = false; + + if (token.getStreamId() != null && token.getTokenId() != null) { + token.setBlackListed(true); + return saveToken(token); + } + + return result; } @Override public Token getBlackListedToken(String tokenId) { + Token token = getToken(tokenId); + if(token != null && token.isBlackListed()){ + return token; + } return null; } } \ No newline at end of file diff --git a/src/main/java/io/antmedia/rest/BroadcastRestService.java b/src/main/java/io/antmedia/rest/BroadcastRestService.java index 0b44e5744..b1380f021 100644 --- a/src/main/java/io/antmedia/rest/BroadcastRestService.java +++ b/src/main/java/io/antmedia/rest/BroadcastRestService.java @@ -613,7 +613,7 @@ public Result validateTokenV2(@ApiParam(value = "Token to be validated", require @Consumes(MediaType.APPLICATION_JSON) @Path("/jwt-black-list") @Produces(MediaType.APPLICATION_JSON) - public Result blackListJwt(@ApiParam(value = "jwt to be added to blacklist.", required = true) @RequestBody Jwt jwt) + public Result blackListJwt(@ApiParam(value = "jwt to be added to blacklist.", required = true) Jwt jwt) { if(getAppSettings().isJwtBlacklistEnabled()){ @@ -662,14 +662,14 @@ public Result blackListJwt(@ApiParam(value = "jwt to be added to blacklist.", re @Consumes(MediaType.APPLICATION_JSON) @Path("/jwt-black-list") @Produces(MediaType.APPLICATION_JSON) - public Result whiteListJwt(@ApiParam(value = "Jwt to be removed from blacklist.", required = true) @QueryParam("jwt") String jwt) + public Result whiteListJwt(@ApiParam(value = "Jwt to be removed from blacklist.", required = true) Jwt jwt) { if(getAppSettings().isJwtBlacklistEnabled()){ - if(getDataStore().getBlackListedToken(jwt) == null){ + if(getDataStore().getBlackListedToken(jwt.getJwt()) == null){ return new Result(false, "JWT does not exist in blacklist."); - }else if(getDataStore().whiteListToken(jwt)){ + }else if(getDataStore().whiteListToken(jwt.getJwt())){ return new Result(true, "JWT successfully removed from blacklist."); }else{ @@ -719,7 +719,7 @@ public Result clearJwtBlacklist() if(getAppSettings().isJwtBlacklistEnabled()) { getDataStore().whiteListAllTokens(); if(getDataStore().getBlackListedTokens().isEmpty()){ - return new Result(true, "All blacklisted tokens are whitelisted successfully."); + return new Result(true, "All blacklisted tokens are removed successfully."); }else{ return new Result(false, "JWT blacklist clear failed."); } diff --git a/src/main/java/io/antmedia/rest/model/Jwt.java b/src/main/java/io/antmedia/rest/model/Jwt.java index a30daca63..5139c4e50 100644 --- a/src/main/java/io/antmedia/rest/model/Jwt.java +++ b/src/main/java/io/antmedia/rest/model/Jwt.java @@ -1,6 +1,13 @@ package io.antmedia.rest.model; +import dev.morphia.annotations.Entity; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel(value="jwt", description="The basic jwt class for jwt blacklist") +@Entity(value = "jwt") public class Jwt { + @ApiModelProperty(value = "the jwt") private String jwt; // Default constructor diff --git a/src/test/java/io/antmedia/test/AppSettingsUnitTest.java b/src/test/java/io/antmedia/test/AppSettingsUnitTest.java index 9074cdf0d..a8ceb510c 100644 --- a/src/test/java/io/antmedia/test/AppSettingsUnitTest.java +++ b/src/test/java/io/antmedia/test/AppSettingsUnitTest.java @@ -510,6 +510,7 @@ public void testUnsetAppSettings(AppSettings appSettings) { assertEquals(30, appSettings.getAbrUpScaleJitterMs(), 0.0001); assertEquals(150, appSettings.getAbrUpScaleRTTMs(), 0.0001); assertNotNull(appSettings.getClusterCommunicationKey()); + assertEquals(false, appSettings.isJwtBlacklistEnabled()); @@ -518,7 +519,7 @@ public void testUnsetAppSettings(AppSettings appSettings) { //by also checking its default value. assertEquals("New field is added to settings. PAY ATTENTION: Please CHECK ITS DEFAULT VALUE and fix the number of fields.", - 164, numberOfFields); + 165, numberOfFields); } diff --git a/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java b/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java index 2714aa48f..a13ff45e4 100644 --- a/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java +++ b/src/test/java/io/antmedia/test/rest/BroadcastRestServiceV2UnitTest.java @@ -3192,27 +3192,29 @@ public void testWhitelistJwt() { restServiceSpy.setAppSettings(appSettings); String jwtStr = "test-jwt"; + Jwt jwt = new Jwt(); + jwt.setJwt(jwtStr); DataStore store = mock(MapDBStore.class); restServiceSpy.setDataStore(store); - Result result1 = restServiceSpy.whiteListJwt(jwtStr); + Result result1 = restServiceSpy.whiteListJwt(jwt); assertFalse(result1.isSuccess()); when(appSettings.isJwtBlacklistEnabled()).thenReturn(true); when(store.getBlackListedToken(jwtStr)).thenReturn(null); - Result result2 = restServiceSpy.whiteListJwt(jwtStr); + Result result2 = restServiceSpy.whiteListJwt(jwt); assertFalse(result2.isSuccess()); Token token = mock(Token.class); when(store.getBlackListedToken(jwtStr)).thenReturn(token); - Result result3 = restServiceSpy.whiteListJwt(jwtStr); + Result result3 = restServiceSpy.whiteListJwt(jwt); assertFalse(result3.isSuccess()); when(store.whiteListToken(jwtStr)).thenReturn(true); - Result result4 = restServiceSpy.whiteListJwt(jwtStr); + Result result4 = restServiceSpy.whiteListJwt(jwt); assertTrue(result4.isSuccess()); }