diff --git a/README.md b/README.md
index a12447b..b2e7cb4 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,10 @@
+# Keycloak extension for API key authentication
+
+The extension contains providers for supporting API key authentication, and also other non related providers like a custom `EmailSenderProvider` (for demo purposes).
+
+It also contains a customization of the account console (the user info page provided by Keycloak) showing the API key. The account console is accessible at `/auth/realms/{realm_name}/account` and requires the user to be already authenticated.
+
+The master branch uses the new Keycloak distribution powered by Quarkus. For Legacy keycloak (versions < 17.0.0), you can switch to the `legacy` branch.
## How to run
you can run the project by running the following from a terminal: `mvn -f api-key-module package && mvn -f dashboard-service package && docker-compose up`
@@ -8,6 +15,6 @@ Note: You need to add `auth-server` to your hosts file (`/etc/hosts` for linux)
1. Navigate to localhost:8180 in a browser, you will redirected to keycloak for authentication
2. you need register a new user, after which you will be redirected to the main dashboard page which will show your API key
-3. copy the API key and use it to call the API: `curl -v -H "x-api-key: $THE_API_KEY" localhost:8200`, if you omit the API key, you will get 401 status
+3. copy the API key and use it to call the API: `curl -v -H "x-api-key: $THE_API_KEY" localhost:8280`, if you omit the API key, you will get 401 status
More explanations can be found in this blog [post](http://www.zakariaamine.com/2019-06-14/extending-keycloak)
diff --git a/api-key-module/pom.xml b/api-key-module/pom.xml
index f2905d6..8c0c46c 100644
--- a/api-key-module/pom.xml
+++ b/api-key-module/pom.xml
@@ -10,7 +10,7 @@
11
- 15.0.2
+ 19.0.1
diff --git a/api-key-module/src/main/java/com/gwidgets/providers/RegisterEventListenerProvider.java b/api-key-module/src/main/java/com/gwidgets/providers/RegisterEventListenerProvider.java
index 241639c..035693d 100644
--- a/api-key-module/src/main/java/com/gwidgets/providers/RegisterEventListenerProvider.java
+++ b/api-key-module/src/main/java/com/gwidgets/providers/RegisterEventListenerProvider.java
@@ -4,7 +4,7 @@ package com.gwidgets.providers;
import java.util.Objects;
import java.util.UUID;
import javax.persistence.EntityManager;
-import org.keycloak.common.util.RandomString;
+import org.keycloak.common.util.SecretGenerator;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
@@ -23,14 +23,14 @@ public class RegisterEventListenerProvider implements EventListenerProvider {
private KeycloakSession session;
private RealmProvider model;
//keycloak utility to generate random strings, anything can be used e.g UUID,..
- private RandomString randomString;
+ private SecretGenerator secretGenerator;
private EntityManager entityManager;
public RegisterEventListenerProvider(KeycloakSession session) {
this.session = session;
this.model = session.realms();
this.entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
- this.randomString = new RandomString(50);
+ this.secretGenerator = SecretGenerator.getInstance();
}
public void onEvent(Event event) {
@@ -55,8 +55,7 @@ public class RegisterEventListenerProvider implements EventListenerProvider {
public void addApiKeyAttribute(String userId) {
-
- String apiKey = randomString.nextString();
+ String apiKey = secretGenerator.randomString(50);
UserEntity userEntity = entityManager.find(UserEntity.class, userId);
UserAttributeEntity attributeEntity = new UserAttributeEntity();
attributeEntity.setName("api-key");
diff --git a/api-key-module/src/main/java/com/gwidgets/providers/SESEmailSenderProvider.java b/api-key-module/src/main/java/com/gwidgets/providers/SESEmailSenderProvider.java
index 0939afc..e8c0b7e 100644
--- a/api-key-module/src/main/java/com/gwidgets/providers/SESEmailSenderProvider.java
+++ b/api-key-module/src/main/java/com/gwidgets/providers/SESEmailSenderProvider.java
@@ -8,6 +8,7 @@ import com.amazonaws.services.simpleemail.model.Message;
import com.amazonaws.services.simpleemail.model.SendEmailRequest;
import java.util.Map;
import org.jboss.logging.Logger;
+import org.keycloak.email.EmailException;
import org.keycloak.email.EmailSenderProvider;
import org.keycloak.models.UserModel;
@@ -24,20 +25,25 @@ public class SESEmailSenderProvider implements EmailSenderProvider {
@Override
public void send(Map config, UserModel user, String subject, String textBody,
- String htmlBody) {
+ String htmlBody) throws EmailException {
+ this.send(config, user.getEmail(), subject, textBody, htmlBody);
+ }
- log.info("attempting to send email using aws ses for " + user.getEmail());
+ @Override
+ public void send(Map config, String address, String subject, String textBody, String htmlBody) throws EmailException {
- Message message = new Message().withSubject(new Content().withData(subject))
- .withBody(new Body().withHtml(new Content().withData(htmlBody))
- .withText(new Content().withData(textBody).withCharset("UTF-8")));
+ log.info("attempting to send email using aws ses for " + address);
- SendEmailRequest sendEmailRequest = new SendEmailRequest()
- .withSource("example<" + config.get("from") + ">")
- .withMessage(message).withDestination(new Destination().withToAddresses(user.getEmail()));
+ Message message = new Message().withSubject(new Content().withData(subject))
+ .withBody(new Body().withHtml(new Content().withData(htmlBody))
+ .withText(new Content().withData(textBody).withCharset("UTF-8")));
- sesClient.sendEmail(sendEmailRequest);
- log.info("email sent to " + user.getEmail() + " successfully");
+ SendEmailRequest sendEmailRequest = new SendEmailRequest()
+ .withSource("example<" + config.get("from") + ">")
+ .withMessage(message).withDestination(new Destination().withToAddresses(address));
+
+ sesClient.sendEmail(sendEmailRequest);
+ log.info("email sent to " + address + " successfully");
}
@Override
diff --git a/api-key-module/src/main/java/com/gwidgets/resources/ApiKeyResource.java b/api-key-module/src/main/java/com/gwidgets/resources/ApiKeyResource.java
index 30e9155..08c7d1c 100644
--- a/api-key-module/src/main/java/com/gwidgets/resources/ApiKeyResource.java
+++ b/api-key-module/src/main/java/com/gwidgets/resources/ApiKeyResource.java
@@ -1,14 +1,15 @@
package com.gwidgets.resources;
-import java.util.List;
-import java.util.Objects;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.UserModel;
+
import javax.ws.rs.GET;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.UserModel;
+import java.util.Objects;
+import java.util.stream.Stream;
public class ApiKeyResource {
@@ -25,7 +26,7 @@ public class ApiKeyResource {
@GET
@Produces("application/json")
public Response checkApiKey(@QueryParam("apiKey") String apiKey) {
- List result = session.userStorageManager().searchForUserByUserAttribute("api-key", apiKey, session.realms().getRealm(realmName));
- return result.isEmpty() ? Response.status(401).type(MediaType.APPLICATION_JSON).build(): Response.ok().type(MediaType.APPLICATION_JSON).build();
+ Stream result = session.users().searchForUserByUserAttributeStream(session.realms().getRealm(realmName), "api-key", apiKey);
+ return result.count() > 0 ? Response.ok().type(MediaType.APPLICATION_JSON).build(): Response.status(401).type(MediaType.APPLICATION_JSON).build();
}
}
diff --git a/dashboard-service/pom.xml b/dashboard-service/pom.xml
index 2bf6dd7..c9a347d 100644
--- a/dashboard-service/pom.xml
+++ b/dashboard-service/pom.xml
@@ -16,7 +16,7 @@
11
- 15.0.2
+ 19.0.1
diff --git a/docker-compose.yaml b/docker-compose.yaml
index f1822e8..7e72ef0 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,16 +1,22 @@
-version: '3.7'
+version: '3.8'
services:
auth-server:
- image: jboss/keycloak:15.0.2
+ image: quay.io/keycloak/keycloak:19.0.1
environment:
- KEYCLOAK_USER: admin
- KEYCLOAK_PASSWORD: admin
- JAVA_OPTS_APPEND: "-Dkeycloak.migration.action=import -Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=/import -Dkeycloak.migration.strategy=IGNORE_EXISTING -Dkeycloak.profile.feature.upload_scripts=enabled"
+ KEYCLOAK_ADMIN: admin
+ KEYCLOAK_ADMIN_PASSWORD: admin
+ KC_HTTP_ENABLED: "true"
+ KC_HOSTNAME: auth-server
+ #to keep compatible with other services that are expecting /auth
+ KC_HTTP_RELATIVE_PATH: /auth
+ KC_HOSTNAME_STRICT_HTTPS: "false"
+ JAVA_OPTS_APPEND: "-Dkeycloak.migration.action=import -Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=/import -Dkeycloak.migration.strategy=IGNORE_EXISTING"
volumes:
- ./import:/import
- - ./api-key-module/target/deploy:/opt/jboss/keycloak/standalone/deployments/
+ - ./api-key-module/target/deploy:/opt/keycloak/providers/
ports:
- "8080:8080"
+ command: ["start"]
dashboard-service:
build: dashboard-service
environment:
diff --git a/import/example-realm.json b/import/example-realm.json
index 475a350..376aca0 100644
--- a/import/example-realm.json
+++ b/import/example-realm.json
@@ -192,31 +192,7 @@
]
}
],
- "policies": [
- {
- "id": "6fc4ef68-6935-4bc1-b1fe-9b7109e43fe1",
- "name": "Default Policy",
- "description": "A policy that grants access only for users within this realm",
- "type": "js",
- "logic": "POSITIVE",
- "decisionStrategy": "AFFIRMATIVE",
- "config": {
- "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n"
- }
- },
- {
- "id": "54f768bd-be40-4b44-acb5-9218d5fd3e2b",
- "name": "Default Permission",
- "description": "A permission that applies to the default resource type",
- "type": "resource",
- "logic": "POSITIVE",
- "decisionStrategy": "UNANIMOUS",
- "config": {
- "defaultResourceType": "urn:dashboard-client:resources:default",
- "applyPolicies": "[\"Default Policy\"]"
- }
- }
- ],
+ "policies": [],
"scopes": []
}
},