Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

jwt blacklist stop play/publish using jwt #4847

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open

Conversation

lastpeony
Copy link
Contributor

@lastpeony lastpeony commented Feb 28, 2023

#4809

This pull request introduces a JWT blacklist implementation, which allows users to ban viewers who are already watching or publishing streams. Since JWTs are not meant to be stored in a database, this feature is not enabled by default. However, a new setting called "jwtBlacklistEnabled" has been added to app settings. Otherwise, each request with a JWT would trigger a database lookup on disk, leading to degraded performance. This feature has several use cases, such as stopping HLS/DASH and WebRTC viewers who are already watching, and stoping WebRTC publishers from publishing who are already publishing. Mostly useful for AMS users who are mapping their viewers using JWT tokens by including some userId in token.

There is no new data storage created to avoid token duplication since tokens are already stored in database on disk. Adding a token to blacklist means setting its blackListed flag to true. If token doesnt exist it inserts. Whitelisting a token means setting its blackListed flag to false.

For example, AMS users who wants to prevent a viewer who is already watching a stream with HLS/DASH can enable the JWT blacklist feature and add the user's JWT token to the blacklist(set its blacklisted flag to true, if not exists insert with blacklisted flag true) using a new API call. When the viewer requests the next video chunk, the server won't return it because the token is blacklisted. To allow the viewer to watch again, the user can whitelist the token by using same API call with whiteFlag parameter true.
Example blacklist http call:
curl --location --request POST 'http://localhost:5080/WebRTCAppEE/rest/v2/broadcasts/jwt-black-list?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicGxheSIsImV4cCI6Mjg4NzUwOTMwMX0.pZ2X8HxqXiUn_pSheky_J61z2HEs-cgjG8ikB4MZ1AE' \
image
Example whitelist http call:
curl --location --request POST 'http://localhost:5080/WebRTCAppEE/rest/v2/broadcasts/jwt-black-list?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdHJlYW1JZCI6InRlc3RzdHJlYW0iLCJ0eXBlIjoicGxheSIsImV4cCI6Mjg4NzUwOTMwMX0.pZ2X8HxqXiUn_pSheky_J61z2HEs-cgjG8ikB4MZ1AE&whiteList=true' \
image

Similarly, if an AMS user wants to prevent a WebRTC viewer from watching a stream, they can stop the viewer using the "/webrtc-viewers/stop" API route with users JWT. This call will disconnect webrtc viewer. Then AMS user can add JWT to blacklist so when page is refreshed stream wont play.
https://github.com/ant-media/Ant-Media-Server/pull/4738/files#diff-c0d849875e95960ae6cb3780196fe1dcb6d87a19f9e5f2784627c074752edac0

JWT's in AMS needs to contain streamId. AMS user can use stopStreamingV2 api call to stop given stream by extracting stream Id from users JWT. After stoping publish, user's JWT can be added to the blacklist, preventing them from publishing again until the token is whitelisted with api call.

There are also two other utility api calls /jwt-black-list-delete-expired and /jwt-black-list-clear
/jwt-black-list-delete-expired deletes all expired(invalid) and blacklisted tokens from database.
image

/jwt-black-list-clear white lists all jwts.
image

jwt-black-list-delete-expired can be valuable if used periodically to prevent the tokenlist from growing infinitely when too many items are added.

Overall, this pull request enhances the functionality of AMS by adding a JWT blacklist feature that gives users greater control over who can watch and publish streams. Mostly useful to stop playing and publishing users who are already doing it.

API calls can be refactored to different names and style if found complicated.

