Skip to content

Commit

Permalink
Merge pull request #12 from nicolabeghin/main
Browse files Browse the repository at this point in the history
Compatibility Keycloak 23
  • Loading branch information
nicolabeghin authored Feb 11, 2024
2 parents af980ba + 5627b85 commit bbf8a99
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 93 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Set up JDK 1.8
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 1.17

- name: Cache Maven packages
uses: actions/cache@v2
Expand Down
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
SERVICE_TARGET := maven:3.8.7-amazoncorretto-11

# all our targets are phony (no files to check).
.PHONY: help package

# suppress makes own output
#.SILENT:

help:
@echo 'Usage: make [TARGET] '

build: package

package:
docker run --rm -v ${shell pwd}:/opt/app -w /opt/app ${SERVICE_TARGET} bash -c "mvn clean package"
49 changes: 27 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,59 @@ with existing applications by leveraging Keycloak identity brokering features.
Keycloak is a nice product, but still lacking on some aspects of SAML2 compatibility,
and the CIE ID specifications deviate from the SAML2 standard in some key aspects.

Besides the CIE ID-SAML2 protocol differences, some of the SP behaviors
are hardcoded to work with simple IdPs only (i.e. there is no support for generating SP metadata
that joins multiple SPs) . Keycloak is slowly improving on this aspect, so over time this plugin
will become simpler and targeted on implementing only the specific changes required by SPID.

