Skip to content

Commit

Permalink
Implement access token validator
Browse files Browse the repository at this point in the history
  • Loading branch information
QubitPi committed Sep 11, 2023
1 parent 73bb1e5 commit 3ec689a
Show file tree
Hide file tree
Showing 13 changed files with 547 additions and 15 deletions.
2 changes: 0 additions & 2 deletions docs/docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@ java -jar $JETTY_HOME/start.jar

The webservice will run on port **8080**, and you will see the data you inserted

[Docker Compose]: https://docs.docker.com/compose/

[jcabi-mysql]: https://mysql.jcabi.com/

[Testcontainers]: https://qubitpi.github.io/testcontainers-java/
54 changes: 54 additions & 0 deletions docs/docs/img/oauth2-filtering.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<mxfile host="app.diagrams.net" modified="2023-09-06T02:17:57.678Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" etag="kJ8t7kbPnoaa44JijZXw" version="21.7.2" type="device">
<diagram name="Page-1" id="qUw_CK1pjzlE3Cx89_ZL">
<mxGraphModel dx="1306" dy="1930" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="iVyjQRI55B28_YTLfMTB-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeWidth=3;strokeColor=#00AA00;fontStyle=1" parent="1" source="iVyjQRI55B28_YTLfMTB-1" target="iVyjQRI55B28_YTLfMTB-14" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-1" value="&lt;font face=&quot;Ubuntu&quot; color=&quot;#ffffff&quot;&gt;&lt;span style=&quot;font-size: 20px;&quot;&gt;&lt;span&gt;OAuth&lt;br&gt;Client&lt;br&gt;&lt;/span&gt;&lt;/span&gt;&lt;/font&gt;" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;strokeColor=none;fillColor=#F26C9D;fontStyle=1" parent="1" vertex="1">
<mxGeometry x="320" y="360" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-4" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeWidth=3;strokeColor=#F8C300;fontStyle=1" parent="1" vertex="1">
<mxGeometry x="620" y="350" width="320" height="190" as="geometry" />
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-5" value="&lt;font face=&quot;Ubuntu&quot; color=&quot;#ffffff&quot;&gt;&lt;span style=&quot;font-size: 20px;&quot;&gt;&lt;span&gt;Auth&lt;br&gt;Server&lt;br&gt;&lt;/span&gt;&lt;/span&gt;&lt;/font&gt;" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;strokeColor=none;fillColor=#E55FFB;fontStyle=1" parent="1" vertex="1">
<mxGeometry x="740" y="-100" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-8" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=1;entryDx=0;entryDy=0;strokeWidth=3;startArrow=classic;startFill=1;strokeColor=#00aa00;endArrow=none;endFill=0;fontStyle=1" parent="1" edge="1" target="iVyjQRI55B28_YTLfMTB-5" source="iVyjQRI55B28_YTLfMTB-1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="391.76" y="377.5999999999999" as="sourcePoint" />
<mxPoint x="761.76" y="-23.839999999999918" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-9" value="access_token (JWT)" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;rotation=313;fontFamily=Ubuntu;fontStyle=1;fontSize=20;" parent="1" vertex="1">
<mxGeometry x="325" y="165" width="540" height="30" as="geometry" />
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;strokeColor=#00AA00;strokeWidth=3;fontStyle=1" parent="1" source="iVyjQRI55B28_YTLfMTB-14" target="iVyjQRI55B28_YTLfMTB-15" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-14" value="&lt;font style=&quot;font-size: 20px;&quot; face=&quot;Ubuntu&quot;&gt;&lt;span&gt;ContainerRequestFilter&lt;/span&gt;&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#F8C300;strokeColor=#F8C300;fontStyle=1" parent="1" vertex="1">
<mxGeometry x="660" y="370" width="240" height="60" as="geometry" />
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-15" value="&lt;font style=&quot;font-size: 20px;&quot; face=&quot;Ubuntu&quot;&gt;&lt;span&gt;Endpoints&lt;/span&gt;&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#F8C300;strokeColor=#F8C300;fontStyle=1" parent="1" vertex="1">
<mxGeometry x="660" y="470" width="240" height="60" as="geometry" />
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-18" value="access_token" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;rotation=0;fontFamily=Ubuntu;fontStyle=1;fontSize=20;" parent="1" vertex="1">
<mxGeometry x="250" y="410" width="540" height="30" as="geometry" />
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;curved=1;strokeWidth=3;strokeColor=#00aa00;fontStyle=1" parent="1" source="iVyjQRI55B28_YTLfMTB-14" target="iVyjQRI55B28_YTLfMTB-14" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="720" y="300" />
<mxPoint x="840" y="300" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="iVyjQRI55B28_YTLfMTB-20" value="Validate access_token" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;rotation=0;fontFamily=Ubuntu;fontStyle=1;fontSize=20;" parent="1" vertex="1">
<mxGeometry x="510" y="260" width="540" height="30" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file added docs/docs/img/oauth2-filtering.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
219 changes: 219 additions & 0 deletions docs/docs/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
---
sidebar_position: 5
title: Security
---

