Skip to content

Commit 679e235

Browse files
authored
Merge pull request #7 from avaje/feature/add-helidon-jwtfilter
Add Helidon JwtAuthFilter
2 parents be60d37 + cfbd59a commit 679e235

File tree

9 files changed

+259
-0
lines changed

9 files changed

+259
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module io.avaje.oauth2.core {
2+
3+
exports io.avaje.oauth2.core.data;
4+
exports io.avaje.oauth2.core.jwt;
5+
6+
requires transitive io.avaje.json;
7+
requires transitive io.avaje.http.client;
8+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>io.avaje</groupId>
8+
<artifactId>avaje-oauth2-parent</artifactId>
9+
<version>0.1</version>
10+
</parent>
11+
12+
<artifactId>avaje-oauth2-helidon-jwtfilter</artifactId>
13+
14+
<properties>
15+
<maven.compiler.source>23</maven.compiler.source>
16+
<maven.compiler.target>23</maven.compiler.target>
17+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
18+
</properties>
19+
20+
<dependencies>
21+
<dependency>
22+
<groupId>io.avaje</groupId>
23+
<artifactId>avaje-oauth2-core</artifactId>
24+
<version>0.1</version>
25+
</dependency>
26+
<dependency>
27+
<groupId>io.helidon.webserver</groupId>
28+
<artifactId>helidon-webserver</artifactId>
29+
<version>4.2.0</version>
30+
<scope>provided</scope>
31+
</dependency>
32+
33+
</dependencies>
34+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package io.avaje.oauth2.helidon.jwtfilter;
2+
3+
import io.avaje.oauth2.core.data.AccessToken;
4+
import io.avaje.oauth2.core.jwt.JwtVerifier;
5+
import io.helidon.common.context.Context;
6+
import io.helidon.http.HeaderNames;
7+
import io.helidon.http.UnauthorizedException;
8+
import io.helidon.webserver.http.FilterChain;
9+
import io.helidon.webserver.http.RoutingRequest;
10+
import io.helidon.webserver.http.RoutingResponse;
11+
12+
import java.security.Principal;
13+
import java.util.List;
14+
15+
final class AuthFilter implements JwtAuthFilter {
16+
17+
private static final String BEARER_ = "Bearer ";
18+
private static final int BEARER_LENGTH = BEARER_.length();
19+
20+
private final JwtVerifier verifier;
21+
private final String[] allowedPaths;
22+
23+
AuthFilter(JwtVerifier verifier, List<String> allowedPaths) {
24+
this.verifier = verifier;
25+
this.allowedPaths = allowedPaths.toArray(new String[0]);
26+
}
27+
28+
@Override
29+
public void filter(FilterChain filterChain, RoutingRequest routingRequest, RoutingResponse routingResponse) {
30+
String header = routingRequest.headers().first(HeaderNames.AUTHORIZATION).orElse("");
31+
if (header.startsWith(BEARER_)) {
32+
String token = header.substring(BEARER_LENGTH);
33+
AccessToken accessToken = verifier.verifyAccessToken(token);
34+
Context context = routingRequest.context();
35+
context.register("security.principal", new TokenPrincipal(accessToken.clientId()));
36+
context.register("security.roles", accessToken.scope());
37+
filterChain.proceed();
38+
return;
39+
}
40+
41+
final String path = routingRequest.path().path();
42+
for (String allowedPath : allowedPaths) {
43+
if (path.startsWith(allowedPath)) {
44+
filterChain.proceed();
45+
return;
46+
}
47+
}
48+
throw new UnauthorizedException("Unauthorized");
49+
}
50+
51+
private static final class TokenPrincipal implements Principal {
52+
53+
private final String name;
54+
55+
TokenPrincipal(String name) {
56+
this.name = name;
57+
}
58+
59+
@Override
60+
public String getName() {
61+
return name;
62+
}
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.avaje.oauth2.helidon.jwtfilter;
2+
3+
import io.avaje.oauth2.core.jwt.JwtVerifier;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
8+
final class AuthFilterBuilder implements JwtAuthFilter.Builder {
9+
10+
private final List<String> allowedPaths = new ArrayList<>();
11+
private JwtVerifier verifier;
12+
13+
@Override
14+
public JwtAuthFilter.Builder permit(String path) {
15+
allowedPaths.add(path);
16+
return this;
17+
}
18+
19+
@Override
20+
public JwtAuthFilter.Builder verifier(JwtVerifier verifier) {
21+
this.verifier = verifier;
22+
return this;
23+
}
24+
25+
@Override
26+
public JwtAuthFilter build() {
27+
return new AuthFilter(verifier, allowedPaths);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.avaje.oauth2.helidon.jwtfilter;
2+
3+
import io.avaje.oauth2.core.jwt.JwtVerifier;
4+
import io.helidon.webserver.http.Filter;
5+
6+
/**
7+
* Filter that ensures a valid Signed JWT token is presented as a Authorization Bearer header.
8+
* <p>
9+
* The filter uses a JwtVerifier to verify the SignedJWT is valid.
10+
* <p>
11+
* The filter can allow some paths to not require a JWT token such as health endpoints.
12+
*
13+
* <pre>{@code
14+
*
15+
* String issuer = "https://cognito-idp.<region>.amazonaws.com/<region>_<foo>";
16+
*
17+
* JwtVerifier jwtVerifier = JwtVerifier.builder()
18+
* .issuer(issuer)
19+
* .build();
20+
*
21+
* JwtAuthFilter filter = JwtAuthFilter.builder()
22+
* .permit("/health")
23+
* .permit("/ping")
24+
* .verifier(jwtVerifier)
25+
* .build();
26+
*
27+
* }</pre>
28+
*/
29+
public interface JwtAuthFilter extends Filter {
30+
31+
/**
32+
* Return a builder for JwtAuthFilter.
33+
*/
34+
static Builder builder() {
35+
return new AuthFilterBuilder();
36+
}
37+
38+
/**
39+
* Builder for JwtAuthFilter.
40+
*/
41+
interface Builder {
42+
43+
/**
44+
* Permit paths starting with the given prefix to not require a JWT token.
45+
*
46+
* @param pathPrefix The path prefix that does not require a JWT token.
47+
*/
48+
Builder permit(String pathPrefix);
49+
50+
/**
51+
* Specify the JwtVerifier to use.
52+
*/
53+
Builder verifier(JwtVerifier verifier);
54+
55+
/**
56+
* Build and return the JwtAuthFilter.
57+
*/
58+
JwtAuthFilter build();
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module io.avaje.oauth2.helidon.jwtfilter {
2+
3+
exports io.avaje.oauth2.helidon.jwtfilter;
4+
5+
requires transitive io.avaje.oauth2.core;
6+
requires transitive io.helidon.webserver;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.avaje.oauth2.helidon.jwtfilter;
2+
3+
import io.avaje.oauth2.core.data.JsonDataMapper;
4+
import io.avaje.oauth2.core.data.KeySet;
5+
import io.avaje.oauth2.core.jwt.JwtKeySource;
6+
import io.avaje.oauth2.core.jwt.JwtVerifier;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.io.InputStream;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
class JwtAuthFilterTest {
14+
15+
@Test
16+
void build() {
17+
InputStream is = JwtAuthFilterTest.class.getResourceAsStream("/keys.json");
18+
JsonDataMapper jsonMapper = JsonDataMapper.builder().build();
19+
KeySet keySet = jsonMapper.readKeySet(is);
20+
21+
//String issuer = "https://cognito-idp.REGION.amazonaws.com/REGION_FOO";
22+
JwtVerifier jwtVerifier = JwtVerifier.builder()
23+
// .issuer(issuer)
24+
.addRS256()
25+
.keySource(JwtKeySource.build(keySet))
26+
.build();
27+
28+
JwtAuthFilter filter = JwtAuthFilter.builder()
29+
.permit("/health")
30+
.permit("/ping")
31+
.verifier(jwtVerifier)
32+
.build();
33+
34+
assertThat(filter).isNotNull();
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"keys": [
3+
{
4+
"alg": "RS256",
5+
"e": "AQAB",
6+
"kid": "jGyPpG803SsVfJ4mdDdVKDD9UnQk7GCAKqN2h3Of6ic=",
7+
"kty": "RSA",
8+
"n": "5GSe0QejeIrhbhBPgJBOOfr_KIW6o3wpt6aoR4D_ft48ToLxAQKq6WLq-Ccb4lKIk-j1DbW3lju3DepugwR3IDtUuNO-zCi8--tAI2k_XgU-9oWoEifnz5RD0wlezjxBCjBMxxzhowD_EjcmyN5WUv0u4f3VMnKBsTSWxTkrShzYnmIoo8WEFk-UQKxw9AgDV_VtN4na8NnXiygJ8q0eD-S1tqOz-cvTZeh2qhkLOXyd_dguC7sdlPLb5-I-jszSYx1Ic88Os3UuPqHyLYccVuEd8Jb0dal6625bgD6fQuWVkmdit9xuySJAMKRWT-CSCTDXYcEBm9Vk-PCZOHhAtw",
9+
"use": "sig"
10+
},
11+
{
12+
"alg": "RS256",
13+
"e": "AQAB",
14+
"kid": "iM8z2dS1yIA6PRVdA8gsNXgFlZJwRSM5WeynrLhalqk=",
15+
"kty": "RSA",
16+
"n": "w2eWpy1C_8dZNCb6tHTMz0Ahb6ZSpBPijK4tvW0rv1tghIdXo2h_NZ5qM-Gg25XLAxQdG3-SF9zUz3MLVnbzZCseFi12zqOe8bkzIk9yeHOarDvSaLn9xcWooSyhY-3vWp26VY-zNWsiDI8aZuZPQNb5tcnozo1LTH0G6RPwMiW_Djar9UIloFfgw7bScYUopyMW-asbFlN2QRVHX-JdxAIb8WDM_DBDR6Sdmd9WOuaapPwBS4BGGHiQlArVeFzoxN0VIM4HyXf1mDo039qDRngZf3ZXWeN29iT-PHEOksYN7PLCn0V6fsNVLl_4mLRLnwAnvTd9vIOU-R71T6d4aw",
17+
"use": "sig"
18+
}
19+
]
20+
}

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
<modules>
1818
<module>avaje-oauth2-core</module>
19+
<module>avaje-oauth2-helidon-jwtfilter</module>
1920
<module>avaje-oauth2-oidc-cognito</module>
2021
</modules>
2122

0 commit comments

Comments
 (0)