I have documented a reference configuration for CIE ID and the workarounds required
in the project wiki (https://github.com/lscorcia/keycloak-cieid-provider/wiki). Please make
sure to read it and understand the config steps and the open issues and
limitations before planning your Production environment.

## Status
This project is still at an alpha stage. It is currently under development
and things may change quickly. It builds and successfully allows the CIE ID authentication
process, but I'm still working on it and I haven't tested it extensively since
I don't have access to the CIE ID Production environment yet.
As far as I know it has not been used in Production in any environment yet.
This project is still at a beta stage, but it has been successfully tested for [CIE ID federation](https://docs.italia.it/italia/cie/cie-manuale-operativo-docs/it/master/onboarding.html) and **it's currently used in Production**.

Until the project gets to a stable release, it will be targeting the most recent release
of Keycloak as published on the website (see property `version.keycloak` in file `pom.xml`).
Currently the main branch is targeting Keycloak 16.1.1. **Do not use the latest release with previous
versions of Keycloak, it won't work!**

Since this plugin uses some Keycloak internal modules, versions of this plugin
are coupled to Keycloak versions. After (major) Keycloak upgrades, you will almost
certainly have also to update this provider.

Detailed instructions on how to install and configure this component are
## Compatibility
* Keycloak 23.x.x: Release 1.0.7
* Keycloak 19.x.x: Release 1.0.6

## Configuration
### Release 1.0.7 (latest, Keycloak 23.x.x compatibility)
With the latest release targeting latest Keycloak 23.x.x it's not possible to configure the plugin through the Keycloak web UI,
but only through REST services. Suggested to use https://github.com/nicolabeghin/keycloak-cieid-provider-configuration-client

### Release 1.0.6
It's possible to configure the plugin through the Keycloak web UI, detailed instructions are
available in the project wiki (https://github.com/lscorcia/keycloak-cieid-provider/wiki/Installing-the-CIE-ID-provider).
To avoid errors, it's suggested to use anyway https://github.com/nicolabeghin/keycloak-cieid-provider-configuration-client

## Build requirements
* git
* JDK8+
* JDK17+
* Maven

## Build
## Build (without docker)
Just run `mvn clean package` for a full rebuild. The output package will
be generated under `target/cieid-provider.jar`.

## Deployment
This provider should be deployed as a module, i.e. copied under
`{$KEYCLOAK_PATH}/standalone/deployments/`, with the right permissions.
Keycloak will take care of loading the module, no restart needed.
## Build (with docker)
Requirements:
* Docker

Use this command for reference:
Just run:
```
mvn clean package && \
sudo install -C -o keycloak -g keycloak target/cieid-provider.jar /opt/keycloak/standalone/deployments/
git clone https://github.com/italia/keycloak-cieid-provider.git
docker run --rm -v $(pwd)/keycloak-cieid-provider:/opt/keycloak-cieid-provider -w /opt/keycloak-cieid-provider maven:3.8.6-openjdk-18-slim bash -c "mvn clean package"
```
The output package will be generated under `cieid-provider/target/cieid-provider.jar`.

## Deployment
This provider should be deployed as a module, i.e. copied under
`{$KEYCLOAK_PATH}/providers/`, with the right permissions.
Keycloak will take care of loading the module, no restart needed.

If successful you will find a new provider type called `CIE ID` in the
`Add Provider` drop down list in the Identity Provider configuration screen.
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.lscorcia</groupId>
<artifactId>keycloak-cieid-provider</artifactId>
<version>1.0.6-SNAPSHOT</version>
<version>1.0.7-SNAPSHOT</version>
<packaging>jar</packaging>

<name>Keycloak CIE ID Service Provider</name>
Expand All @@ -21,7 +21,7 @@
<failOnMissingWebXml>false</failOnMissingWebXml>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<version.keycloak>16.1.1</version.keycloak>
<version.keycloak>23.0.6</version.keycloak>
<slf4j-api.version>1.7.30</slf4j-api.version>
<junit.version>4.13.2</junit.version>
</properties>
Expand Down
77 changes: 58 additions & 19 deletions src/main/java/org/keycloak/broker/cieid/CieIdIdentityProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.metadata.AttributeConsumingServiceType;
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.dom.saml.v2.metadata.LocalizedNameType;
import org.keycloak.dom.saml.v2.metadata.RequestedAttributeType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
Expand Down Expand Up @@ -79,26 +81,22 @@
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.parsers.ParserConfigurationException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.*;
import java.util.stream.Collectors;
import javax.xml.stream.XMLStreamWriter;

import java.io.StringWriter;
import java.net.URI;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

/**
* @author Pedro Igor
Expand All @@ -115,7 +113,7 @@ public CieIdIdentityProvider(KeycloakSession session, CieIdIdentityProviderConfi

@Override
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
return new CieIdSAMLEndpoint(realm, this, getConfig(), callback, destinationValidator);
return new CieIdSAMLEndpoint(session, this, getConfig(), callback, destinationValidator);
}

@Override
Expand Down Expand Up @@ -178,7 +176,27 @@ public Response performLogin(AuthenticationRequest request) {
boolean postBinding = getConfig().isPostBindingAuthnRequest();

if (getConfig().isWantAuthnRequestsSigned()) {
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
KeyManager.ActiveRsaKey keys = session.keys().getKeysStream(realm, KeyUse.SIG, Algorithm.RS256)
.filter(Objects::nonNull)
.filter(key -> key.getCertificate() != null)
.sorted(SamlService::compareKeys)
.filter(keyWrapper -> keyWrapper.getStatus().isEnabled())
.filter(keyWrapper -> keyWrapper.getStatus().isActive())
.filter(keyWrapper -> {
final Optional<String> realmKeysProviderId = Optional.ofNullable(getConfig().getRealmKeysProviderId());
if (realmKeysProviderId.isPresent()) {
return keyWrapper.getProviderId().equalsIgnoreCase(realmKeysProviderId.get());
}
return true;
})
.map(keyWrapper -> {
return new KeyManager.ActiveRsaKey(
keyWrapper.getKid(),
(PrivateKey) keyWrapper.getPrivateKey(),
(PublicKey) keyWrapper.getPublicKey(),
keyWrapper.getCertificate()
);
}).findFirst().orElseThrow(() -> new RuntimeException("Cannot find valid certificate for signin."));

String keyName = getConfig().getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
binding.signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
Expand Down Expand Up @@ -379,8 +397,8 @@ public Response export(UriInfo uriInfo, RealmModel realm, String format) {
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();


List<Element> signingKeys = new LinkedList<>();
List<Element> encryptionKeys = new LinkedList<>();
List<KeyDescriptorType> signingKeys = new LinkedList<>();
List<KeyDescriptorType> encryptionKeys = new LinkedList<>();

session.keys().getKeysStream(realm, KeyUse.SIG, Algorithm.RS256)
.filter(Objects::nonNull)
Expand All @@ -390,10 +408,10 @@ public Response export(UriInfo uriInfo, RealmModel realm, String format) {
try {
Element element = SPMetadataDescriptor
.buildKeyInfoElement(key.getKid(), PemUtils.encodeCertificate(key.getCertificate()));
signingKeys.add(element);
signingKeys.add(SPMetadataDescriptor.buildKeyDescriptorType(element, KeyTypes.SIGNING, null));

if (key.getStatus() == KeyStatus.ACTIVE) {
encryptionKeys.add(element);
encryptionKeys.add(SPMetadataDescriptor.buildKeyDescriptorType(element, KeyTypes.ENCRYPTION, null));
}
} catch (ParserConfigurationException e) {
logger.warn("Failed to export SAML SP Metadata!", e);
Expand All @@ -406,7 +424,7 @@ public Response export(UriInfo uriInfo, RealmModel realm, String format) {
XMLStreamWriter writer = StaxUtil.getXMLStreamWriter(sw);
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);

EntityDescriptorType entityDescriptor = SPMetadataDescriptor.buildSPdescriptor(
EntityDescriptorType entityDescriptor = SPMetadataDescriptor.buildSPDescriptor(
authnBinding, authnBinding, endpoint, endpoint,
wantAuthnRequestsSigned, wantAssertionsSigned, wantAssertionsEncrypted,
entityId, nameIDPolicyFormat, signingKeys, encryptionKeys);
Expand Down Expand Up @@ -457,7 +475,28 @@ public Response export(UriInfo uriInfo, RealmModel realm, String format) {
// Metadata signing
if (getConfig().isSignSpMetadata())
{
KeyManager.ActiveRsaKey activeKey = session.keys().getActiveRsaKey(realm);
KeyManager.ActiveRsaKey activeKey = session.keys().getKeysStream(realm, KeyUse.SIG, Algorithm.RS256)
.filter(Objects::nonNull)
.filter(key -> key.getCertificate() != null)
.sorted(SamlService::compareKeys)
.filter(keyWrapper -> keyWrapper.getStatus().isEnabled())
.filter(keyWrapper -> keyWrapper.getStatus().isActive())
.filter(keyWrapper -> {
final Optional<String> realmKeysProviderId = Optional.ofNullable(getConfig().getRealmKeysProviderId());
if (realmKeysProviderId.isPresent()) {
return keyWrapper.getProviderId().equalsIgnoreCase(realmKeysProviderId.get());
}
return true;
})
.map(keyWrapper -> {
return new KeyManager.ActiveRsaKey(
keyWrapper.getKid(),
(PrivateKey) keyWrapper.getPrivateKey(),
(PublicKey) keyWrapper.getPublicKey(),
keyWrapper.getCertificate()
);
}).findFirst().orElseThrow(() -> new RuntimeException("Cannot find valid certificate for signin."));

String keyName = getConfig().getXmlSigKeyInfoKeyNameTransformer().getKeyName(activeKey.getKid(), activeKey.getCertificate());
KeyPair keyPair = new KeyPair(activeKey.getPublicKey(), activeKey.getPrivateKey());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public class CieIdIdentityProviderConfig extends IdentityProviderModel {
public static final String TECHNICAL_CONTACT_COUNTRY = "technicalContactCountry";
public static final String TECHNICAL_CONTACT_PHONE = "technicalContactPhone";
public static final String TECHNICAL_CONTACT_EMAIL = "technicalContactEmail";
public static final String REALM_KEYS_PROVIDER_ID = "realmKeysProviderId";

public CieIdIdentityProviderConfig(){
}
Expand Down Expand Up @@ -603,4 +604,13 @@ public String getTechnicalContactPhone() {
public void setTechnicalContactPhone(String contactPhone) {
getConfig().put(TECHNICAL_CONTACT_PHONE, contactPhone);
}

public void setRealmKeysProviderId(String realmKeysProviderId) {
getConfig().put(REALM_KEYS_PROVIDER_ID, realmKeysProviderId);
}

public String getRealmKeysProviderId() {
return getConfig().get(REALM_KEYS_PROVIDER_ID);
}

}
Loading

0 comments on commit bbf8a99

Please sign in to comment.