Core Concepts
-------------

API authentication is largely a solved problem and generally outside the scope of Jersey WS Template.

Jersey WS Template does, however, adds a layer of security on its own by validating [OAuth 2 access token] on all
incoming request. Each API request requires a standard **"Authentication": "Bearer <access_token>"** token header:

![Error loading oauth2-filtering.png](./img/oauth2-filtering.png)

To define a token validator, simply implement the **AccessTokenValidator** like so:

```java
public class JwtTokenValidator implements AccessTokenValidator {

@Override
public boolean validate(@NotNull final String accessToken) {
// use https://github.com/auth0/java-jwt as recommended by https://jwt.io/
return true;
}
}
```

and bind the validator, in [BinderFactory], using

```java
bind(new JwtTokenValidator()).to(AccessTokenValidator.class);
```

For example, to validate a ES384 token using _AccessTokenValidator_, we may implement a _ES384JwtTokenValidator_
validator below:

:::note

Note that the implementation below depends on 2 JWT libraries:

```xml
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.22.1</version>
</dependency>
```

:::

```java
import com.auth0.jwk.InvalidPublicKeyException;
import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.JwkProviderBuilder;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Verification;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.validation.constraints.NotNull;
import net.jcip.annotations.Immutable;
import net.jcip.annotations.ThreadSafe;

import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.ECPublicKey;
import java.util.Objects;

/**
* {@link ES384JwtTokenValidator} validates an <a href="https://jwt.io/">JWT</a> token in ES384 JWS form.
*
* It validates the access token by verifying the integrity of the header and payload to ensure that they have not been
* altered by using token's signature section.
*/
@Immutable
@ThreadSafe
public class ES384JwtTokenValidator implements AccessTokenValidator {

private static final Logger LOG = LoggerFactory.getLogger(ES384JwtTokenValidator.class);

private final String jwksUrl;

/**
* Constructs a new {@link ES384JwtTokenValidator} that verfies the signed JWT token with the JWK keys stored at a
* specified URL.
*
* @param jwksUrl The provided JWKS URL that, on GET, returns a json object such as
* <pre>
* {@code
* {
* "keys": [
* {
* "kty": "EC",
* "use": "sig",
* "kid": "eTERknhur9q8gisdaf_dfrqrgdfsg",
* "alg": "ES384",
* "crv": "P-384",
* "x": "sdfrgHGYF...",
* "y": "sdfuUIG&8..."
* }
* ]
* }
* }
* </pre>
*
* @throws NullPointerException if {@code jwksUrl} is {@code null}
*/
public ES384JwtTokenValidator(final String jwksUrl) {
this.jwksUrl = Objects.requireNonNull(jwksUrl);
}

@Override
public boolean validate(@NotNull final String accessToken) {
final JwkProvider jwkProvider = getJwkProvider(getJwksUrl());
final Jwk jwk = getJwk(jwkProvider, accessToken);
final Algorithm algorithm = getVerificationAlgorithm(jwk);
final Verification verifier = JWT.require(algorithm);

verifier.build().verify(accessToken);

return true;
}

/**
* Returns a one-time built instance of {@link JwkProvider} to save performance.
*
* @param jwksUrl The JWKS URL that, on GET, returns a json object such as
* <pre>
* {@code
* {
* "keys": [
* {
* "kty": "EC",
* "use": "sig",
* "kid": "eTERknhur9q8gisdaf_dfrqrgdfsg",
* "alg": "ES384",
* "crv": "P-384",
* "x": "sdfrgHGYF...",
* "y": "sdfuUIG&8..."
* }
* ]
* }
* }
* </pre>
*
* @return a new instance
*
* @throws IllegalStateException if the {@code jwksUrl} is an invalid URL
*/
@NotNull
private static JwkProvider getJwkProvider(@NotNull final String jwksUrl) {
try {
return new JwkProviderBuilder(new URL(jwksUrl)).build();
} catch (final MalformedURLException exception) {
final String message = String.format("Invalid JWKS URL: '%s'", jwksUrl);
LOG.error(message, exception);
throw new IllegalStateException(message, exception);
}
}

/**
* Returns a JWK key set that will be used to verify a given JWT access token.
*
* @param jwkProvider An object that contains the JWK; cannot be {@code null}
* @param accessToken A "key" that indexes the JWK from the {@code jwkProvider}; cannot be {@code null}
*
* @return a JWK key set
*
* @throws IllegalStateException if no JWK set associated with the provided token is found from {@code jwkProvider}
*/
private static Jwk getJwk(@NotNull final JwkProvider jwkProvider, @NotNull final String accessToken) {
try {
return jwkProvider.get(JWT.decode(accessToken).getKeyId());
} catch (final JwkException exception) {
final String message = "The key ID in the access token does not match any JWK";
LOG.error(message, exception);
throw new IllegalStateException(message, exception);
}
}

/**
* Returns the verifying algorithm associated with a specified JWK.
*
* @param jwk The JWK contains the public key for decrypting the token signature, cannot be {@code null}
*
* @return a decrypting algorithm with public key and without private key enclosed
*
* @throws IllegalStateException if public key cannot be retrieved from the JWK
*/
private static Algorithm getVerificationAlgorithm(@NotNull final Jwk jwk) {
try {
return Algorithm.ECDSA384((ECPublicKey) jwk.getPublicKey(), null);
} catch (final InvalidPublicKeyException exception) {
final String message = "The public key cannot be build from JWK";
LOG.error(message, exception);
throw new IllegalStateException(message, exception);
}
}

@NotNull
private String getJwksUrl() {
return jwksUrl;
}
}
```

