8000 Experimental SPIFFE identity provider by stianst · Pull Request #42314 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public enum Feature {

CLIENT_AUTH_FEDERATED("Authenticates client based on assertions issued by identity provider", Type.EXPERIMENTAL),

SPIFFE("SPIFFE trust relationship provider", Type.EXPERIMENTAL),

// Check if kerberos is available in underlying JVM and auto-detect if feature should be enabled or disabled by default based on that
KERBEROS("Kerberos", Type.DEFAULT, 1, () -> KerberosJdkProvider.getProvider().isKerberosAvailable()),

Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/org/keycloak/crypto/KeyUse.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
public enum KeyUse {

SIG("sig"),
ENC("enc");
ENC("enc"),
JWT_SVID("jwt-svid");

private String specName;

Expand Down
38 changes: 21 additions & 17 deletions core/src/main/java/org/keycloak/crypto/KeyWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,14 @@
*/
package org.keycloak.crypto;

import java.util.HashMap;
import java.util.List;
import javax.crypto.SecretKey;
import java.security.Key;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Map;
import java.util.List;

public class KeyWrapper {

/**
* A repository for the default algorithms by key type.
*/
private static final Map<String, String> DEFAULT_ALGORITHM_BY_TYPE = new HashMap<>();

static {
//backwards compatibility: RSA keys without "alg" field set are considered RS256
DEFAULT_ALGORITHM_BY_TYPE.put(KeyType.RSA, Algorithm.RS256);
}

private String providerId;
private long providerPriority;
private String kid;
Expand Down Expand Up @@ -85,18 +73,34 @@ public String getAlgorithm() {
}

/**
* <p>Returns the value of the optional {@code alg} claim. If not defined, a default is returned depending on the
* key type as per {@code kty} claim.
* <p>Returns the value of the optional {@code alg} claim. If not defined, a default is
* inferred for some algorithms.
*
* <p>For keys of type {@link KeyType#RSA}, the default algorithm is {@link Algorithm#RS256} as this is the default
* algorithm recommended by OIDC specs.
*
* <p>For keys of type {@link KeyType#EC}, {@link Algorithm#ES256}, {@link Algorithm#ES384}, or {@link Algorithm#ES512}
* is returned based on the curve
*
* @return the algorithm set or a default based on the key type.
*/
public String getAlgorithmOrDefault() {
if (algorithm == null) {
return DEFAULT_ALGORITHM_BY_TYPE.get(type);
if (algorithm == null && type != null) {
switch (type) {
case KeyType.EC:
if (curve != null) {
switch (curve) {
case "P-256":
return Algorithm.ES256;
case "P-384":
return Algorithm.ES384;
case "P-512":
return Algorithm.ES512;
}
}
case KeyType.RSA:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is probably similar that for KeyType.OKP , the Algorithm is EdDSA . But possibly this case could added/tested as a follow-up though... I can do that in the follow-up as I am looking at some DPoP related task, which is related: #42286

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I think we can extend on this quite a bit, but probably easier just as follow-ups when needed

return Algorithm.RS256;
}
}
return algorithm;
}
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/org/keycloak/jose/jwk/JSONWebKeySet.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@

package org.keycloak.jose.jwk;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class JSONWebKeySet {

@JsonProperty("keys")
Expand Down
7 changes: 6 additions & 1 deletion core/src/main/java/org/keycloak/jose/jwk/JWK.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,14 @@ public class JWK {

public static final String SHA256_509_THUMBPRINT = "x5t#S256";

/**
* This duplicates {@link org.keycloak.crypto.KeyUse}, which should be used instead when possible
*/
@Deprecated
public enum Use {
SIG("sig"),
ENCRYPTION("enc");
ENCRYPTION("enc"),
JWT_SVID("jwt-svid");

private String str;

Expand Down
2 changes: 1 addition & 1 deletion core/src/test/java/org/keycloak/util/JWKSUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public void publicRs256() throws Exception {

key = keyWrappersForUse.getKeyByKidAndAlg(kidEC2, null);
assertNotNull(key);
assertNull(key.getAlgorithmOrDefault());
assertEquals("ES384", key.getAlgorithmOrDefault());
assertEquals(KeyUse.SIG, key.getUse());
assertEquals(kidEC2, key.getKid());
assertEquals("EC", key.getType());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.JsonWebToken;

public interface ClientAssertionContext {

RealmModel getRealm();
ClientModel getClient();
String getAssertionType();
JWSInput getJwsInput();
JsonWebToken getToken();
ClientModel getClient();
boolean isFailure();
String getError();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,15 @@ public static IdentityProviderModel toModel(RealmModel realm, IdentityProviderRe
identityProviderModel.setAddReadTokenRoleOnCreate(representation.isAddReadTokenRoleOnCreate());
updateOrganizationBroker(representation, session);
identityProviderModel.setOrganizationId(representation.getOrganizationId());
identityProviderModel.setConfig(removeEmptyString(representation.getConfig()));

// Merge config from the identity provider model in case the provider sets some default config
Map<String, String> repConfig = removeEmptyString(representation.getConfig());
if (repConfig != null && !repConfig.isEmpty()) {
if (identityProviderModel.getConfig() == null) {
identityProviderModel.setConfig(new HashMap<>());
}
identityProviderModel.getConfig().putAll(repConfig);
}

String flowAlias = representation.getFirstBrokerLoginFlowAlias();
if (flowAlias == null || flowAlias.trim().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import org.keycloak.broker.provider.ClientAssertionContext;
import org.keycloak.broker.provider.ClientAssertionIdentityProvider;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.spiffe.SpiffeConstants;
import org.keycloak.common.Profile;
import org.keycloak.events.Details;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.JsonWebToken;
Expand All @@ -37,7 +39,7 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
new ProviderConfigProperty(JWT_CREDENTIAL_ISSUER_KEY, "Identity provider", "Issuer of the client assertion", ProviderConfigProperty.STRING_TYPE, null)
);

private static final Set<String> SUPPORTED_ASSERTION_TYPES = Set.of(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
private static final Set<String> SUPPORTED_ASSERTION_TYPES = Set.of(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT, SpiffeConstants.CLIENT_ASSERTION_TYPE);

@Override
public String getId() {
Expand Down Expand Up @@ -69,7 +71,7 @@ public void authenticateClient(ClientAuthenticationFlowContext context) {

ClientAssertionIdentityProvider identityProvider = lookupIdentityProvider(context, client);

ClientAssertionContext clientAssertionContext = new DefaultClientAssertionContext(client, clientAssertionType, jws, token);
ClientAssertionContext clientAssertionContext = new DefaultClientAssertionContext(context.getRealm(), client, clientAssertionType, jws, token);
if (identityProvider.verifyClientAssertion(clientAssertionContext)) {
context.success();
} else {
Expand Down Expand Up @@ -155,19 +157,31 @@ public boolean isSupported(Config.Scope config) {

private static class DefaultClientAssertionContext implements ClientAssertionContext {

private final RealmModel realm;
private final ClientModel client;
private final String assertionType;
private final JWSInput jwsInput;
private final JsonWebToken token;
private String error;

public DefaultClientAssertionContext(ClientModel client, String assertionType, JWSInput jwsInput, JsonWebToken token) {
public DefaultClientAssertionContext(RealmModel realm, ClientModel client, String assertionType, JWSInput jwsInput, JsonWebToken token) {
this.realm = realm;
this.client = client;
this.assertionType = assertionType;
this.jwsInput = jwsInput;
this.token = token;
}

@Override
public RealmModel getRealm() {
return realm;
}

@Override
public ClientModel getClient() {
return client;
}

@Override
public String getAssertionType() {
return assertionType;
Expand All @@ -183,11 +197,6 @@ public JsonWebToken getToken() {
return token;
}

@Override
public ClientModel getClient() {
return client;
}

@Override
public boolean isFailure() {
return error != null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.keycloak.broker.spiffe;

import org.keycloak.crypto.PublicKeysWrapper;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.keys.PublicKeyLoader;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
import org.keycloak.util.JWKSUtils;

public class SpiffeBundleEndpointLoader implements PublicKeyLoader {

private final KeycloakSession session;
private final String bundleEndpoint;

public SpiffeBundleEndpointLoader(KeycloakSession session, String bundleEndpoint) {
this.session = session;
this.bundleEndpoint = bundleEndpoint;
}

@Override
public PublicKeysWrapper loadKeys() throws Exception {
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, bundleEndpoint);
return JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.JWT_SVID);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.keycloak.broker.spiffe;

public interface SpiffeConstants {

String CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe";

}
Loading
0