From 41943db01df35cd6fb5ebd1e9d0582aa7220943c Mon Sep 17 00:00:00 2001 From: Simon Levermann Date: Tue, 25 Jul 2023 16:00:28 +0200 Subject: [PATCH] Protect custom resource with custom filter --- .../custom/endpoints/filter/AuthFilter.java | 50 +++++++++++++++++++ .../endpoints/filter/AuthFilterBinding.java | 11 ++++ .../endpoints/filter/ProtectedResource.java | 30 +++++++++++ .../filter/ProtectedResourceProvider.java | 16 ++++++ .../ProtectedResourceProviderFactory.java | 37 ++++++++++++++ 5 files changed, 144 insertions(+) create mode 100644 keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/AuthFilter.java create mode 100644 keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/AuthFilterBinding.java create mode 100644 keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResource.java create mode 100644 keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResourceProvider.java create mode 100644 keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResourceProviderFactory.java diff --git a/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/AuthFilter.java b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/AuthFilter.java new file mode 100644 index 00000000..aaeef2e8 --- /dev/null +++ b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/AuthFilter.java @@ -0,0 +1,50 @@ +package com.github.thomasdarimont.keycloak.custom.endpoints.filter; + +import com.github.thomasdarimont.keycloak.custom.support.KeycloakSessionLookup; +import jakarta.ws.rs.ForbiddenException; +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; +import java.util.regex.Pattern; + +/** + * Custom request filter for token handling. Executed if a request method is annotated with @FilterBinding + */ +@Provider +@AuthFilterBinding +public class AuthFilter implements ContainerRequestFilter { + + private final KeycloakSession keycloakSession = KeycloakSessionLookup.currentSession(); + + private final Pattern clientPattern = Pattern.compile("acme.*"); + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + // For some reason, the Authorization header is missing from the headers stored in keycloakSession + String header = requestContext.getHeaderString("Authorization"); + String token = null; + + if (header != null) { + int index = header.indexOf("Bearer "); + if (index >= 0) { + token = header.substring(index + 7); + } + } + + AccessToken accessToken = Tokens.getAccessToken(token, keycloakSession); + + if (accessToken == null) { + throw new NotAuthorizedException("Invalid or missing token"); + } + + if (!clientPattern.matcher(accessToken.getIssuedFor()).matches()) { + throw new ForbiddenException("This resource is only accessible for acme clients"); + } + } +} diff --git a/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/AuthFilterBinding.java b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/AuthFilterBinding.java new file mode 100644 index 00000000..5fcf76da --- /dev/null +++ b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/AuthFilterBinding.java @@ -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 AuthFilterBinding { +} diff --git a/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResource.java b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResource.java new file mode 100644 index 00000000..a4ccf808 --- /dev/null +++ b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResource.java @@ -0,0 +1,30 @@ +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 java.util.Map; + +/** + * Example for a resource protected by a request filter. + * Methods annotated with @FilterBinding execute the filter before executing the method + */ +public class ProtectedResource { + + @GET + @Path("/protected") + @AuthFilterBinding + @Produces("application/json") + public Response protectedResource() { + return Response.ok(Map.of("secret", "opensesame")).build(); + } + + @GET + @Path("/protected") + @Produces("application/json") + public Response openResource() { + return Response.ok(Map.of("public", "this is not a secret")).build(); + } +} diff --git a/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResourceProvider.java b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResourceProvider.java new file mode 100644 index 00000000..a4e5be1a --- /dev/null +++ b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResourceProvider.java @@ -0,0 +1,16 @@ +package com.github.thomasdarimont.keycloak.custom.endpoints.filter; + +import org.keycloak.services.resource.RealmResourceProvider; + +public class ProtectedResourceProvider implements RealmResourceProvider { + + @Override + public Object getResource() { + return new ProtectedResource(); + } + + @Override + public void close() { + + } +} diff --git a/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResourceProviderFactory.java b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResourceProviderFactory.java new file mode 100644 index 00000000..7363fe6a --- /dev/null +++ b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/filter/ProtectedResourceProviderFactory.java @@ -0,0 +1,37 @@ +package com.github.thomasdarimont.keycloak.custom.endpoints.filter; + +import com.google.auto.service.AutoService; +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.services.resource.RealmResourceProvider; +import org.keycloak.services.resource.RealmResourceProviderFactory; + + +@AutoService(RealmResourceProviderFactory.class) +public class ProtectedResourceProviderFactory implements RealmResourceProviderFactory { + @Override + public RealmResourceProvider create(KeycloakSession session) { + return new ProtectedResourceProvider(); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "protected"; + } +}