[BinderFactory]: https://github.com/QubitPi/jersey-ws-template/blob/master/src/main/java/com/qubitpi/ws/jersey/template/application/BinderFactory.java

[OAuth 2 access token]: https://www.oauth.com/oauth2-servers/access-tokens/
10 changes: 1 addition & 9 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<version.slf4j>1.7.25</version.slf4j>
<version.logback>1.2.3</version.logback>
<version.jackson>2.13.3</version.jackson>
<version.owner>1.1.2</version.owner>
<version.owner>1.0.4</version.owner>
<version.servlet>6.0.0</version.servlet>
<version.jersey>3.1.1</version.jersey>
<version.groovy>4.0.6</version.groovy>
Expand Down Expand Up @@ -509,12 +509,4 @@
</build>
</profile>
</profiles>

<repositories>
<repository>
<id>paion</id>
<name>Paion Data Official Release Repository</name>
<url>https://nexus.paion-data.dev/repository/maven-oss</url>
</repository>
</repositories>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.qubitpi.ws.jersey.template.application;

import com.qubitpi.ws.jersey.template.web.filters.oauth.AccessTokenValidator;

import org.glassfish.hk2.utilities.Binder;
import org.glassfish.hk2.utilities.binding.AbstractBinder;

Expand Down Expand Up @@ -45,7 +47,12 @@ public Binder buildBinder() {
return new AbstractBinder() {
@Override
protected void configure() {
// intentionally left blank
bind(new AccessTokenValidator() {
@Override
public boolean validate(final String accessToken) {
return true;
}
}).to(AccessTokenValidator.class);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
package com.qubitpi.ws.jersey.template.application;

import com.qubitpi.ws.jersey.template.web.filters.CorsFilter;
import com.qubitpi.ws.jersey.template.web.filters.OAuthFilter;

import org.glassfish.hk2.utilities.Binder;
import org.glassfish.jersey.media.multipart.MultiPartFeature;

import jakarta.inject.Inject;
import jakarta.ws.rs.ApplicationPath;
Expand All @@ -45,6 +45,6 @@ public ResourceConfig() {
packages(ENDPOINT_PACKAGE);
register(new CorsFilter());
register(binder);
register(MultiPartFeature.class);
register(OAuthFilter.class);
}
}
Loading

0 comments on commit 3ec689a

Please sign in to comment.