I\'ve seen those two post that give a solution to this question but they do not provide detailed enough informations about how to do it for non Java developer like me:
K
I hope this step by step guide helps you
I'm using Keycloak 4.5.0 - because I have this newer version installed - but I should not make a big difference. And I implemented a OIDCProtocolMapper
in the example.
Just to summarize it - for the quick overview for others - each step is described more detailed later
You implement a CustomProtocolMapper class based on
AbstractOIDCProtocolMapper
META-INF/services File with the
name org.keycloak.protocol.ProtocolMapper
must be available and
contains the name of your mapper
jboss-deployment-structure.xml
need to be available to use
keycloak built in classes
Jar File is deployed in
/opt/jboss/keycloak/standalone/deployments/
Okay now more details :-)
I uploaded you my maven pom.xml
(pom) - just import it into your IDE and all the dependencies should be loaded automatically. The dependencies are just provided
and will be later used from keycloak directly at runtime
Relevant is the keycloak.version
property - all keycloak dependencies are currently loaded in version 4.5.0.Final
Now i created a custom Protocol Mapper Class called CustomOIDCProtocolMapper
. Find "full" code here
It should extend AbstractOIDCProtocolMapper
and need to implement all abstract methods. Maybe you want to have a SAML Protocol Mapper then it's another base class (AbstractSAMLProtocolMapper
)
one relevant method is transformAccessToken
- here I set a additional Claim to the AccessToken. You need your logic here but yeah - depends on your database, etc. ;-)
The services File is important for keycloak to find your custom-Implementation
Place a file with the fileName org.keycloak.protocol.ProtocolMapper
inside \src\main\resources\META-INF\services\
Inside this file you write to Name of your custom Provider - so keycloak knows that this class is available as Protocol Mapper
In my example the file content is just one line
com.stackoverflow.keycloak.custom.CustomOIDCProtocolMapper
In your custom mapper you use files from keycloak. In order to use them we need to inform jboss about this dependency.
Therefore create a file jboss-deployment-structure.xml
inside \src\main\resources\META-INF\
Content:
<jboss-deployment-structure>
<deployment>
<dependencies>
<module name="org.keycloak.keycloak-services" />
</dependencies>
</deployment>
</jboss-deployment-structure>
Build a jar File of your Extension (mvn clean package
) - and place the jar
in /opt/jboss/keycloak/standalone/deployments/
and restart keycloak
In the logfile you should see when it's deployed and (hopefully no) error messages
Now you can use your mapper - In my example I can create a Mapper in keycloak admin ui and select Stackoverflow Custom Protocol Mapper
from dropdown
Just as info - this is not fully official supported by keycloak - so interfaces could possible change in later versions
I hope it's understandable and you will be able to succesfully implement your own mapper
EDIT: Exported eclipse file structure zip
I am using a Custom Protocol Mapper1 to send an authenticated2 GraphQL query3 to an external system and put the JSON response data into the user's access token (JWT). It currently runs with Keycloak 10.
==> You can find the full code in this repository.
As others have noted, your project needs at least 3 files.
AbstractOIDCProtocolMapper
& its method setClaim
(among others).jboss-deployment-structure.xml
file that contains the dependencies for deployment.org.keycloak.protocol.ProtocolMapper
file that contains the full name of the custom protocol mapper.Here is the folder structure:
$ tree src/ -A
src/
└── main
├── java
│ └── com
│ └── thohol
│ └── keycloak
│ └── JsonGraphQlRemoteClaim.java
└── resources
└── META-INF
├── jboss-deployment-structure.xml
└── services
└── org.keycloak.protocol.ProtocolMapper
If the remote endpoint requires authentication, we can obtain an Access Token from Keycloak. The complete flow would look as follows (especially steps 3-6):
login-client
.login-client
is configured to use the Custom Protocol Mapper, its code gets executed while the user's authentication request is being processed.remote-claims-client
) using client_credentials
(Client ID + Secret).remote-claims-client
.Authorization: Bearer <access token>
header is added to the request headers.remote-claims-client
.Steps 3/4 can be implemented as a simple HTTP POST request in Java (error handling omitted!):
// Call remote service
HttpClient httpClient = HttpClient.newHttpClient();
URIBuilder uriBuilder = new URIBuilder(keycloakAuthUrl);
URI uri = uriBuilder.build();
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(uri);
String queryBody = "grant_type=client_credentials&client_id=remote-claims-client&client_secret=dfebc62a-e8d7-4ab3-9196-258ddb5684ab";
builder.POST(HttpRequest.BodyPublishers.ofString(queryBody));
// Build headers
builder.header(HttpHeaders.CONTENT_TYPE , MediaType.APPLICATION_FORM_URLENCODED);
// Call
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
// Process Response
JsonNode json = return new ObjectMapper().readTree(response.body());
String accessToken = json.findValue("access_token").asText();
A GraphQL query is essentially an HTTP POST request, with a body
like
{
"query": "query HeroName($episode: Episode) {
hero(episode: $episode) {
name
}
}",
"variables": {
"episode" : "JEDI"
}
}
This can be sent from Java like (error handling omitted!):
HttpClient httpClient = HttpClient.newHttpClient();
URIBuilder uriBuilder = new URIBuilder(remoteUrl);
URI uri = uriBuilder.build();
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(uri);
String queryBody = "{
\"query\": \"query HeroName($episode: Episode) {
hero(episode: $episode) {
name
}
}\",
\"variables\": {
\"episode\" : \"JEDI\"
}
}";
builder.POST(HttpRequest.BodyPublishers.ofString(queryBody));
// Build headers
builder.header(HttpHeaders.CONTENT_TYPE , MediaType.APPLICATION_JSON);
builder.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
// Call
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
// Process Response and add to token
JsonNode json = return new ObjectMapper().readTree(response.body());
clientSessionCtx.setAttribute("custom_claims", json);
I am the owner/author of the linked repository. However, I did not start from scratch but used multiple other repositories as basis/inspiration. See the repo's README.