简体   繁体   中英

Bearer only authentication in Wildfly without using Keycloak

I want to do my own implementation of Bearer-only authentication in Wildfly. In essence, I will do the following steps:

  1. When I receive a request, I will check if it has an Authorization header.

  2. I obtain the token and check against a database (in this case I will be using Redis) for the validity of it.

  3. I obtain the role for that user from the database.

  4. I want to be able to use the @RolesAllowed annotation on my rest services.

How do I go about it? How do I need to modify the Wildfly configuration files? What interfaces do I need to implement? How can I pass the role of the user to the security context so that Wildfly does the @RolesAllowed check for me?

If answering, consider that I am an experienced Java Programmer, but new to Wildfly, so you can skip details on programming logic but not on Wildfly configuration. Also in your answer don't worry on how the token got to Redis in the first place, or how the client obtained it.

EDIT

This is what I have done, but with no luck yet. I have implemented an AuthenticationFilter that implements ContainerRequestFilter . (I am including below only the main filter function that I have implemented. Note that there are some helper functions that get the roles from the database that are not included). Even when, at the end of the function I set the security context of the request context with the user profile (which contains the role), I cannot get to work the @RolesAllowed annotations on my JAX-RS rest services. Any pointers on what should I do?

Note: I have not modified any Wildfly configuration files or the web.xml file. I know that the filter is being called for every request because I am able to LOG messages from it on every request.

/** 
 * (non-Javadoc)
 * @see javax.ws.rs.container.ContainerRequestFilter#filter(javax.ws.rs.container.ContainerRequestContext)
 */
@Override
public void filter(ContainerRequestContext requestContext) {

    //1. Read the JSON web token from the header
    String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
    if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
        requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
        return;
    }

    String token = authorizationHeader.substring("Bearer".length()).trim();

    try{
        //Note that if the token is not in the database,
        //an exception will be thrown and we abort.

        UserProfile userProfile = this.getUserProfile(token);

        if (null == userProfile){
            userProfile = this.decodeToken(token);
        }


        if (null == userProfile){
            throw new Exception();
        }


        String role = userProfile.getUserRole();
        if (null == role){
            role = this.getRoleFromMod(userProfile);
            if (null == role){
                role = RoleType.READ_ONLY;
            }
            userProfile.setUserRole(role);
            this.updateUserProfileForToken(token, userProfile);

        }

        userProfile.setUserRole(role);

        //5. Create a security context class that implements the crazy interface 
        //and set it here.
        requestContext.setSecurityContext(new ModSecurityContext(userProfile));

    }
    catch(Exception e){
        requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
    }
}

Yeah I am not sure how it would work in an EE environment, even making the resource class an stateless bean. The @RolesAllowed annotation is meant to be used for ejbs. In which case the principal is retrieved from the servlet request (I believe). What I would do is just implements your own authorization filter that looks up the annotation and checks against the principal in the security context.

You can see how Jersey implements it . Nothing is really Jersey specific about it except the AnnotatedMethod class. For that you can just do some reflection with java.lang.reflect.Method (resourceInfo.getResourceMethod()) instead. Other than that, you can pretty much copy the code as is. Once you're done, just register the RolesAllowedDynamicFeature with the application. Or just annotate it with @Provider to be scanned for.

You will also need to make sure your authentication filter is annotated with @Priority(Priorities.AUTHENTICATION) so that it is called before the authorization filter, which is annotated with @Priority(Priorities.AUTHORIZATION) .


UPDATE

Here is a refactor of the the code I linked to, so It doesn't use an Jersey specific classes. The AnnotatedMethod is just changed to Method .

