Skip to content

Commit 86d657b

Browse files
authored
Merge pull request #89 from prime-framework/degroff/always_bind_jwtAuthorizeMethods5x
degroff/always bind JWT authorize methods5x
2 parents 399156b + ec9ac8f commit 86d657b

File tree

9 files changed

+131
-18
lines changed

9 files changed

+131
-18
lines changed

build.savant

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ logbackVersion = "1.5.13"
2929
slf4jVersion = "2.0.13"
3030
testngVersion = "7.8.0"
3131

32-
project(group: "org.primeframework", name: "prime-mvc", version: "5.2.0", licenses: ["ApacheV2_0"]) {
32+
project(group: "org.primeframework", name: "prime-mvc", version: "5.3.0", licenses: ["ApacheV2_0"]) {
3333
workflow {
3434
fetch {
3535
// Dependency resolution order:

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>org.primeframework</groupId>
77
<artifactId>prime-mvc</artifactId>
8-
<version>5.2.0</version>
8+
<version>5.3.0</version>
99
<packaging>jar</packaging>
1010

1111
<name>FusionAuth App</name>

src/main/java/org/primeframework/mvc/action/config/DefaultActionConfigurationBuilder.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012-2024, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2012-2025, Inversoft Inc., All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -412,8 +412,15 @@ protected Map<HTTPMethod, ExecuteMethodConfiguration> findExecuteMethods(Class<?
412412
protected Map<HTTPMethod, List<JWTMethodConfiguration>> findJwtAuthorizationMethods(Class<?> actionClass,
413413
List<String> securitySchemes,
414414
Map<HTTPMethod, ExecuteMethodConfiguration> executeMethods) {
415-
// When JWT scheme is not enabled, we will not call any of the JWT Authorization Methods.
416-
if (!securitySchemes.contains("jwt")) {
415+
// When a JWT scheme is not enabled, we will not call any of the JWT Authorization Methods.
416+
// - Note that anyone can bind a jwt security scheme, and they may wish to use these authorization methods.
417+
// So as long as the scheme contains "jwt", bind them. For example, you may wish to bind `jwt-1` and `jwt-2` with
418+
// different constraint validations.
419+
// - In theory we could just always bind them, however we do validate that the action is properly configured to
420+
// have a method to cover the expected HTTP verbs, etc. Ideally we would keep this logic to keep the user from
421+
// not using these methods correctly. But if we wanted to tell the user it's their problem, we could remove
422+
// some of that validation and just always bind them.
423+
if (securitySchemes.stream().noneMatch(s -> s.contains("jwt"))) {
417424
return Collections.emptyMap();
418425
}
419426

@@ -454,15 +461,17 @@ protected Map<HTTPMethod, List<JWTMethodConfiguration>> findJwtAuthorizationMeth
454461

455462
if (!jwtMethods.keySet().containsAll(authenticatedMethods)) {
456463
throw new PrimeException("The action class [" + actionClass + "] is missing at a JWT Authorization method. " +
457-
"The class must define one or more methods annotated " + JWTAuthorizeMethod.class.getSimpleName() + " when [jwtEnabled] is set to [true]. "
458-
+ "Ensure that for each execute method in your action such as post, put, get and delete that a method is configured to authorize the JWT.");
464+
"The class must define one or more methods annotated " + JWTAuthorizeMethod.class.getSimpleName() + " when [jwtEnabled] " +
465+
"is set to [true], which is deprecated, or you are using a jwt based security scheme. Your action has " +
466+
"defined the following security schemes [" + String.join(", ", securitySchemes) + "].Ensure that for each execute " +
467+
"method in your action such as post, put, get and delete that a method is configured to authorize the JWT.");
459468
}
460469

461470
return jwtMethods;
462471
}
463472

464473
/**
465-
* Finds all of the result configurations for the action class.
474+
* Finds all the result configurations for the action class.
466475
*
467476
* @param actionClass The action class.
468477
* @return The map of all the result configurations.

src/test/java/org/example/action/JwtAuthorizedAction.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016-2018, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2016-2025, Inversoft Inc., All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,7 +24,7 @@
2424
/**
2525
* @author Daniel DeGroff
2626
*/
27-
@Action(requiresAuthentication = true, jwtEnabled = true)
27+
@Action(requiresAuthentication = true, scheme = "jwt")
2828
@Status.List({
2929
@Status(),
3030
@Status(code = "unauthenticated", status = 401),
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2025, Inversoft Inc., All Rights Reserved
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package org.example.action;
17+
18+
import io.fusionauth.http.HTTPValues.Methods;
19+
import io.fusionauth.jwt.domain.JWT;
20+
import org.primeframework.mvc.action.annotation.Action;
21+
import org.primeframework.mvc.action.result.annotation.Status;
22+
import org.primeframework.mvc.security.annotation.JWTAuthorizeMethod;
23+
24+
/**
25+
* @author Daniel DeGroff
26+
*/
27+
@Action(requiresAuthentication = true, jwtEnabled = true)
28+
@Status.List({
29+
@Status(),
30+
@Status(code = "unauthenticated", status = 401),
31+
@Status(code = "unauthorized", status = 401)
32+
})
33+
// Use the deprecated jwtEnabled instead of scheme = jwt
34+
public class JwtAuthorizedDeprecatedAction {
35+
@JWTAuthorizeMethod(httpMethods = {Methods.GET})
36+
public boolean authorize(JWT jwt) {
37+
return true;
38+
}
39+
40+
public String get() {
41+
return "success";
42+
}
43+
}

src/test/java/org/example/action/JwtAuthorizedDisabledAction.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016-2018, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2016-2025, Inversoft Inc., All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,19 +23,16 @@
2323
/**
2424
* @author Daniel DeGroff
2525
*/
26-
@Action(requiresAuthentication = true, jwtEnabled = false)
26+
@Action(requiresAuthentication = true, scheme = "jwt")
2727
@Status.List({
2828
@Status(code = "success", status = 200),
2929
@Status(code = "unauthenticated", status = 401),
3030
@Status(code = "unauthorized", status = 401)
3131
})
3232
public class JwtAuthorizedDisabledAction {
33-
34-
public boolean authorized;
35-
3633
@JWTAuthorizeMethod
3734
public boolean authorize(JWT jwt) {
38-
return authorized;
35+
return false;
3936
}
4037

4138
public String get() {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2025, Inversoft Inc., All Rights Reserved
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package org.example.action;
17+
18+
import io.fusionauth.http.HTTPValues.Methods;
19+
import io.fusionauth.jwt.domain.JWT;
20+
import org.primeframework.mvc.action.annotation.Action;
21+
import org.primeframework.mvc.action.result.annotation.Status;
22+
import org.primeframework.mvc.security.annotation.JWTAuthorizeMethod;
23+
24+
/**
25+
* @author Daniel DeGroff
26+
*/
27+
@Action(requiresAuthentication = true, scheme = "jwt-other")
28+
@Status.List({
29+
@Status(),
30+
@Status(code = "unauthenticated", status = 401),
31+
@Status(code = "unauthorized", status = 401)
32+
})
33+
// Use a jwt scheme with a suffix, as long as the scheme name contains jwt it will work.
34+
public class JwtAuthorizedOtherAction {
35+
@JWTAuthorizeMethod(httpMethods = {Methods.GET})
36+
public boolean authorize(JWT jwt) {
37+
return true;
38+
}
39+
40+
public String get() {
41+
return "success";
42+
}
43+
}

src/test/java/org/primeframework/mvc/GlobalTest.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -808,14 +808,12 @@ public void get_jwtAuthorized() throws Exception {
808808
public void get_jwtDisabledJwtAuthentication() throws Exception {
809809
// Send in a JWT Authorization header when the Action has JWT disabled. Should always get a 401. When a JWT is provided, the action expects JWT to be enabled.
810810
test.simulate(() -> simulator.test("/jwt-authorized-disabled")
811-
.withURLParameter("authorized", true)
812811
.withHeader("Authorization", "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkifQ.qHdut1UR4-2FSAvh7U3YdeRR5r5boVqjIGQ16Ztp894")
813812
.get()
814813
.assertStatusCode(401));
815814

816815
// Same, use Bearer scheme
817816
test.simulate(() -> simulator.test("/jwt-authorized-disabled")
818-
.withURLParameter("authorized", true)
819817
.withHeader("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkifQ.qHdut1UR4-2FSAvh7U3YdeRR5r5boVqjIGQ16Ztp894")
820818
.get()
821819
.assertStatusCode(401));
@@ -866,6 +864,26 @@ public void get_jwtNotBefore() throws Exception {
866864
.assertStatusCode(401));
867865
}
868866

867+
@Test
868+
public void get_jwt_jwtEnabled_deprecated() throws Exception {
869+
// Test an action using the deprecated jwtEnabled instead of the jwt scheme
870+
test.simulate(() -> simulator.test("/jwt-authorized-deprecated")
871+
.withHeader("Authorization", "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkifQ.qHdut1UR4-2FSAvh7U3YdeRR5r5boVqjIGQ16Ztp894")
872+
.get()
873+
.assertHeaderContains("Cache-Control", "no-cache")
874+
.assertStatusCode(200));
875+
}
876+
877+
@Test
878+
public void get_jwt_other_scheme() throws Exception {
879+
// Test an action with a jwt based scheme, but not named 'jwt'
880+
test.simulate(() -> simulator.test("/jwt-authorized-other")
881+
.withHeader("Authorization", "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkifQ.qHdut1UR4-2FSAvh7U3YdeRR5r5boVqjIGQ16Ztp894")
882+
.get()
883+
.assertHeaderContains("Cache-Control", "no-cache")
884+
.assertStatusCode(200));
885+
}
886+
869887
@Test
870888
public void get_largeFTL() {
871889
simulator.test("/large-ftl")

src/test/java/org/primeframework/mvc/PrimeBaseTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
import org.primeframework.mvc.security.CBCCipherProvider;
7979
import org.primeframework.mvc.security.CipherProvider;
8080
import org.primeframework.mvc.security.GCMCipherProvider;
81+
import org.primeframework.mvc.security.JWTSecurityScheme;
8182
import org.primeframework.mvc.security.MockOAuthUserLoginSecurityContext;
8283
import org.primeframework.mvc.security.MockStaticClasspathResourceFilter;
8384
import org.primeframework.mvc.security.MockStaticResourceFilter;
@@ -87,6 +88,7 @@
8788
import org.primeframework.mvc.security.UserLoginSecurityContext;
8889
import org.primeframework.mvc.security.VerifierProvider;
8990
import org.primeframework.mvc.security.csrf.CSRFProvider;
91+
import org.primeframework.mvc.security.guice.SecuritySchemeFactory;
9092
import org.primeframework.mvc.test.RequestBuilder.HTTPRequestConsumer;
9193
import org.primeframework.mvc.test.RequestSimulator;
9294
import org.primeframework.mvc.util.ThrowingRunnable;
@@ -239,6 +241,7 @@ protected void configure() {
239241
bind(MessageObserver.class).toInstance(messageObserver);
240242
bind(MetricRegistry.class).toInstance(metricRegistry);
241243
bind(UserLoginSecurityContext.class).to(MockUserLoginSecurityContext.class);
244+
SecuritySchemeFactory.addSecurityScheme(binder(), "jwt-other", JWTSecurityScheme.class);
242245

243246
// Test Content-Type
244247
ContentHandlerFactory.addContentHandler(binder(), "application/test+json", JacksonContentHandler.class);

0 commit comments

Comments
 (0)