From 119a89f2ee6350939f5b7184287f6c1cf3cd860e Mon Sep 17 00:00:00 2001 From: June Date: Fri, 31 Oct 2025 23:59:38 +0100 Subject: [PATCH 1/4] have export endpoint actually return a list of valid keys Dynamically get a list of attributes to get the keys from, get their values, ensure they are in a valid format using a regex and finally return the list of keys as a JSON list. Co-authored-by: kritzl --- .../hamburg/keycloak/ssh_key/AuthHelper.java | 9 +-- .../ssh_key/SSHKeyResourceProvider.java | 59 ++++++++++++++----- .../SSHKeyResourceProviderFactory.java | 3 +- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/AuthHelper.java b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/AuthHelper.java index cf3091d..25af562 100644 --- a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/AuthHelper.java +++ b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/AuthHelper.java @@ -1,7 +1,7 @@ package de.ccc.hamburg.keycloak.ssh_key; -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.NotAuthorizedException; +import java.util.function.Function; + import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -9,7 +9,8 @@ import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; -import java.util.function.Function; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.NotAuthorizedException; public class AuthHelper { @@ -47,4 +48,4 @@ public class AuthHelper { return new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false); } -} \ No newline at end of file +} diff --git a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java index fb5c2d4..000abd2 100644 --- a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java +++ b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java @@ -1,27 +1,37 @@ package de.ccc.hamburg.keycloak.ssh_key; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Stream; import org.jboss.logging.Logger; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.MediaType; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; -import org.keycloak.services.managers.Auth; +import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.services.resource.RealmResourceProvider; +import org.keycloak.userprofile.UserProfileProvider; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; public class SSHKeyResourceProvider implements RealmResourceProvider { private static final Logger LOG = Logger.getLogger(SSHKeyResourceProvider.class); private final KeycloakSession session; + // taken from: https://github.com/nemchik/ssh-key-regex + private static final Pattern SSH_PUBLIC_KEY = Pattern.compile( + "^(?(ssh-dss AAAAB3NzaC1kc3|ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNT|ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzOD|ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1Mj|sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3})(\\s.*)?$"); + public SSHKeyResourceProvider(KeycloakSession keycloakSession) { this.session = keycloakSession; } @@ -40,26 +50,43 @@ public class SSHKeyResourceProvider implements RealmResourceProvider { @Produces(MediaType.APPLICATION_JSON) public Response exportKeys(@PathParam("group_id") String groupId) { UserProvider userProvider = session.users(); + UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); + UPConfig upconfig = profileProvider.getConfiguration(); + + Stream attributeNames = upconfig.getAttributes() + .stream() + .filter(a -> a.getGroup() != null && a.getGroup().equals("de.ccc.hamburg.keycloak.ssh_key.keys")) + .map(a -> a.getName()); try { - Auth auth = AuthHelper.getAuth(session, + AuthHelper.getAuth( + session, authResult -> authResult.getToken().getIssuedFor().equals("admin-cli")); RealmModel realm = session.getContext().getRealm(); + // TODO: add allowlist check GroupModel group = realm.getGroupById(groupId); - LOG.info(String.format("Getting Users from Group \"%s\" with ID %s", group.getName(), group.getId())); - Stream users = userProvider.getGroupMembersStream(realm, group); - users.forEach(user -> { - String sshKey = user.getAttributeStream("ssh-key-1").findFirst().get(); - LOG.info(String.format("SSH Key of %s: %s", user.getUsername(), sshKey)); - }); + List keys = users + .map(user -> { + return attributeNames + .map(attributeName -> user.getAttributeStream(attributeName).findFirst()) + .filter(attribute -> attribute.isPresent()) + .map(attribute -> attribute.get()) + .toList(); + }) + .flatMap(List::stream) + .map(key -> { + final Matcher matcher = SSH_PUBLIC_KEY.matcher(key); + return matcher.find() ? matcher.group("key") : null; + }) + .filter(Objects::nonNull) + .toList(); - - return Response.ok(Map.of("hello", auth.getUser().getUsername())).build(); + return Response.ok(Map.of("keys", keys)).build(); } catch (Exception e) { System.err.println(e); return Response.status(401, e.getMessage()).build(); diff --git a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProviderFactory.java b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProviderFactory.java index 4f615d2..d5ac583 100644 --- a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProviderFactory.java +++ b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProviderFactory.java @@ -1,11 +1,12 @@ package de.ccc.hamburg.keycloak.ssh_key; +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; + import com.google.auto.service.AutoService; -import org.keycloak.Config; @AutoService(RealmResourceProviderFactory.class) public class SSHKeyResourceProviderFactory implements RealmResourceProviderFactory { From c60c4978df04fa860e645981fbe063e6e64861d5 Mon Sep 17 00:00:00 2001 From: kritzl Date: Sat, 1 Nov 2025 01:49:54 +0100 Subject: [PATCH 2/4] fix: use list instead of consumed stream --- .../keycloak/ssh_key/SSHKeyResourceProvider.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java index 000abd2..01fab43 100644 --- a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java +++ b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java @@ -53,10 +53,11 @@ public class SSHKeyResourceProvider implements RealmResourceProvider { UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); UPConfig upconfig = profileProvider.getConfiguration(); - Stream attributeNames = upconfig.getAttributes() + List attributeNames = upconfig.getAttributes() .stream() .filter(a -> a.getGroup() != null && a.getGroup().equals("de.ccc.hamburg.keycloak.ssh_key.keys")) - .map(a -> a.getName()); + .map(a -> a.getName()) + .toList(); try { AuthHelper.getAuth( @@ -73,10 +74,11 @@ public class SSHKeyResourceProvider implements RealmResourceProvider { List keys = users .map(user -> { return attributeNames - .map(attributeName -> user.getAttributeStream(attributeName).findFirst()) - .filter(attribute -> attribute.isPresent()) - .map(attribute -> attribute.get()) - .toList(); + .stream() + .map(attributeName -> user.getAttributeStream(attributeName).findFirst()) + .filter(attribute -> attribute.isPresent()) + .map(attribute -> attribute.get()) + .toList(); }) .flatMap(List::stream) .map(key -> { From 9fe298a899ada90372a88bd3ffcc3b4088d7a45a Mon Sep 17 00:00:00 2001 From: kritzl Date: Sat, 1 Nov 2025 01:51:48 +0100 Subject: [PATCH 3/4] move auth chack to top of route handler --- .../ssh_key/SSHKeyResourceProvider.java | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java index 01fab43..7400069 100644 --- a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java +++ b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java @@ -49,50 +49,49 @@ public class SSHKeyResourceProvider implements RealmResourceProvider { @Path("export/{group_id}") @Produces(MediaType.APPLICATION_JSON) public Response exportKeys(@PathParam("group_id") String groupId) { + try { + AuthHelper.getAuth( + session, + authResult -> authResult.getToken().getIssuedFor().equals("admin-cli")); + } catch (Exception e) { + System.err.println(e); + return Response.status(401, e.getMessage()).build(); + } + UserProvider userProvider = session.users(); UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); UPConfig upconfig = profileProvider.getConfiguration(); - List attributeNames = upconfig.getAttributes() .stream() .filter(a -> a.getGroup() != null && a.getGroup().equals("de.ccc.hamburg.keycloak.ssh_key.keys")) .map(a -> a.getName()) .toList(); - try { - AuthHelper.getAuth( - session, - authResult -> authResult.getToken().getIssuedFor().equals("admin-cli")); + RealmModel realm = session.getContext().getRealm(); - RealmModel realm = session.getContext().getRealm(); + // TODO: add allowlist check + GroupModel group = realm.getGroupById(groupId); - // TODO: add allowlist check - GroupModel group = realm.getGroupById(groupId); + Stream users = userProvider.getGroupMembersStream(realm, group); - Stream users = userProvider.getGroupMembersStream(realm, group); + List keys = users + .map(user -> { + return attributeNames + .stream() + .map(attributeName -> user.getAttributeStream(attributeName).findFirst()) + .filter(attribute -> attribute.isPresent()) + .map(attribute -> attribute.get()) + .toList(); + }) + .flatMap(List::stream) + .map(key -> { + final Matcher matcher = SSH_PUBLIC_KEY.matcher(key); + return matcher.find() ? matcher.group("key") : null; + }) + .filter(Objects::nonNull) + .toList(); - List keys = users - .map(user -> { - return attributeNames - .stream() - .map(attributeName -> user.getAttributeStream(attributeName).findFirst()) - .filter(attribute -> attribute.isPresent()) - .map(attribute -> attribute.get()) - .toList(); - }) - .flatMap(List::stream) - .map(key -> { - final Matcher matcher = SSH_PUBLIC_KEY.matcher(key); - return matcher.find() ? matcher.group("key") : null; - }) - .filter(Objects::nonNull) - .toList(); - - return Response.ok(Map.of("keys", keys)).build(); - } catch (Exception e) { - System.err.println(e); - return Response.status(401, e.getMessage()).build(); - } + return Response.ok(Map.of("keys", keys)).build(); } From 8d190c29700794371ce748fd886b95a368e5b0b4 Mon Sep 17 00:00:00 2001 From: kritzl Date: Sat, 1 Nov 2025 02:05:53 +0100 Subject: [PATCH 4/4] simplify auth validation Co-authored-by: June --- .../hamburg/keycloak/ssh_key/AuthHelper.java | 51 ------------------- .../ssh_key/SSHKeyResourceProvider.java | 25 +++++++-- 2 files changed, 22 insertions(+), 54 deletions(-) delete mode 100644 ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/AuthHelper.java diff --git a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/AuthHelper.java b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/AuthHelper.java deleted file mode 100644 index 25af562..0000000 --- a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/AuthHelper.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.ccc.hamburg.keycloak.ssh_key; - -import java.util.function.Function; - -import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.services.managers.AppAuthManager; -import org.keycloak.services.managers.Auth; -import org.keycloak.services.managers.AuthenticationManager; - -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.NotAuthorizedException; - -public class AuthHelper { - - public static AuthenticationManager.AuthResult getAuthResult(KeycloakSession session, Function authFn) { - AuthenticationManager.AuthResult auth = new AppAuthManager.BearerTokenAuthenticator(session).authenticate(); - - if (auth == null) { - throw new NotAuthorizedException("Bearer"); - } else if (!authFn.apply(auth)) { - throw new ForbiddenException(); - } - return auth; - } - - public static Auth getAuth(KeycloakSession session, Function authFn) { - return getAuth(session, getAuthResult(session, authFn)); - } - - public static Auth getAuth(KeycloakSession session, String clientId, Function authFn) { - return getAuth(session, getAuthResult(session, authFn), clientId); - } - - public static Auth getAuth(KeycloakSession session, AuthenticationManager.AuthResult authResult) { - return getAuth(session, authResult, null); - } - - public static Auth getAuth(KeycloakSession session, AuthenticationManager.AuthResult authResult, String clientId) { - RealmModel realm = session.getContext().getRealm(); - ClientModel client; - if (clientId == null) { - client = authResult.getClient(); - } else { - client = realm.getClientByClientId(clientId); - } - return new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false); - } - -} diff --git a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java index 7400069..fd91f34 100644 --- a/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java +++ b/ssh-key-provider/src/main/java/de/ccc/hamburg/keycloak/ssh_key/SSHKeyResourceProvider.java @@ -8,16 +8,23 @@ import java.util.regex.Pattern; import java.util.stream.Stream; import org.jboss.logging.Logger; +import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.representations.userprofile.config.UPConfig; +import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.AppAuthManager.BearerTokenAuthenticator; +import org.keycloak.services.managers.Auth; +import org.keycloak.services.managers.AuthenticationManager.AuthResult; import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.userprofile.UserProfileProvider; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; @@ -50,9 +57,7 @@ public class SSHKeyResourceProvider implements RealmResourceProvider { @Produces(MediaType.APPLICATION_JSON) public Response exportKeys(@PathParam("group_id") String groupId) { try { - AuthHelper.getAuth( - session, - authResult -> authResult.getToken().getIssuedFor().equals("admin-cli")); + SSHKeyResourceProvider.getAuth(session); } catch (Exception e) { System.err.println(e); return Response.status(401, e.getMessage()).build(); @@ -95,4 +100,18 @@ public class SSHKeyResourceProvider implements RealmResourceProvider { } + private static Auth getAuth(KeycloakSession session) { + AuthResult auth = new AppAuthManager.BearerTokenAuthenticator(session).authenticate(); + + if (auth == null) { + throw new NotAuthorizedException("Bearer"); + } else if (!auth.getToken().getIssuedFor().equals("admin-cli")) { + throw new ForbiddenException(); + } + + RealmModel realm = session.getContext().getRealm(); + ClientModel client = auth.getClient(); + return new Auth(realm, auth.getToken(), auth.getUser(), client, auth.getSession(), false); + } + }