@Provider
public class RolesAllowedFeature implements DynamicFeature {

    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext configuration) {
        Method resourceMethod = resourceInfo.getResourceMethod();

        if (resourceMethod.isAnnotationPresent(DenyAll.class)) {
            configuration.register(new RolesAllowedRequestFilter());
            return;
        }

        RolesAllowed ra = resourceMethod.getAnnotation(RolesAllowed.class);
        if (ra != null) {
            configuration.register(new RolesAllowedRequestFilter(ra.value()));
            return;
        }

        if (resourceMethod.isAnnotationPresent(PermitAll.class)) {
            return;
        }

        ra = resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
        if (ra != null) {
             configuration.register(new RolesAllowedRequestFilter(ra.value()));
        }
    }

    @Priority(Priorities.AUTHORIZATION) // authorization filter - should go after any authentication filters
    private static class RolesAllowedRequestFilter implements ContainerRequestFilter {

        private final boolean denyAll;
        private final String[] rolesAllowed;

        RolesAllowedRequestFilter() {
            this.denyAll = true;
            this.rolesAllowed = null;
        }

        RolesAllowedRequestFilter(final String[] rolesAllowed) {
            this.denyAll = false;
            this.rolesAllowed = (rolesAllowed != null) ? rolesAllowed : new String[]{};
        }

        @Override
        public void filter(final ContainerRequestContext requestContext) throws IOException {
            if (!denyAll) {
                if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
                    throw new ForbiddenException("Not Authorized");
                }

                for (final String role : rolesAllowed) {
                    if (requestContext.getSecurityContext().isUserInRole(role)) {
                        return;
                    }
                }
            }

            throw new ForbiddenException("Not Authorized");
        }

        private static boolean isAuthenticated(final ContainerRequestContext requestContext) {
            return requestContext.getSecurityContext().getUserPrincipal() != null;
        }
    }
}

First let me explain a bit about how the DynamicFeature works. For that let's first change the context of discussion to your current implementation of your AuthenticationFilter .

Right now it is a filter that is processed for every request. But let's say we introduced a custom @Authenticated annotation

@Target({METHOD, TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Authenticated{}

We could use this annotation to annotate different methods and classes. To make it so that only the methods and classes annotated are filtered by the filter, we can introduce a DynamicFeature that checks for the annotation, then only register the filter when the annotation is found. For example

@Provider
public class AuthenticationDynamicFeature implements DynamicFeature {

    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext configuration) {
        if (resourceInfo.getResourceMethod().isAnnotationPresent(Authenticated.class)) {
            configuration.register(new AuthenticationFilter());
            return;
        }

        if (resourceInfo.getResourceClass().isAnnotationPresent(Authenticated.class)) {
            configuration.register(new AuthenticationFilter());
        }
    } 
}

Once we register this AuthenticationDynamicFeature class, it will make it so that only methods and classes annotated with @Authenticated will be filtered.

Alternatively, this can even be done within the filter. We can get a reference to the ResourceInfo from within the AuthenticationFilter . For example check for the annotation, if is not there, then move on.

@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext context) throws IOException {

        boolean hasAnnotation = false;
        if (resourceInfo.getResourceMethod().isAnnotationPresent(Authenticated.class)
                || resourceInfo.getResourceClass().isAnnotationPresent(Authenticated.class)) {
            hasAnnotation = true;
        }
        if (!hasAnnotation) return;

        // process authentication is annotation is present

This way we could completely forget about the DynamicFeature . It's better to just use the DynamicFeature , I was just giving an example for demonstration purposes.

But that being said, if we look at the first block of code with the RolesAllowedDynamicFeature , you can better understand what is going on. It only registers the filter for methods and classes annotated with @RolesAllowed and @DenyAll . You could even refactor it to have all the annotation logic in the filter instead of the feature. You only have the filter. Just like I did with the AuthenticationFilter example above. Again this would be just for example purposes.

Now as far as the registration of the DynamicFeature , it works the same way as registering any other resource class or provider class (eg your authentication filter). So however you register those, just register the RolesAllowedDynamicFeature the same way. There is scanning, where @Path and @Provider annotations are scanned for. If this is what you are current using, then just annotating the feature class with @Provider should register it. For example just having an empty Application subclass will cause scanning to happen

@ApplicationPath("/api")
public class RestApplication extends Application {}

Then there is explicit registration in your Application subclass. For example

@ApplicationPath("/api")
public class RestApplication extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<>();
        classes.add(AuthenticationFilter.class);
        classes.add(RolesAllowedFeature.class);
        classes.add(SomeResource.class);
        return classes;
    }
}

Note that when doing this, you disable any scanning that goes on.

So a couple other things to make sure after all the above is clear it still isn't working.

  1. Make sure your current AuthenticationFilter is annotated with @Priority(Priorities.AUTHENTICATION) . This is to ensure that your authentication filter is called before the authorization filter. This needs to happen because the authentication filter is what sets the security context, and the authorization filter checks it.

  2. Make sure you are creating the security context correctly. The authorization filter will call the SecurityContext.isUserInRole(role) passing in roles from the @RolesAllowed annotation. So you need to make sure to implements the isUserInRole correctly.

如果我有积分,我会支持上面的帖子 xD

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM