Skip to content
Snippets Groups Projects
Verified Commit e74867a3 authored by Ira ¯\_(ツ)_/¯'s avatar Ira ¯\_(ツ)_/¯
Browse files

Convert to domain extension to enable authenticated requests

parent 57aad6b9
No related branches found
No related tags found
No related merge requests found
Showing
with 176 additions and 46 deletions
......@@ -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
......@@ -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"
}
}
#!/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";
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();
}
}
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();
}
}
......@@ -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 {
......
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");
}
}
}
<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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment