Skip to content

Commit

Permalink
Protect custom resource with custom filter
Browse files Browse the repository at this point in the history
  • Loading branch information
sonOfRa committed Jul 25, 2023
1 parent d074227 commit 9d57de8
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.github.thomasdarimont.keycloak.custom.endpoints.account.AcmeAccountResource;
import com.github.thomasdarimont.keycloak.custom.endpoints.applications.ApplicationsInfoResource;
import com.github.thomasdarimont.keycloak.custom.endpoints.credentials.UserCredentialsInfoResource;
import com.github.thomasdarimont.keycloak.custom.endpoints.filter.ProtectedResource;
import com.github.thomasdarimont.keycloak.custom.endpoints.offline.OfflineSessionPropagationResource;
import com.github.thomasdarimont.keycloak.custom.endpoints.profile.UserProfileResource;
import com.github.thomasdarimont.keycloak.custom.endpoints.settings.UserSettingsResource;
Expand Down Expand Up @@ -86,4 +87,9 @@ public AcmeAccountResource account() {
public OfflineSessionPropagationResource sessionPropagation() {
return resourceContext.initResource(new OfflineSessionPropagationResource(session, token));
}

@Path("protected")
public ProtectedResource protectedResource() {
return resourceContext.initResource(new ProtectedResource(session));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.github.thomasdarimont.keycloak.custom.endpoints.filter;

import com.github.thomasdarimont.keycloak.custom.support.KeycloakSessionLookup;
import jakarta.annotation.Priority;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.representations.AccessToken;

import java.io.IOException;
import java.util.regex.Pattern;

import static com.github.thomasdarimont.keycloak.custom.endpoints.filter.TokenFilter.ACCESS_TOKEN_SESSION_KEY;

@AzpFilterBinding
@JBossLog
@Priority(1)
@Provider
public class AzpFilter implements ContainerRequestFilter {

private final Pattern clientPattern = Pattern.compile("acme.*");

@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
AccessToken accessToken = KeycloakSessionLookup.currentSession().getAttribute(ACCESS_TOKEN_SESSION_KEY, AccessToken.class);

if (accessToken == null) {
log.error("AzpFilter must run after TokenFilter");
throw new InternalServerErrorException();
}

if (!clientPattern.matcher(accessToken.getIssuedFor()).matches()) {
throw new ForbiddenException("This resource is only accessible for acme clients");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.github.thomasdarimont.keycloak.custom.endpoints.filter;

import jakarta.ws.rs.NameBinding;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
public @interface AzpFilterBinding {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.github.thomasdarimont.keycloak.custom.endpoints.filter;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import org.keycloak.models.KeycloakSession;

import java.util.Map;

import static com.github.thomasdarimont.keycloak.custom.endpoints.filter.TokenFilter.ACCESS_TOKEN_SESSION_KEY;

/**
* Example for a resource protected by a request filter.
* Methods annotated with @AuthFilterBinding execute the filter before executing the method
*/
public class ProtectedResource {

private final KeycloakSession keycloakSession;

public ProtectedResource(KeycloakSession keycloakSession) {
this.keycloakSession = keycloakSession;
}

/*
* Run a token check
*/
@GET
@Path("/protected")
@TokenFilterBinding
@Produces("application/json")
public Response protectedResource() {
return Response.ok(Map.of("secret", keycloakSession.getAttribute(ACCESS_TOKEN_SESSION_KEY))).build();
}

/*
* Run a token check AND an azp check
*/
@GET
@Path("/veryprotected")
@AzpFilterBinding
@TokenFilterBinding
@Produces("application/json")
public Response veryProtectedResource() {
return Response.ok(Map.of("supersecret", "The magic words are squeamish ossifrage",
"secret", keycloakSession.getAttribute(ACCESS_TOKEN_SESSION_KEY))).build();
}

/*
* Run no checks at all
*/
@GET
@Path("/public")
@Produces("application/json")
public Response openResource() {
return Response.ok(Map.of("secret", "no secrets here")).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.github.thomasdarimont.keycloak.custom.endpoints.filter;

import com.github.thomasdarimont.keycloak.custom.support.KeycloakSessionLookup;
import jakarta.annotation.Priority;
import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;
import org.keycloak.authorization.util.Tokens;
import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.AccessToken;

import java.io.IOException;

/**
* Custom request filter for token handling. Executed if a request method is annotated with @AuthFilterBinding
*/
@Priority(0)
@Provider
@TokenFilterBinding
public class TokenFilter implements ContainerRequestFilter {

public static final String ACCESS_TOKEN_SESSION_KEY = "acme-access-token";

@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Important: The filter is instantiated only once. KeycloakSession must be local, not a field
KeycloakSession keycloakSession = KeycloakSessionLookup.currentSession();

AccessToken accessToken = Tokens.getAccessToken(keycloakSession);

if (accessToken == null) {
throw new NotAuthorizedException("Invalid or missing token");
}

/*
* Store the access token in the keycloak session, so that it can be used by the actual
* resource without having to repeat the validation step
*/
keycloakSession.setAttribute(ACCESS_TOKEN_SESSION_KEY, accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.github.thomasdarimont.keycloak.custom.endpoints.filter;

import jakarta.ws.rs.NameBinding;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenFilterBinding {
}

0 comments on commit 9d57de8

Please sign in to comment.