ugrade to Keycloak 19.0.1

This commit is contained in:
zak905 2022-09-07 12:22:02 +02:00
parent 2ec76fe137
commit bf9a740c2b
8 changed files with 50 additions and 55 deletions

View file

@ -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 ## 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` 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 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 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) More explanations can be found in this blog [post](http://www.zakariaamine.com/2019-06-14/extending-keycloak)

View file

@ -10,7 +10,7 @@
<properties> <properties>
<java.version>11</java.version> <java.version>11</java.version>
<keycloak.version>15.0.2</keycloak.version> <keycloak.version>19.0.1</keycloak.version>
</properties> </properties>
<dependencies> <dependencies>

View file

@ -4,7 +4,7 @@ package com.gwidgets.providers;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import javax.persistence.EntityManager; 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.connections.jpa.JpaConnectionProvider;
import org.keycloak.events.Event; import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventListenerProvider;
@ -23,14 +23,14 @@ public class RegisterEventListenerProvider implements EventListenerProvider {
private KeycloakSession session; private KeycloakSession session;
private RealmProvider model; private RealmProvider model;
//keycloak utility to generate random strings, anything can be used e.g UUID,.. //keycloak utility to generate random strings, anything can be used e.g UUID,..
private RandomString randomString; private SecretGenerator secretGenerator;
private EntityManager entityManager; private EntityManager entityManager;
public RegisterEventListenerProvider(KeycloakSession session) { public RegisterEventListenerProvider(KeycloakSession session) {
this.session = session; this.session = session;
this.model = session.realms(); this.model = session.realms();
this.entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager(); this.entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
this.randomString = new RandomString(50); this.secretGenerator = SecretGenerator.getInstance();
} }
public void onEvent(Event event) { public void onEvent(Event event) {
@ -55,8 +55,7 @@ public class RegisterEventListenerProvider implements EventListenerProvider {
public void addApiKeyAttribute(String userId) { public void addApiKeyAttribute(String userId) {
String apiKey = secretGenerator.randomString(50);
String apiKey = randomString.nextString();
UserEntity userEntity = entityManager.find(UserEntity.class, userId); UserEntity userEntity = entityManager.find(UserEntity.class, userId);
UserAttributeEntity attributeEntity = new UserAttributeEntity(); UserAttributeEntity attributeEntity = new UserAttributeEntity();
attributeEntity.setName("api-key"); attributeEntity.setName("api-key");

View file

@ -8,6 +8,7 @@ import com.amazonaws.services.simpleemail.model.Message;
import com.amazonaws.services.simpleemail.model.SendEmailRequest; import com.amazonaws.services.simpleemail.model.SendEmailRequest;
import java.util.Map; import java.util.Map;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailSenderProvider; import org.keycloak.email.EmailSenderProvider;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -24,20 +25,25 @@ public class SESEmailSenderProvider implements EmailSenderProvider {
@Override @Override
public void send(Map<String, String> config, UserModel user, String subject, String textBody, public void send(Map<String, String> 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<String, String> config, String address, String subject, String textBody, String htmlBody) throws EmailException {
Message message = new Message().withSubject(new Content().withData(subject)) log.info("attempting to send email using aws ses for " + address);
.withBody(new Body().withHtml(new Content().withData(htmlBody))
.withText(new Content().withData(textBody).withCharset("UTF-8")));
SendEmailRequest sendEmailRequest = new SendEmailRequest() Message message = new Message().withSubject(new Content().withData(subject))
.withSource("example<" + config.get("from") + ">") .withBody(new Body().withHtml(new Content().withData(htmlBody))
.withMessage(message).withDestination(new Destination().withToAddresses(user.getEmail())); .withText(new Content().withData(textBody).withCharset("UTF-8")));
sesClient.sendEmail(sendEmailRequest); SendEmailRequest sendEmailRequest = new SendEmailRequest()
log.info("email sent to " + user.getEmail() + " successfully"); .withSource("example<" + config.get("from") + ">")
.withMessage(message).withDestination(new Destination().withToAddresses(address));
sesClient.sendEmail(sendEmailRequest);
log.info("email sent to " + address + " successfully");
} }
@Override @Override

View file

@ -1,14 +1,15 @@
package com.gwidgets.resources; package com.gwidgets.resources;
import java.util.List; import org.keycloak.models.KeycloakSession;
import java.util.Objects; import org.keycloak.models.UserModel;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.keycloak.models.KeycloakSession; import java.util.Objects;
import org.keycloak.models.UserModel; import java.util.stream.Stream;
public class ApiKeyResource { public class ApiKeyResource {
@ -25,7 +26,7 @@ public class ApiKeyResource {
@GET @GET
@Produces("application/json") @Produces("application/json")
public Response checkApiKey(@QueryParam("apiKey") String apiKey) { public Response checkApiKey(@QueryParam("apiKey") String apiKey) {
List<UserModel> result = session.userStorageManager().searchForUserByUserAttribute("api-key", apiKey, session.realms().getRealm(realmName)); Stream<UserModel> result = session.users().searchForUserByUserAttributeStream(session.realms().getRealm(realmName), "api-key", apiKey);
return result.isEmpty() ? Response.status(401).type(MediaType.APPLICATION_JSON).build(): Response.ok().type(MediaType.APPLICATION_JSON).build(); return result.count() > 0 ? Response.ok().type(MediaType.APPLICATION_JSON).build(): Response.status(401).type(MediaType.APPLICATION_JSON).build();
} }
} }

View file

@ -16,7 +16,7 @@
<properties> <properties>
<java.version>11</java.version> <java.version>11</java.version>
<keycloak.version>15.0.2</keycloak.version> <keycloak.version>19.0.1</keycloak.version>
</properties> </properties>
<dependencies> <dependencies>

View file

@ -1,16 +1,22 @@
version: '3.7' version: '3.8'
services: services:
auth-server: auth-server:
image: jboss/keycloak:15.0.2 image: quay.io/keycloak/keycloak:19.0.1
environment: environment:
KEYCLOAK_USER: admin KEYCLOAK_ADMIN: admin
KEYCLOAK_PASSWORD: admin KEYCLOAK_ADMIN_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" 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: volumes:
- ./import:/import - ./import:/import
- ./api-key-module/target/deploy:/opt/jboss/keycloak/standalone/deployments/ - ./api-key-module/target/deploy:/opt/keycloak/providers/
ports: ports:
- "8080:8080" - "8080:8080"
command: ["start"]
dashboard-service: dashboard-service:
build: dashboard-service build: dashboard-service
environment: environment:

View file

@ -192,31 +192,7 @@
] ]
} }
], ],
"policies": [ "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\"]"
}
}
],
"scopes": [] "scopes": []
} }
}, },