Skip to content

Commit

Permalink
Enable OAuth switch (#36)
Browse files Browse the repository at this point in the history
* Enable OAuth switch

* Add documentation

* Add documentation
  • Loading branch information
QubitPi authored Sep 11, 2023
1 parent be6223f commit 550bbad
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 20 deletions.
11 changes: 11 additions & 0 deletions checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,17 @@
<property name="tokens" value="ENUM_CONSTANT_DEF"/>
<property name="severity" value="error"/>
</module>
<module name="MissingJavadocMethod">
<property name="scope" value="anoninner"/>
<property name="allowMissingPropertyJavadoc" value="true"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/>
<property name="severity" value="error"/>
</module>
<module name="MissingJavadocType">
<property name="scope" value="anoninner"/>
<property name="tokens" value="INTERFACE_DEF, CLASS_DEF, ENUM_DEF, ANNOTATION_DEF"/>
<property name="severity" value="error"/>
</module>
<module name="NonEmptyAtclauseDescription">
<property name="javadocTokens" value=" PARAM_LITERAL, RETURN_LITERAL, THROWS_LITERAL, DEPRECATED_LITERAL"/>
<property name="severity" value="error"/>
Expand Down
39 changes: 39 additions & 0 deletions docs/docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,45 @@ API authentication is largely a solved problem and generally outside the scope o
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:

:::info

The validator can be turned on by setting **OAUTH_ENABLED** to _true_. There are 3 ways to do so (with the priority in
the following order):

1. Setting an OS environment variable using, for example, `export OAUTH_ENABLED=true`
2. Define a JVM system property by

```java
System.setProperty("OAUTH_ENABLED", "true");
```

3. Putting an **oauth.properties** file under _src/main/resources_ directory with the following content

```properties
OAUTH_ENABLED=true
```

In addition, JWKs URL needs to be set with **JWKS_URL** using the same approach above. The URL should display something
like the following

```json
{
"keys": [
{
"kty": "EC",
"use": "sig",
"kid": "eTERknhur9q8gisdaf_dfrqrgdfsg",
"alg": "ES384",
"crv": "P-384",
"x": "sdfrgHGYF...",
"y": "sdfuUIG&8..."
}
]
}
```

:::

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

To define a token validator, simply implement the **AccessTokenValidator** like so:
Expand Down
2 changes: 1 addition & 1 deletion 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.0.4</version.owner>
<version.owner>1.0.12</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
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
*/
package com.qubitpi.ws.jersey.template.application;

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

import org.aeonbits.owner.ConfigFactory;
import org.glassfish.hk2.utilities.Binder;

import jakarta.inject.Inject;
Expand All @@ -34,17 +36,30 @@
public class ResourceConfig extends org.glassfish.jersey.server.ResourceConfig {

private static final String ENDPOINT_PACKAGE = "com.qubitpi.ws.jersey.template.web.endpoints";
private static final OAuthConfig OAUTH_CONFIG = ConfigFactory.create(OAuthConfig.class);

/**
* DI Constructor.
*/
@Inject
public ResourceConfig() {
final Binder binder = new BinderFactory().buildBinder();
this(OAUTH_CONFIG.authEnabled());
}

/**
* Constructor that allows for finer dependency injection control.
*
* @param oauthEnabled Flag on whether or not to enable auth feature, mainly for differentiating dev/test and prod
*/
public ResourceConfig(final boolean oauthEnabled) {
packages(ENDPOINT_PACKAGE);
register(new CorsFilter());

register(CorsFilter.class);
if (oauthEnabled) {
register(OAuthFilter.class);
}

final Binder binder = new BinderFactory().buildBinder();
register(binder);
register(OAuthFilter.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qubitpi.ws.jersey.template.application;
package com.qubitpi.ws.jersey.template.config;

import org.aeonbits.owner.Config;

Expand All @@ -24,26 +24,31 @@
* {@link ApplicationConfig} provides an interface for retrieving configuration values, allowing for implicit type
* conversion, defaulting, and use of a runtime properties interface to override configured settings.
* <p>
* {@link ApplicationConfig} supports overriding between properties:
* {@link ApplicationConfig} tries to load the configurations from several sources in the following order:
* <ol>
* <li> It will try to load the given property from the
* <a href="https://docs.oracle.com/javase/tutorial/essential/environment/env.html">operating system's
* environment variables</a>; if an environment variable with the same name is found, its value will be
* returned. For instance, an environment variable can be set with
* {@code export EXAMPLE_CONFIG_KEY_NAME="some-value"}
* <li> Otherwise, it will try to load the given property from the
* <a href="https://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html">Java system properties
* </a>; if such property is defined, the associated value is returned. For example, a Java system property can
* be set using {@code System.setProperty("EXAMPLE_CONFIG_KEY_NAME", "some-value")}
* <li> <b>The first resource defining the property will prevail.</b>
* <li> the <a href="https://docs.oracle.com/javase/tutorial/essential/environment/env.html">
* operating system's environment variables</a>; for instance, an environment variable can be set with
* {@code export EXAMPLE_CONFIG_KEY_NAME="foo"}
* <li> the <a href="https://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html">
* Java system properties</a>; for example, a Java system property can
* be set using {@code System.setProperty("EXAMPLE_CONFIG_KEY_NAME", "foo")}
* <li> a file named <b>oauth.properties</b> placed under CLASSPATH. This file can be put under
* {@code src/main/resources} source directory with contents, for example, {@code EXAMPLE_CONFIG_KEY_NAME=foo}
* </ol>
* Note that environment config has higher priority than Java system properties. Java system properties have higher
* priority than file based configuration.
*/
@Immutable
@ThreadSafe
@Config.LoadPolicy(Config.LoadType.MERGE)
@Config.Sources({"system:env", "system:properties"})
@Config.Sources({"system:env", "system:properties", "classpath:application.properties"})
public interface ApplicationConfig extends Config {

/**
* Example config definition.
*
* @return a config value as string.
*/
@Key("EXAMPLE_CONFIG_KEY_NAME")
String exampleConfigKey();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright Jiaqi Liu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qubitpi.ws.jersey.template.config;

import org.aeonbits.owner.Config;

import net.jcip.annotations.Immutable;
import net.jcip.annotations.ThreadSafe;

/**
* {@link OAuthConfig} is responsible for all configs related to OAuth 2 aspects of the template.
* <p>
* {@link OAuthConfig} tries to load the configurations from several sources in the following order:
* <ol>
* <li> the <a href="https://docs.oracle.com/javase/tutorial/essential/environment/env.html">
* operating system's environment variables</a>; for instance, an environment variable can be set with
* {@code export OAUTH_ENABLED="true"}
* <li> the <a href="https://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html">
* Java system properties</a>; for example, a Java system property can
* be set using {@code System.setProperty("OAUTH_ENABLED", "true")}
* <li> a file named <b>oauth.properties</b> placed under CLASSPATH. This file can be put under
* {@code src/main/resources} source directory with contents, for example, {@code OAUTH_ENABLED=true}
* </ol>
* Note that environment config has higher priority than Java system properties. Java system properties have higher
* priority than file based configuration.
*/
@Immutable
@ThreadSafe
@Config.LoadPolicy(Config.LoadType.MERGE)
@Config.Sources({"system:env", "system:properties", "classpath:oauth.properties"})
public interface OAuthConfig extends Config {

/**
* Whether or not to enable {@link com.qubitpi.ws.jersey.template.web.filters.OAuthFilter} in as container request
* filter.
*
* @return {@code true} if enabling the OAuth filter or {@code false}, otherwise
*/
@Key("OAUTH_ENABLED")
boolean authEnabled();

/**
* A standard <a href="https://datatracker.ietf.org/doc/html/rfc7517">JWKS</a> URL that, on GET, returns a json
* object.
* <p>
* For example:
* <pre>
* {@code
* {
* "keys": [
* {
* "kty": "EC",
* "use": "sig",
* "kid": "eTERknhur9q8gisdaf_dfrqrgdfsg",
* "alg": "ES384",
* "crv": "P-384",
* "x": "sdfrgHGYF...",
* "y": "sdfuUIG&8..."
* }
* ]
* }
* }
* </pre>
*
* @return a valid URL
*/
@Key("JWKS_URL")
String jwksUrl();
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ public DataServlet() {
// intentionally left blank
}

/**
* A webservice sanity-check endpoint.
*
* @return 200 OK response
*/
@GET
@Path("/healthcheck")
public Response healthcheck() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;

/**
* {@link CorsFilter} prevents corss-origin request error in local dev environment.
*/
public class CorsFilter implements ContainerResponseFilter {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import org.testcontainers.spock.Testcontainers

import io.restassured.RestAssured
import io.restassured.builder.RequestSpecBuilder
import spock.lang.IgnoreIf
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Subject
Expand All @@ -41,7 +40,6 @@ import java.nio.file.Paths
* see https://www.testcontainers.org/test_framework_integration/spock/#testcontainers-class-annotation
*/
@Testcontainers
@IgnoreIf({ isLocal() })
class DataServletITSpec extends Specification {

static final int SUCCESS = 0
Expand All @@ -68,6 +66,7 @@ class DataServletITSpec extends Specification {
GenericContainer container = new GenericContainer<>(
new ImageFromDockerfile().withDockerfile(Paths.get(DOCKERFILE_ABS_PATH))
)
.withEnv("OAUTH_ENABLED", "true")
.withExposedPorts(8080)
.withImagePullPolicy(PullPolicy.defaultPolicy())

Expand All @@ -78,8 +77,10 @@ class DataServletITSpec extends Specification {
RestAssured.requestSpecification = new RequestSpecBuilder()
.addHeader(
OAuthFilter.AUTHORIZATION_HEADER,
OAuthFilter.AUTHORIZATION_SCHEME + " " + "someAccessToken")
OAuthFilter.AUTHORIZATION_SCHEME + " " + "someAccessToken"
)
.build()
System.setProperty("OAUTH_ENABLED", "true")
}

@Unroll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class JettyServerFactorySpec extends Specification {
OAuthFilter.AUTHORIZATION_HEADER,
OAuthFilter.AUTHORIZATION_SCHEME + " " + "someAccessToken")
.build()
System.setProperty("OAUTH_ENABLED", "true")
}

def "Factory produces Jsersey-Jetty applications"() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class DataServletSpec extends Specification {
OAuthFilter.AUTHORIZATION_HEADER,
OAuthFilter.AUTHORIZATION_SCHEME + " " + "someAccessToken")
.build()
System.setProperty("OAUTH_ENABLED", "true")
}

def "Healthchecking endpoints returns 200"() {
Expand Down

0 comments on commit 550bbad

Please sign in to comment.