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

Authorization feature and bearer #43

Merged
merged 5 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions plume-web-jersey/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,103 @@ public void waitAsync(@Suspended final AsyncResponse asyncResponse) {
}
```

Requests authorization
----------------------
Multiple authorization provider are implemented.
For all provider, the HTTP header `Authorization` is used to fetch the authorization value.

### Basic authorization
This feature is provided by the `BasicAuthenticator` class.
Sample usage:
```java
@Path("/example")
@Tag(name = "example", description = "Manage exemple web-services")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
// Since we are performing authorization in the resource, we can mark this API as public
@PublicApi
@Singleton
public class ExampleWs {
private final BasicAuthenticator<String> basicAuthenticator;

@Inject
public ExampleWs() {
this.basicAuthenticator = BasicAuthenticator.fromSingleCredentials(
"my-username",
"my-password",
// Message displayed to the user trying to access the API with a browser
"My protected API"
);
}

@GET
@Path("/test")
@Operation(description = "Example web-service")
public Test test(@Context ContainerRequestContext requestContext) {
basicAuthenticator.requireAuthentication(requestContext);

return new Test("Hello world");
}
}
```

One sample valid request: `curl -u 'my-username:my-password' 'http://localhost:8080/api/example/test'`

### API Key authorization
This feature is provided by the `BearerAuthenticator` class.
Sample usage:
```java
@Path("/example")
@Tag(name = "example", description = "Manage exemple web-services")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
// Since we are performing authorization in the resource, we can mark this API as public
@PublicApi
@Singleton
public class ExampleWs {
private final BearerAuthenticator bearerAuthenticator;

@Inject
public ExampleWs() {
this.bearerAuthenticator = new BearerAuthenticator("my-bearer-token");
}

@GET
@Path("/test")
@Operation(description = "Example web-service")
public Test test(@Context ContainerRequestContext requestContext) {
bearerAuthenticator.verifyAuthentication(requestContext);

return new Test("Hello world");
}
}
```

One sample valid request: `curl -H 'Authorization: Bearer my-bearer-token' 'http://localhost:8080/api/example/test'`

### Resource authorization based on annotation
This feature is provided `AuthorizationSecurityFeature` and works with any authentication system.
For example using bearer auth:
- In the `JerseyConfigProvider` file, declare the feature with the annotation used for resource identification: `config.register(new BearerAuthenticator("my-bearer-token").toAuthorizationFeature(BearerRestricted.class));`
- In the `JerseyConfigProvider` file, register if needed the annotation used in the `RequireExplicitAccessControlFeature`: `config.register(RequireExplicitAccessControlFeature.accessControlAnnotations(PublicApi.class, BearerRestricted.class));`
- Use the annotation in a resource definition:
```java
@Path("/example")
@Tag(name = "example", description = "Manage exemple web-services")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
// The new annotation that will ensure the authorization process before granting access
@BearerRestricted
@Singleton
public class ExampleWs {
@GET
@Path("/test")
@Operation(description = "Example web-service")
public Test test() {
return new Test("Hello world");
}
}
```

### Permissions based authorization
TODO to details
11 changes: 11 additions & 0 deletions plume-web-jersey/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework</groupId>
<artifactId>jersey-test-framework-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.coreoz.plume.jersey.security;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.DynamicFeature;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.FeatureContext;
import lombok.extern.slf4j.Slf4j;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;

/**
* Provide a general way of implementing a Jersey request authentication filter
* that will be active on a method or a class depending on the presence of an annotation.<br>
* <br>
* The priority will be:<br>
* 1. If the annotation is present on the resource method, then this annotation will be used when calling the {@link AuthorizationVerifier}<br>
* 2. Else if the annotation is present on the resource class, then this annotation will be used when calling the {@link AuthorizationVerifier}<br>
* 3. Else the authentication filter will not be active for the whole resource
* @param <A> The annotation type that will be looked to on the resource to enable the authentication filter
* @see com.coreoz.plume.jersey.security.basic.BasicAuthenticator
* @see com.coreoz.plume.jersey.security.bearer.BearerAuthenticator
* @see com.coreoz.plume.jersey.security.permission.PermissionFeature
*/
@Slf4j
public class AuthorizationSecurityFeature<A extends Annotation> implements DynamicFeature {
private final Class<A> annotation;
private final AuthorizationVerifier<A> authorizationVerifier;

/**
* @param annotation The annotation type that will hold the authentication
* @param authorizationVerifier The function that will verify that the request is authorized, else it should throw {@link jakarta.ws.rs.ForbiddenException} or {@link jakarta.ws.rs.ClientErrorException}
*/
public AuthorizationSecurityFeature(Class<A> annotation, AuthorizationVerifier<A> authorizationVerifier) {
this.annotation = annotation;
this.authorizationVerifier = authorizationVerifier;
}

@Override
public void configure(ResourceInfo methodResourceInfo, FeatureContext methodResourceContext) {
A methodAnnotation = getAnnotation(methodResourceInfo.getResourceMethod());
if (methodAnnotation != null) {
methodResourceContext.register(new AuthorizationFilter(methodAnnotation));
} else {
A classAnnotation = getAnnotation(methodResourceInfo.getResourceClass());
if (classAnnotation != null) {
methodResourceContext.register(new AuthorizationFilter(classAnnotation));
}
}
}

private A getAnnotation(AnnotatedElement annotatedElement) {
return annotatedElement.getAnnotation(annotation);
}

private class AuthorizationFilter implements ContainerRequestFilter {
private final A authorizationAnnotation;

public AuthorizationFilter(A authorizationAnnotation) {
this.authorizationAnnotation = authorizationAnnotation;
}

@Override
public void filter(ContainerRequestContext requestContext) {
authorizationVerifier.verifyAuthentication(authorizationAnnotation, requestContext);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.coreoz.plume.jersey.security;

import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.container.ContainerRequestContext;

import javax.annotation.Nonnull;
import java.lang.annotation.Annotation;

/**
* The function that will verify that an incoming request is authorized or not<br>
* <br>
* This function should be used within the {@link AuthorizationSecurityFeature}.
* @param <A> The annotation type used to identify that the verifier should be applied on a resource method/class
*/
@FunctionalInterface
public interface AuthorizationVerifier<A extends Annotation> {
/**
* Verify that an incoming request is authorized
*
* @param authorizationAnnotation The annotation type used to identify that the verifier should be applied on a resource method/class
* @param requestContext The request context that should be used to verify the authorization
* @throws ForbiddenException If the user cannot access the resource
* @throws ClientErrorException If there is another error about the request authorization
*/
void verifyAuthentication(@Nonnull A authorizationAnnotation, @Nonnull ContainerRequestContext requestContext) throws ForbiddenException, ClientErrorException;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package com.coreoz.plume.jersey.security.basic;

import lombok.extern.slf4j.Slf4j;

import com.coreoz.plume.jersey.security.AuthorizationSecurityFeature;
import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import lombok.extern.slf4j.Slf4j;

import java.lang.annotation.Annotation;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.function.Function;

Expand Down Expand Up @@ -42,15 +44,14 @@ public BasicAuthenticator(Function<Credentials, U> authenticator, String realm)

/**
* Create a new {@link BasicAuthenticator} for a unique credentials.
* This should be used to protect a non strategic resource since users
* will all share the same user name and password.
* This should be used to protect a non-strategic resource since users
* will all share the same username and password.
*/
public static BasicAuthenticator<String> fromSingleCredentials(String singleUsername,
String password, String realm) {
return new BasicAuthenticator<>(
credentials ->
singleUsername.equals(credentials.getUsername())
&& password.equals(credentials.getPassword()) ?
credentials -> MessageDigest.isEqual(singleUsername.getBytes(), credentials.getUsername().getBytes())
&& MessageDigest.isEqual(password.getBytes(),credentials.getPassword().getBytes()) ?
lucas-amiaud marked this conversation as resolved.
Show resolved Hide resolved
"Basic user"
: null,
realm
Expand All @@ -59,6 +60,20 @@ public static BasicAuthenticator<String> fromSingleCredentials(String singleUser

// API

/**
* Provide a {@link AuthorizationSecurityFeature} from the bearer basic that can be used in Jersey
* to provide authentication on annotated resources.
* @param basicAnnotation The annotation that will be used to identify resources that must be authorized. For example {@link BasicRestricted} can be used if it is not already used in the project for another authorization system
* @return The corresponding {@link AuthorizationSecurityFeature}
* @param <A> The annotation type used to identify required basic authenticated resources
*/
public <A extends Annotation> AuthorizationSecurityFeature<A> toAuthorizationFeature(Class<A> basicAnnotation) {
return new AuthorizationSecurityFeature<>(
basicAnnotation,
(authorizationAnnotation, requestContext) -> requireAuthentication(requestContext)
);
}

/**
* Check that users accessing the resource are well authenticated.
* A {@link ClientErrorException} will be raised if a user is not authenticated.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.coreoz.plume.jersey.security.basic;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* An annotation that can be used to set up an {@link com.coreoz.plume.jersey.security.AuthorizationSecurityFeature}
* with a {@link BasicAuthenticator}
*/
@Documented
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface BasicRestricted {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.coreoz.plume.jersey.security.bearer;

import com.coreoz.plume.jersey.security.AuthorizationSecurityFeature;
import com.google.common.net.HttpHeaders;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.container.ContainerRequestContext;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;

import java.lang.annotation.Annotation;
import java.security.MessageDigest;

/**
* Provide a generic resource authenticator for API-key based resource authorization
*/
@Slf4j
public class BearerAuthenticator {
private static final String BEARER_PREFIX = "Bearer ";

private final String authenticationSecretHeader;

public BearerAuthenticator(String bearerToken) {
this.authenticationSecretHeader = BEARER_PREFIX + bearerToken;
}

/**
* Provide a {@link AuthorizationSecurityFeature} from the bearer authenticator that can be used in Jersey
* to provide authentication on annotated resources.
* @param bearerAnnotation The annotation that will be used to identify resources that must be authorized. For example {@link BearerRestricted} can be used if it is not already used in the project for another authorization system
* @return The corresponding {@link AuthorizationSecurityFeature}
* @param <A> The annotation type used to identify required bearer authenticated resources
*/
public <A extends Annotation> AuthorizationSecurityFeature<A> toAuthorizationFeature(Class<A> bearerAnnotation) {
return new AuthorizationSecurityFeature<>(
bearerAnnotation,
(authorizationAnnotation, requestContext) -> verifyAuthentication(requestContext)
);
}

public void verifyAuthentication(@NotNull ContainerRequestContext requestContext) {
String bearer = parseBearerHeader(requestContext.getHeaderString(HttpHeaders.AUTHORIZATION));

if (bearer == null || !MessageDigest.isEqual(authenticationSecretHeader.getBytes(), bearer.getBytes())) {
logger.warn("Invalid bearer header: {}", bearer);
throw new ForbiddenException();
}
}

public static String parseBearerHeader(String authorizationHeader) {
if(authorizationHeader == null) {
logger.debug("Missing Authorization header");
return null;
}

if(!authorizationHeader.startsWith(BEARER_PREFIX)) {
logger.debug("Bearer Authorization header must starts with '{}'", BEARER_PREFIX);
return null;
}

return authorizationHeader;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.coreoz.plume.jersey.security.bearer;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* An annotation that can be used to set up an {@link com.coreoz.plume.jersey.security.AuthorizationSecurityFeature}
* with a {@link BearerAuthenticator}
*/
@Documented
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface BearerRestricted {
}
Loading
Loading