final JSONParser parser = new JSONParser();
try {
final JSONObject jwtPayload = (JSONObject) parser.parse(payload);
final String tokenType = jwtPayload.get("type").toString();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7% of developers fix this issue

NULLPTR_DEREFERENCE: null (last assigned on line 1283) is dereferenced.


ℹ️ Expand to see all @sonatype-lift commands

You can reply with the following commands. For example, reply with @sonatype-lift ignoreall to leave out all findings.

Command Usage
@sonatype-lift ignore Leave out the above finding from this PR
@sonatype-lift ignoreall Leave out all the existing findings from this PR
@sonatype-lift exclude <file|issue|path|tool> Exclude specified file|issue|path|tool from Lift findings by updating your config.toml file

Note: When talking to LiftBot, you need to refresh the page to see its response.
Click here to add LiftBot to another repo.


Help us improve LIFT! (Sonatype LiftBot external survey)

Was this a good recommendation for you? Answering this survey will not impact your Lift settings.

[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]

logger.info("Deleting all expired JWTs from black list.");

AtomicInteger deletedTokenCount = new AtomicInteger();
tokenBlacklistMap.forEach((key, value) -> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6% of developers fix this issue

THREAD_SAFETY_VIOLATION: Read/Write race. Non-private method MapBasedDataStore.deleteAllExpiredJwtFromBlacklist(...) reads without synchronization from container this.tokenBlacklistMap via call to Map.forEach(...). Potentially races with write in method MapBasedDataStore.deleteTokenFromBlacklist(...).
Reporting because another access to the same memory occurs on a background thread, although this access may not.


ℹ️ Expand to see all @sonatype-lift commands

You can reply with the following commands. For example, reply with @sonatype-lift ignoreall to leave out all findings.

Command Usage
@sonatype-lift ignore Leave out the above finding from this PR
@sonatype-lift ignoreall Leave out all the existing findings from this PR
@sonatype-lift exclude <file|issue|path|tool> Exclude specified file|issue|path|tool from Lift findings by updating your config.toml file

Note: When talking to LiftBot, you need to refresh the page to see its response.
Click here to add LiftBot to another repo.


Help us improve LIFT! (Sonatype LiftBot external survey)

Was this a good recommendation for you? Answering this survey will not impact your Lift settings.

[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]

@sonatype-lift
Copy link

sonatype-lift bot commented Feb 28, 2023

🛠 Lift Auto-fix

Some of the Lift findings in this PR can be automatically fixed. You can download and apply these changes in your local project directory of your branch to review the suggestions before committing.1

# Download the patch
curl https://lift.sonatype.com/api/patch/github.com/ant-media/Ant-Media-Server/4847.diff -o lift-autofixes.diff

# Apply the patch with git
git apply lift-autofixes.diff

# Review the changes
git diff

Want it all in a single command? Open a terminal in your project's directory and copy and paste the following command:

curl https://lift.sonatype.com/api/patch/github.com/ant-media/Ant-Media-Server/4847.diff | git apply

Once you're satisfied, commit and push your changes in your project.

Footnotes

  1. You can preview the patch by opening the patch URL in the browser.

@Produces(MediaType.APPLICATION_JSON)
public List<String> getJwtBlacklist()
{
if(getAppSettings().isJwtBlacklistEnabled()) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

18% of developers fix this issue

NULL_DEREFERENCE: object returned by getAppSettings() could be null and is dereferenced at line 687.


ℹ️ Expand to see all @sonatype-lift commands

You can reply with the following commands. For example, reply with @sonatype-lift ignoreall to leave out all findings.

Command Usage
@sonatype-lift ignore Leave out the above finding from this PR
@sonatype-lift ignoreall Leave out all the existing findings from this PR
@sonatype-lift exclude <file|issue|path|tool> Exclude specified file|issue|path|tool from Lift findings by updating your config.toml file

Note: When talking to LiftBot, you need to refresh the page to see its response.
Click here to add LiftBot to another repo.


Help us improve LIFT! (Sonatype LiftBot external survey)

Was this a good recommendation for you? Answering this survey will not impact your Lift settings.

[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]

jwt. blacklist using already existed token db with flag
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()){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16% of developers fix this issue

NULL_DEREFERENCE: object returned by getAppSettings() could be null and is dereferenced at line 618.


ℹ️ Expand to see all @sonatype-lift commands

You can reply with the following commands. For example, reply with @sonatype-lift ignoreall to leave out all findings.

Command Usage
@sonatype-lift ignore Leave out the above finding from this PR
@sonatype-lift ignoreall Leave out all the existing findings from this PR
@sonatype-lift exclude <file|issue|path|tool> Exclude specified file|issue|path|tool from Lift findings by updating your config.toml file

Note: When talking to LiftBot, you need to refresh the page to see its response.
Click here to add LiftBot to another repo.


Help us improve LIFT! (Sonatype LiftBot external survey)

Was this a good recommendation for you? Answering this survey will not impact your Lift settings.

[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated but i didnt like the naming of this blackListOrWhitelistJwt function but couldnt find something better. Previously there were 2 different api calls one for blacklisting other for whitelisting but i changed to this. im sure someone can come up with a better naming.

@sonarcloud
Copy link

sonarcloud bot commented Mar 2, 2023

SonarCloud Quality Gate failed.    Quality Gate failed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 6 Code Smells

81.4% 81.4% Coverage
5.3% 5.3% Duplication

@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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sending a POST to /jwt-black-list?whiteList=true seems a bit weird to me, just read back the docs: "unblacklist".. Would it not be clearer if one had to send a DELETE request to the collection /jwt-black-list to get something removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not removing anything from disk thats why i made it a post call.
But we can also make it a delete

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the disk that matters, but it does remove someone from the blacklist, so I think a DELETE would be a better choice.

*
* @return
*/
public abstract boolean whiteListAllTokens();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concept of allowing only people with valid tokens is implicitly a whitelist, so I think you should not give the impression that there is a separate whitelist and a blacklist. Is it not only clearing the blacklist..?

@Override
public boolean whiteListAllTokens(){

synchronized (this) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this as a synchronization point might not be a good choice as other classes can still hold a lock on this class. I would suggest using a non-public internal locking mechanism for synchronization purposes, which is more fine-grained and only applies to operations on the tokenMap, e.g. a ReadWriteLock or similar.

public boolean blackListToken(Token token) {
boolean result = false;

synchronized (this) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd revisit the usage of synchronized in this class, as there seems to be some overuse and overscoping. Using this as a monitor is not very fortunate, as it can be held externally. Here the monitor is obtained too early, only saveToken seems to be critical, but as far as I can see saveToken itself also takes care of synchronization using this, so this might be completely unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree

public boolean blackListToken(Token token) {
boolean result = false;
//update if exists, else insert
synchronized (this) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scope of the synchronized block looks unnecessarily broad and blocks other concurrent operations in the store. Sorry if I misunderstand, but if you are trying to ensure some kind of atomic transaction here so that the find-update-save cannot be interrupted, then what about other JVM participants in the cluster, that are not held up by the lock, hence can still concurrently fiddle with Mongo?

synchronized (this){
Query<Token> query = tokenDatastore.find(Token.class).filter(Filters.eq(BLACKLISTED, true));
for (Token token : query) {
tokenBlacklist.add(token.getTokenId());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to hold the monitor as long as the iteration processes all the tokens?

@jantekb
Copy link
Contributor

jantekb commented Mar 20, 2023

I would suggest not using query parameters for mandatory data required for a POST to happen. Concretely I would not recommend sending the blacklisted JWT via query parameter, but POST body. Query parameters could be used for optional things instead (remember the ? mark), that narrow down or refine the results of a GET request.

@jantekb jantekb mentioned this pull request Mar 20, 2023
@Produces(MediaType.APPLICATION_JSON)
public Result blackListJwt(@ApiParam(value = "jwt to be added to blacklist.", required = true) @RequestBody Jwt jwt)
{
if(getAppSettings().isJwtBlacklistEnabled()){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16% of developers fix this issue

NULL_DEREFERENCE: object returned by getAppSettings() could be null and is dereferenced at line 618.


ℹ️ Expand to see all @sonatype-lift commands

You can reply with the following commands. For example, reply with @sonatype-lift ignoreall to leave out all findings.

Command Usage
@sonatype-lift ignore Leave out the above finding from this PR
@sonatype-lift ignoreall Leave out all the existing findings from this PR
@sonatype-lift exclude <file|issue|path|tool> Exclude specified file|issue|path|tool from Lift findings by updating your config.toml file

Note: When talking to LiftBot, you need to refresh the page to see its response.
Click here to add LiftBot to another repo.

@Produces(MediaType.APPLICATION_JSON)
public Result whiteListJwt(@ApiParam(value = "Jwt to be removed from blacklist.", required = true) @QueryParam("jwt") String jwt)
{
if(getAppSettings().isJwtBlacklistEnabled()){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16% of developers fix this issue

NULL_DEREFERENCE: object returned by getAppSettings() could be null and is dereferenced at line 667.


ℹ️ Expand to see all @sonatype-lift commands

You can reply with the following commands. For example, reply with @sonatype-lift ignoreall to leave out all findings.

Command Usage
@sonatype-lift ignore Leave out the above finding from this PR
@sonatype-lift ignoreall Leave out all the existing findings from this PR
@sonatype-lift exclude <file|issue|path|tool> Exclude specified file|issue|path|tool from Lift findings by updating your config.toml file

Note: When talking to LiftBot, you need to refresh the page to see its response.
Click here to add LiftBot to another repo.

@lastpeony lastpeony force-pushed the tokenBlacklist branch 2 times, most recently from a16a4c1 to 06b4627 Compare June 20, 2023 06:34
@Produces(MediaType.APPLICATION_JSON)
public Result whiteListJwt(@ApiParam(value = "Jwt to be removed from blacklist.", required = true) Jwt jwt)
{
if(getAppSettings().isJwtBlacklistEnabled()){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15% of developers fix this issue

NULL_DEREFERENCE: object returned by getAppSettings() could be null and is dereferenced at line 667.


ℹ️ Expand to see all @sonatype-lift commands

You can reply with the following commands. For example, reply with @sonatype-lift ignoreall to leave out all findings.

Command Usage
@sonatype-lift ignore Leave out the above finding from this PR
@sonatype-lift ignoreall Leave out all the existing findings from this PR
@sonatype-lift exclude <file|issue|path|tool> Exclude specified file|issue|path|tool from Lift findings by updating your config.toml file

Note: When talking to LiftBot, you need to refresh the page to see its response.
Click here to add LiftBot to another repo.

@sonarcloud
Copy link

sonarcloud bot commented Jun 20, 2023

SonarCloud Quality Gate failed.    Quality Gate failed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 7 Code Smells

66.0% 66.0% Coverage
10.0% 10.0% Duplication

idea Catch issues before they fail your Quality Gate with our IDE extension sonarlint SonarLint

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants