Skip to content

Commit

Permalink
Add AuthorizationSecurityFeature, BearerAuthenticator and refactor ex…
Browse files Browse the repository at this point in the history
…isting code using this
  • Loading branch information
amanteaux committed Oct 16, 2024
1 parent 7a759dd commit f4f791f
Show file tree
Hide file tree
Showing 12 changed files with 587 additions and 80 deletions.
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> restrictedAnnotations;
private final AuthorizationVerifier<A> authorizationVerifier;

/**
* @param restrictedAnnotations 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> restrictedAnnotations, AuthorizationVerifier<A> authorizationVerifier) {
this.restrictedAnnotations = restrictedAnnotations;
this.authorizationVerifier = authorizationVerifier;
}

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

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

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,5 +1,6 @@
package com.coreoz.plume.jersey.security.basic;

import com.coreoz.plume.jersey.security.AuthorizationVerifier;
import lombok.extern.slf4j.Slf4j;

import jakarta.ws.rs.ClientErrorException;
Expand All @@ -9,7 +10,9 @@
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;

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 +45,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()) ?
"Basic user"
: null,
realm
Expand All @@ -59,6 +61,16 @@ public static BasicAuthenticator<String> fromSingleCredentials(String singleUser

// API

/**
* Provide an {@link AuthorizationVerifier} from the basic authenticator to provide annotation based request authorization using {@link com.coreoz.plume.jersey.security.AuthorizationSecurityFeature}
* @param annotation 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 basic authenticator corresponding {@link AuthorizationVerifier}
* @param <A> The annotation type
*/
public <A extends Annotation> AuthorizationVerifier<A> toAuthorizationVerifier(A annotation) {
return (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,58 @@
package com.coreoz.plume.jersey.security.bearer;

import com.coreoz.plume.jersey.security.AuthorizationVerifier;
import com.coreoz.plume.jersey.security.basic.BasicRestricted;
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 an {@link AuthorizationVerifier} from the bearer authenticator to provide annotation based request authorization using {@link com.coreoz.plume.jersey.security.AuthorizationSecurityFeature}
* @param annotation 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 basic authenticator corresponding {@link AuthorizationVerifier}
* @param <A> The annotation type
*/
public <A extends Annotation> AuthorizationVerifier<A> toAuthorizationVerifier(A annotation) {
return (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())) {
throw new ForbiddenException("Invalid bearer header: " + bearer);
}
}

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

0 comments on commit f4f791f

Please sign in to comment.