diff --git a/roles/keycloak/files/providers/keycloak-mailpass-rest/README.md b/roles/keycloak/files/providers/keycloak-mailpass-rest/README.md index 73ddfaba9b83e36f3c8d74f3def5e3d1770a91ae..f1b20898d9146c3ee269919ce04dadaf8ce6650f 100644 --- a/roles/keycloak/files/providers/keycloak-mailpass-rest/README.md +++ b/roles/keycloak/files/providers/keycloak-mailpass-rest/README.md @@ -42,3 +42,5 @@ docker-compose up -d ## Install provider using the Keycloak Deployer If you copy your provider jar to the Keycloak standalone/deployments/ directory, your provider will automatically be deployed. Hot deployment works too. + +Need to add admin role to admin_cli client diff --git a/roles/keycloak/files/providers/keycloak-mailpass-rest/build.gradle.kts b/roles/keycloak/files/providers/keycloak-mailpass-rest/build.gradle.kts index 7be43bf263df235937bca3176866752bec278ca2..6e618ce020f1966b7dcf82a98ef18882a0f6f77e 100644 --- a/roles/keycloak/files/providers/keycloak-mailpass-rest/build.gradle.kts +++ b/roles/keycloak/files/providers/keycloak-mailpass-rest/build.gradle.kts @@ -16,16 +16,28 @@ repositories { } dependencies { + val bouncyCastleVersion = "1.67" val keycloakVersion = "11.0.3" val javaxVersion = "2.0.1.Final" - val bouncyCastleVersion = "1.67" + val junitVersion = "4.13.1" + val hamcrestVersion = "2.2" + val restassuredVersion = "4.3.3" + val keycloakMockVersion = "0.6.0" implementation("org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion") compileOnly("org.keycloak:keycloak-core:$keycloakVersion") compileOnly("org.keycloak:keycloak-server-spi:$keycloakVersion") compileOnly("org.keycloak:keycloak-server-spi-private:$keycloakVersion") + compileOnly("org.keycloak:keycloak-services:$keycloakVersion") + compileOnly("org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_2.1_spec:$javaxVersion") + + testImplementation("junit:junit:$junitVersion") + testImplementation("org.hamcrest:hamcrest:$hamcrestVersion") + testImplementation("io.rest-assured:rest-assured:$restassuredVersion") + testImplementation("com.tngtech.keycloakmock:mock-junit:$keycloakMockVersion") + } tasks { @@ -41,4 +53,14 @@ tasks { wrapper { gradleVersion = "6.7" } + + test { + useJUnit() + + testLogging { + showStandardStreams = true + } + + maxHeapSize = "1G" + } } diff --git a/roles/keycloak/files/providers/keycloak-mailpass-rest/docker/invoke-authenticated.sh b/roles/keycloak/files/providers/keycloak-mailpass-rest/docker/invoke-authenticated.sh new file mode 100644 index 0000000000000000000000000000000000000000..56eeda5ffc7b4881b5493c4253e1427a6f70184b --- /dev/null +++ b/roles/keycloak/files/providers/keycloak-mailpass-rest/docker/invoke-authenticated.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +export DIRECT_GRANT_RESPONSE=$(curl -i --request POST http://localhost:8080/auth/realms/master/protocol/openid-connect/token --header "Accept: application/json" --header "Content-Type: application/x-www-form-urlencoded" --data "grant_type=password&username=admin&password=admin&client_id=admin-cli") + +echo -e "\n\nSENT RESOURCE-OWNER-PASSWORD-CREDENTIALS-REQUEST. OUTPUT IS:\n\n"; +echo $DIRECT_GRANT_RESPONSE; + +export ACCESS_TOKEN=$(echo $DIRECT_GRANT_RESPONSE | grep "access_token" | sed 's/.*\"access_token\":\"\([^\"]*\)\".*/\1/g'); +echo -e "\n\nACCESS TOKEN IS \"$ACCESS_TOKEN\""; + +echo -e "\n\nSENDING UN-AUTHENTICATED REQUEST. THIS SHOULD FAIL WITH 401: "; +curl -i --request POST http://localhost:8080/auth/realms/master/mailpass/roleauth/compute-password-hash --data "{ \"pass\": \"password\" }" --header "Content-type: application/json" + +echo -e "\n\nSENDING AUTHENTICATED REQUEST. THIS SHOULD SUCCESSFULY CREATE PASSWORD HASH AND SUCCESS WITH 201: "; +curl -i --request POST http://localhost:8080/auth/realms/master/mailpass/roleauth/compute-password-hash --data "{ \"pass\": \"ompany\" }" --header "Content-type: application/json" --header "Authorization: Bearer $ACCESS_TOKEN"; diff --git a/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResource.java b/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResource.java new file mode 100644 index 0000000000000000000000000000000000000000..8bd06c843b5fbf9138667332f050aa639c95baa5 --- /dev/null +++ b/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResource.java @@ -0,0 +1,71 @@ +package org.archlinux.keycloak.mailpass.rest; + +import java.security.SecureRandom; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.models.KeycloakSession; + +/** + * A custom REST endpoint to trigger functionality on the Keycloak server, which is not available + * through the default set of built-in Keycloak REST endpoints. This is to be used during the + * storage of a custom attribute on the Account Management Console for the mail password. The data + * stored will be a Bcrypt hash instead of the plain text password. + */ +public class MailPassResource { + + + @SuppressWarnings("unused") + private final KeycloakSession session; + + private static final int SALT_LENGTH = 16; + private static final int COST = 12; + private static final String VARIANT = "2b"; + + private byte[] generateSalt() { + // https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#secure-random-number-generation + SecureRandom random = new SecureRandom(); + byte[] salt = new byte[SALT_LENGTH]; + random.nextBytes(salt); + return salt; + } + + public MailPassResource(KeycloakSession session) { + this.session = session; + } + + @GET + @Path("hello") + @Produces("text/plain; charset=utf-8") + public String get() { + String name = session.getContext().getRealm().getDisplayName(); + if (name == null) { + name = session.getContext().getRealm().getName(); + } + return "Hello " + name; + } + + /** + * The custom REST endpoint reachable at {{ base_url }}/realms/{{ realm }}/mailpass/hashify. + * + * @param data The JSON property including the password entry. + * @return Response instance including the hashed password string. + */ + @POST + @Path("compute-password-hash") + @NoCache + @Consumes(MediaType.APPLICATION_JSON) + public Response generateBcryptHash(String data) { + + byte[] salt = generateSalt(); + String hash = OpenBSDBCrypt.generate(VARIANT, data.toCharArray(), salt, COST); + + return Response.status(201).entity(hash).build(); + } +} diff --git a/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResourceProvider.java b/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResourceProvider.java index f4af4958f96906f9a0f9c452fc367c88020174b8..18daad81db108980a43d3870faa71237902ffd1f 100644 --- a/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResourceProvider.java +++ b/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResourceProvider.java @@ -1,66 +1,28 @@ package org.archlinux.keycloak.mailpass.rest; -import java.security.SecureRandom; -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.keycloak.models.KeycloakSession; import org.keycloak.services.resource.RealmResourceProvider; /** - * A custom REST endpoint to trigger functionality on the Keycloak server, which is not available - * through the default set of built-in Keycloak REST endpoints. This is to be used during the - * storage of a custom attribute on the Account Management Console for the mail password. The data - * stored will be a Bcrypt hash instead of the plain text password. + * A provider of a custom REST resource for a path relative to Realm's RESTful API + * that cannot be resolved by the Keycloak server. This implementation is + * a requirement for custom REST endpoints. */ public class MailPassResourceProvider implements RealmResourceProvider { - private static final int SALT_LENGTH = 16; - private static final int COST = 12; - private static final String VARIANT = "2b"; - - @SuppressWarnings("unused") private KeycloakSession session; - private byte[] generateSalt() { - // https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#secure-random-number-generation - SecureRandom random = new SecureRandom(); - byte[] salt = new byte[SALT_LENGTH]; - random.nextBytes(salt); - return salt; - } - public MailPassResourceProvider(KeycloakSession session) { this.session = session; } @Override public Object getResource() { - return this; + return new MailPassRestResource(session); } @Override public void close() { } - /** - * The custom REST endpoint reachable at {{ base_url }}/realms/{{ realm }}/mailpass/hashify. - * - * @param data The JSON property including the password entry. - * @return Response instance including the hashed password string. - */ - @POST - @Path("compute-password-hash") - @Consumes(MediaType.APPLICATION_JSON) - public Response generateBcryptHash(String data) { - - byte[] salt = generateSalt(); - String hash = OpenBSDBCrypt.generate(VARIANT, data.toCharArray(), salt, COST); - - return Response.status(201).entity(hash).build(); - } - } diff --git a/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResourceProviderFactory.java b/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResourceProviderFactory.java index 853f0d0a985c9201f57f50c17f740356a8e14cd1..dc39bb25b6523883ed41a1529191282f3bc2622c 100644 --- a/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResourceProviderFactory.java +++ b/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassResourceProviderFactory.java @@ -8,8 +8,7 @@ import org.keycloak.services.resource.RealmResourceProviderFactory; /** * A factory that extends RealmResourceProviderFactory instance to create the custom mail pass - * resource for a path relative to Realm's RESTful API that cannot be resolved by the Keycloak - * server. This is a requirement for custom REST endpoints. + * resource provider. This implementation is a requirement for custom REST endpoints. */ public class MailPassResourceProviderFactory implements RealmResourceProviderFactory { diff --git a/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassRestResource.java b/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassRestResource.java new file mode 100644 index 0000000000000000000000000000000000000000..353319c8f04425ae4ea082278658c5b1f39ca6d7 --- /dev/null +++ b/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/java/org/archlinux/keycloak/mailpass/rest/MailPassRestResource.java @@ -0,0 +1,46 @@ +package org.archlinux.keycloak.mailpass.rest; + +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.Path; +import org.keycloak.models.KeycloakSession; +import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.AuthenticationManager; + +/** + * A custom REST resource in the form of a domain extension which makes the custom endpoint + * accessible just for authenticated users. The REST request must be authenticated with bearer + * access token of an authenticated user and the user must be part of a pre-configured realm role in + * order to access the resource. + */ +public class MailPassRestResource { + + private final KeycloakSession session; + private final AuthenticationManager.AuthResult auth; + + public MailPassRestResource(KeycloakSession session) { + this.session = session; + this.auth = new AppAuthManager().authenticateBearerToken(session); + } + + @Path("norole") + public MailPassResource getMailPassResource() { + return new MailPassResource(session); + } + + @Path("roleauth") + public MailPassResource getMailPassResourceAuthenticated() { + checkRealmAdmin(); + return new MailPassResource(session); + } + + private void checkRealmAdmin() { + if (auth == null) { + throw new NotAuthorizedException("Bearer"); + } else if (auth.getToken().getRealmAccess() == null + || !auth.getToken().getRealmAccess().isUserInRole("admin")) { + throw new ForbiddenException("Does not have realm admin role"); + } + } + +} diff --git a/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/resources/META-INF/jboss-deployment-structure.xml b/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/resources/META-INF/jboss-deployment-structure.xml index d2577ef12a89e4021ea1c662975bcc83a4c95a76..16268adb63aa3d41b89dd783bd73f04855e154c3 100644 --- a/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/resources/META-INF/jboss-deployment-structure.xml +++ b/roles/keycloak/files/providers/keycloak-mailpass-rest/src/main/resources/META-INF/jboss-deployment-structure.xml @@ -1 +1,14 @@ -<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.0" /> +<jboss-deployment-structure> + <deployment> + <dependencies> + <module name="org.keycloak.keycloak-core" /> + <module name="org.keycloak.keycloak-server-spi" /> + <module name="org.jboss.resteasy.resteasy-jaxrs" /> + <module name="org.keycloak.keycloak-server-spi-private" /> + <module name="org.keycloak.keycloak-services" /> + <module name="org.keycloak.keycloak-common" /> + <module name="org.jboss.logging" /> + <module name="javax.ws.rs.api" /> + </dependencies> + </deployment> +</jboss-deployment-structure>