mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Experimental federated client authentication
Closes #42228 Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
parent
624d236ced
commit
57242d2497
@ -91,6 +91,8 @@ public class Profile {
|
||||
|
||||
STEP_UP_AUTHENTICATION("Step-up Authentication", Type.DEFAULT),
|
||||
|
||||
CLIENT_AUTH_FEDERATED("Authenticates client based on assertions issued by identity 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()),
|
||||
|
||||
|
||||
@ -43,6 +43,10 @@ public class CryptoIntegration {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isInitialised() {
|
||||
return cryptoProvider != null;
|
||||
}
|
||||
|
||||
public static CryptoProvider getProvider() {
|
||||
if (cryptoProvider == null) {
|
||||
throw new IllegalStateException("Illegal state. Please init first before obtaining provider");
|
||||
|
||||
@ -3230,6 +3230,8 @@ organizationDetails=Organization details
|
||||
organizationsList=Organizations
|
||||
caseSensitiveOriginalUsername=Case-sensitive username
|
||||
caseSensitiveOriginalUsernameHelp=If enabled, the original username from the identity provider is kept as is when federating users. Otherwise, the username from the identity provider is lower-cased and might not match the original value if it is case-sensitive. This setting only affects the username associated with the federated identity as usernames in the server are always in lower-case.
|
||||
supportsClientAssertions=Supports client assertions
|
||||
supportsClientAssertionReuse=Allows client assertions to be re-used
|
||||
organizationsExplain=Manage your organizations and members.
|
||||
emptyOrganizations=No organizations
|
||||
emptyOrganizationsInstructions=There is no organization yet. Please create an organization and manage it.
|
||||
|
||||
@ -122,6 +122,9 @@ export const AdvancedSettings = ({
|
||||
const claimFilterRequired = filteredByClaim === "true";
|
||||
const isFeatureEnabled = useIsFeatureEnabled();
|
||||
const isTransientUsersEnabled = isFeatureEnabled(Feature.TransientUsers);
|
||||
const isClientAuthFederatedEnabled = isFeatureEnabled(
|
||||
Feature.ClientAuthFederated,
|
||||
);
|
||||
const transientUsers = useWatch({
|
||||
control,
|
||||
name: "config.doNotStoreUsers",
|
||||
@ -311,6 +314,18 @@ export const AdvancedSettings = ({
|
||||
field="config.caseSensitiveOriginalUsername"
|
||||
label="caseSensitiveOriginalUsername"
|
||||
/>
|
||||
{isClientAuthFederatedEnabled && isOIDC && (
|
||||
<>
|
||||
<SwitchField
|
||||
field="config.supportsClientAssertions"
|
||||
label="supportsClientAssertions"
|
||||
/>
|
||||
<SwitchField
|
||||
field="config.supportsClientAssertionReuse"
|
||||
label="supportsClientAssertionReuse"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -17,6 +17,7 @@ export enum Feature {
|
||||
QuickTheme = "QUICK_THEME",
|
||||
StandardTokenExchangeV2 = "TOKEN_EXCHANGE_STANDARD_V2",
|
||||
Passkeys = "PASSKEYS",
|
||||
ClientAuthFederated = "CLIENT_AUTH_FEDERATED",
|
||||
}
|
||||
|
||||
export default function useIsFeatureEnabled() {
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
public interface ClientAssertionContext {
|
||||
|
||||
String getAssertionType();
|
||||
JWSInput getJwsInput();
|
||||
JsonWebToken getToken();
|
||||
ClientModel getClient();
|
||||
boolean isFailure();
|
||||
String getError();
|
||||
|
||||
boolean failure(String error);
|
||||
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
public interface ClientAssertionIdentityProvider {
|
||||
|
||||
boolean verifyClientAssertion(ClientAssertionContext context);
|
||||
|
||||
}
|
||||
@ -506,6 +506,15 @@ public class DefaultAuthenticationFlows {
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
if (Profile.isFeatureEnabled(Feature.CLIENT_AUTH_FEDERATED)) {
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(clients.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||
execution.setAuthenticator("federated-jwt");
|
||||
execution.setPriority(50);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
}
|
||||
}
|
||||
|
||||
public static void firstBrokerLoginFlow(RealmModel realm, boolean migrate) {
|
||||
|
||||
@ -0,0 +1,209 @@
|
||||
package org.keycloak.authentication.authenticators.client;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.ClientAuthenticationFlowContext;
|
||||
import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
|
||||
import org.keycloak.broker.provider.ClientAssertionContext;
|
||||
import org.keycloak.broker.provider.ClientAssertionIdentityProvider;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
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.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator implements EnvironmentDependentProviderFactory {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(FederatedJWTClientAuthenticator.class);
|
||||
|
||||
public static final String PROVIDER_ID = "federated-jwt";
|
||||
|
||||
public static final String JWT_CREDENTIAL_ISSUER_KEY = "jwt.credential.issuer";
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG = List.of(
|
||||
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);
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
// TODO Should share code with JWTClientAuthenticator/JWTClientValidator rather than duplicating, but that requires quite a bit of refactoring
|
||||
public void authenticateClient(ClientAuthenticationFlowContext context) {
|
||||
try {
|
||||
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
|
||||
String clientAssertionType = params.getFirst(OAuth2Constants.CLIENT_ASSERTION_TYPE);
|
||||
if (!SUPPORTED_ASSERTION_TYPES.contains(clientAssertionType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String clientAssertion = params.getFirst(OAuth2Constants.CLIENT_ASSERTION);
|
||||
if (clientAssertion == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
JWSInput jws = new JWSInput(clientAssertion);
|
||||
JsonWebToken token = jws.readJsonContent(JsonWebToken.class);
|
||||
context.getEvent().detail(Details.CLIENT_ASSERTION_ID, token.getId());
|
||||
context.getEvent().detail(Details.CLIENT_ASSERTION_ISSUER, token.getIssuer());
|
||||
|
||||
ClientModel client = lookupClient(context, token.getSubject());
|
||||
if (client == null) return;
|
||||
|
||||
ClientAssertionIdentityProvider identityProvider = lookupIdentityProvider(context, client);
|
||||
|
||||
ClientAssertionContext clientAssertionContext = new DefaultClientAssertionContext(client, clientAssertionType, jws, token);
|
||||
if (identityProvider.verifyClientAssertion(clientAssertionContext)) {
|
||||
context.success();
|
||||
} else {
|
||||
LOGGER.warnv("Failed to authenticate client: {0}", clientAssertionContext.getError());
|
||||
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Authentication failed", e);
|
||||
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
|
||||
private ClientModel lookupClient(ClientAuthenticationFlowContext context, String subject) {
|
||||
ClientModel client = context.getRealm().getClientByClientId(subject);
|
||||
if (client == null) {
|
||||
context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND);
|
||||
return null;
|
||||
}
|
||||
if (!client.isEnabled()) {
|
||||
context.failure(AuthenticationFlowError.CLIENT_DISABLED);
|
||||
return null;
|
||||
}
|
||||
context.setClient(client);
|
||||
context.getEvent().client(client);
|
||||
context.getEvent().detail(Details.CLIENT_ASSERTION_SUB, subject);
|
||||
return client;
|
||||
}
|
||||
|
||||
private ClientAssertionIdentityProvider lookupIdentityProvider(ClientAuthenticationFlowContext context, ClientModel client) {
|
||||
String idpAlias = client.getAttribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY);
|
||||
IdentityProvider<?> identityProvider = IdentityBrokerService.getIdentityProvider(context.getSession(), idpAlias);
|
||||
if (identityProvider instanceof ClientAssertionIdentityProvider clientAssertionProvider) {
|
||||
return clientAssertionProvider;
|
||||
} else {
|
||||
throw new RuntimeException("Provider does not support client assertions");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Signed JWT - Federated";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Validates client based on signed JWT issued and signed by an external identity provider";
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return ConfigurableAuthenticatorFactory.REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
|
||||
return CONFIG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAdapterConfiguration(ClientModel client) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.CLIENT_AUTH_FEDERATED);
|
||||
}
|
||||
|
||||
private static class DefaultClientAssertionContext implements ClientAssertionContext {
|
||||
|
||||
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) {
|
||||
this.client = client;
|
||||
this.assertionType = assertionType;
|
||||
this.jwsInput = jwsInput;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAssertionType() {
|
||||
return assertionType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JWSInput getJwsInput() {
|
||||
return jwsInput;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWebToken getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFailure() {
|
||||
return error != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failure(String error) {
|
||||
this.error = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -133,7 +133,8 @@ public class JWTClientValidator {
|
||||
}
|
||||
|
||||
if (!clientId.equals(token.getIssuer())) {
|
||||
throw new RuntimeException("Issuer mismatch. The issuer should match the subject");
|
||||
logger.debug("Token not issued by client, ignoring.");
|
||||
return false;
|
||||
}
|
||||
|
||||
String clientIdParam = params.getFirst(OAuth2Constants.CLIENT_ID);
|
||||
|
||||
@ -17,16 +17,27 @@
|
||||
package org.keycloak.broker.oidc;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
|
||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.ClientAssertionContext;
|
||||
import org.keycloak.broker.provider.ClientAssertionIdentityProvider;
|
||||
import org.keycloak.broker.provider.ExchangeExternalToken;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.common.util.Time;
|
||||
@ -50,6 +61,7 @@ import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.SingleUseObjectProvider;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
@ -59,6 +71,7 @@ import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
@ -68,16 +81,6 @@ import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
import org.keycloak.vault.VaultStringSecret;
|
||||
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
@ -90,7 +93,7 @@ import java.util.Optional;
|
||||
/**
|
||||
* @author Pedro Igor
|
||||
*/
|
||||
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> implements ExchangeExternalToken {
|
||||
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> implements ExchangeExternalToken, ClientAssertionIdentityProvider {
|
||||
protected static final Logger logger = Logger.getLogger(OIDCIdentityProvider.class);
|
||||
|
||||
public static final String SCOPE_OPENID = "openid";
|
||||
@ -1039,4 +1042,74 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
|
||||
return Boolean.valueOf(emailVerified.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyClientAssertion(ClientAssertionContext context) {
|
||||
if (!Profile.isFeatureEnabled(Profile.Feature.CLIENT_AUTH_FEDERATED)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getConfig().isSupportsClientAssertions()) {
|
||||
return context.failure("Issuer does not support client assertions");
|
||||
}
|
||||
|
||||
if (!getConfig().isValidateSignature()) {
|
||||
return context.failure("Signature validation not enabled for issuer");
|
||||
}
|
||||
|
||||
if (!context.getAssertionType().equals(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JWSInput jws = context.getJwsInput();
|
||||
JsonWebToken token = context.getToken();
|
||||
ClientModel client = context.getClient();
|
||||
|
||||
if (!verify(jws)) {
|
||||
return context.failure("Invalid signature");
|
||||
}
|
||||
|
||||
if (token.getIssuer() == null) {
|
||||
return context.failure("Token issuer required");
|
||||
}
|
||||
|
||||
if (!token.getIssuer().equals(getConfig().getIssuer())) {
|
||||
return context.failure("Invalid token issuer");
|
||||
}
|
||||
|
||||
String expectedAudience = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), session.getContext().getRealm().getName());
|
||||
int allowedClockSkew = getConfig().getAllowedClockSkew();
|
||||
|
||||
if (token.getExp() == null || token.getExp() <= 0) {
|
||||
return context.failure("Token does not contain an expiration");
|
||||
}
|
||||
|
||||
if (!(token.getAudience().length == 1 && token.getAudience()[0].equals(expectedAudience))) {
|
||||
return context.failure("Invalid audience");
|
||||
}
|
||||
|
||||
if (!token.isActive(allowedClockSkew)) {
|
||||
return context.failure("Token not active");
|
||||
}
|
||||
if (token.getIat() != null && token.getIat() > 0 && token.getIat() - allowedClockSkew > Time.currentTime()) {
|
||||
return context.failure("Token was issued in the future");
|
||||
}
|
||||
|
||||
if (!(getConfig().isSupportsClientAssertionReuse())) {
|
||||
if (token.getId() == null) {
|
||||
return context.failure("Token id required");
|
||||
}
|
||||
SingleUseObjectProvider singeUseCache = session.singleUseObjects();
|
||||
long lifespanInSecs = token.getExp() + allowedClockSkew - Time.currentTime();
|
||||
if (singeUseCache.putIfAbsent(token.getId(), lifespanInSecs)) {
|
||||
logger.tracef("Added token '%s' to single-use cache. Lifespan: %d seconds, client: %s", token.getId(), lifespanInSecs, client.getClientId());
|
||||
} else {
|
||||
logger.warnf("Token '%s' already used when authenticating client '%s'.", token.getId(), client.getClientId());
|
||||
return context.failure("Token already used");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -33,6 +33,8 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
||||
public static final String VALIDATE_SIGNATURE = "validateSignature";
|
||||
public static final String IS_ACCESS_TOKEN_JWT = "isAccessTokenJWT";
|
||||
public static final String ISSUER = "issuer";
|
||||
public static final String SUPPORTS_CLIENT_ASSERTIONS = "supportsClientAssertions";
|
||||
public static final String SUPPORTS_CLIENT_ASSERTION_REUSE = "supportsClientAssertionReuse";
|
||||
|
||||
public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
|
||||
super(identityProviderModel);
|
||||
@ -180,6 +182,14 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSupportsClientAssertions() {
|
||||
return Boolean.parseBoolean(getConfig().get(SUPPORTS_CLIENT_ASSERTIONS));
|
||||
}
|
||||
|
||||
public boolean isSupportsClientAssertionReuse() {
|
||||
return Boolean.parseBoolean(getConfig().get(SUPPORTS_CLIENT_ASSERTION_REUSE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(RealmModel realm) {
|
||||
super.validate(realm);
|
||||
|
||||
@ -18,4 +18,5 @@
|
||||
org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator
|
||||
org.keycloak.authentication.authenticators.client.JWTClientAuthenticator
|
||||
org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator
|
||||
org.keycloak.authentication.authenticators.client.X509ClientAuthenticator
|
||||
org.keycloak.authentication.authenticators.client.X509ClientAuthenticator
|
||||
org.keycloak.authentication.authenticators.client.FederatedJWTClientAuthenticator
|
||||
@ -0,0 +1,113 @@
|
||||
package org.keycloak.testframework.oauth;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.ECDSASignatureSignerContext;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.crypto.def.DefaultCryptoProvider;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKBuilder;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
|
||||
public class OAuthIdentityProvider {
|
||||
|
||||
private final HttpServer httpServer;
|
||||
|
||||
private final OAuthIdentityProviderKeys keys;
|
||||
|
||||
public OAuthIdentityProvider(HttpServer httpServer) {
|
||||
if (!CryptoIntegration.isInitialised()) {
|
||||
CryptoIntegration.setProvider(new DefaultCryptoProvider());
|
||||
}
|
||||
|
||||
this.httpServer = httpServer;
|
||||
httpServer.createContext("/idp/jwks", new JwksHttpHandler());
|
||||
|
||||
keys = new OAuthIdentityProviderKeys();
|
||||
}
|
||||
|
||||
public String encodeToken(JsonWebToken token) {
|
||||
return encodeToken(token, keys);
|
||||
}
|
||||
|
||||
public String encodeToken(JsonWebToken token, OAuthIdentityProviderKeys keys) {
|
||||
return new JWSBuilder().type("JWT").jsonContent(token).sign(new ECDSASignatureSignerContext(keys.getKeyWrapper()));
|
||||
}
|
||||
|
||||
public OAuthIdentityProviderKeys createKeys() {
|
||||
return new OAuthIdentityProviderKeys();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
httpServer.removeContext("/idp/jwks");
|
||||
}
|
||||
|
||||
public class JwksHttpHandler implements HttpHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(200, keys.getJwksString().length());
|
||||
OutputStream outputStream = exchange.getResponseBody();
|
||||
outputStream.write(keys.getJwksString().getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class OAuthIdentityProviderKeys {
|
||||
|
||||
private final KeyWrapper keyWrapper;
|
||||
|
||||
private final String jwksString;
|
||||
|
||||
public OAuthIdentityProviderKeys() {
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
|
||||
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
|
||||
keyPairGenerator.initialize(ecSpec);
|
||||
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
JWK jwk = JWKBuilder.create().ec(keyPair.getPublic());
|
||||
jwk.setAlgorithm("ES256");
|
||||
jwk.setPublicKeyUse(KeyUse.SIG.getSpecName());
|
||||
|
||||
JSONWebKeySet jwks = new JSONWebKeySet();
|
||||
jwks.setKeys(new JWK[] { jwk });
|
||||
jwksString = JsonSerialization.writeValueAsString(jwks);
|
||||
|
||||
keyWrapper = new KeyWrapper();
|
||||
keyWrapper.setKid(jwk.getKeyId());
|
||||
keyWrapper.setPublicKey(keyPair.getPublic());
|
||||
keyWrapper.setPrivateKey(keyPair.getPrivate());
|
||||
keyWrapper.setUse(KeyUse.SIG);
|
||||
keyWrapper.setAlgorithm(Algorithm.ES256);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public KeyWrapper getKeyWrapper() {
|
||||
return keyWrapper;
|
||||
}
|
||||
|
||||
public String getJwksString() {
|
||||
return jwksString;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package org.keycloak.testframework.oauth;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.keycloak.testframework.injection.InstanceContext;
|
||||
import org.keycloak.testframework.injection.RequestedInstance;
|
||||
import org.keycloak.testframework.injection.Supplier;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
|
||||
|
||||
public class OAuthIdentityProviderSupplier implements Supplier<OAuthIdentityProvider, InjectOAuthIdentityProvider> {
|
||||
|
||||
@Override
|
||||
public OAuthIdentityProvider getValue(InstanceContext<OAuthIdentityProvider, InjectOAuthIdentityProvider> instanceContext) {
|
||||
HttpServer httpServer = instanceContext.getDependency(HttpServer.class);
|
||||
return new OAuthIdentityProvider(httpServer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean compatible(InstanceContext<OAuthIdentityProvider, InjectOAuthIdentityProvider> a, RequestedInstance<OAuthIdentityProvider, InjectOAuthIdentityProvider> b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@ -9,7 +9,7 @@ public class OAuthTestFrameworkExtension implements TestFrameworkExtension {
|
||||
|
||||
@Override
|
||||
public List<Supplier<?, ?>> suppliers() {
|
||||
return List.of(new OAuthClientSupplier(), new TestAppSupplier());
|
||||
return List.of(new OAuthClientSupplier(), new TestAppSupplier(), new OAuthIdentityProviderSupplier());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
package org.keycloak.testframework.oauth.annotations;
|
||||
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface InjectOAuthIdentityProvider {
|
||||
|
||||
LifeCycle lifecycle() default LifeCycle.GLOBAL;
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package org.keycloak.tests.client.authentication.external;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfig;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
|
||||
public class ClientAuthIdpServerConfig implements KeycloakServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return config.features(Profile.Feature.CLIENT_AUTH_FEDERATED);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package org.keycloak.tests.client.authentication.external;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.authentication.authenticators.client.FederatedJWTClientAuthenticator;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.realm.ClientConfig;
|
||||
import org.keycloak.testframework.realm.ClientConfigBuilder;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@KeycloakIntegrationTest(config = ClientAuthIdpServerConfig.class)
|
||||
public class FederatedClientAuthFromKeycloakTest {
|
||||
|
||||
@InjectRealm(config = InternalRealmConfig.class)
|
||||
ManagedRealm internalRealm;
|
||||
|
||||
@InjectRealm(ref = "external")
|
||||
ManagedRealm externalRealm;
|
||||
|
||||
@InjectOAuthClient(config = InternalClientConfig.class)
|
||||
OAuthClient internalOAuthClient;
|
||||
|
||||
@InjectOAuthClient(ref = "external", realmRef = "external", config = ExternalClientConfig.class)
|
||||
OAuthClient externalOAuthClient;
|
||||
|
||||
@Test
|
||||
public void testValidToken() {
|
||||
String externalClientAssertion = externalOAuthClient.doClientCredentialsGrantAccessTokenRequest().getAccessToken();
|
||||
|
||||
AccessTokenResponse send = internalOAuthClient.clientCredentialsGrantRequest().clientJwt(externalClientAssertion).send();
|
||||
Assertions.assertTrue(send.isSuccess());
|
||||
}
|
||||
|
||||
public static class InternalRealmConfig implements RealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
return realm.identityProvider(
|
||||
IdentityProviderBuilder.create()
|
||||
.providerId(OIDCIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias("external")
|
||||
.setAttribute("issuer", "http://localhost:8080/realms/external")
|
||||
.setAttribute(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTIONS, "true")
|
||||
.setAttribute(OIDCIdentityProviderConfig.USE_JWKS_URL, "true")
|
||||
.setAttribute(OIDCIdentityProviderConfig.JWKS_URL, "http://localhost:8080/realms/external/protocol/openid-connect/certs")
|
||||
.setAttribute(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, "true")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
public static class InternalClientConfig implements ClientConfig {
|
||||
|
||||
@Override
|
||||
public ClientConfigBuilder configure(ClientConfigBuilder client) {
|
||||
return client.clientId("myclient")
|
||||
.serviceAccountsEnabled(true)
|
||||
.authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, "external");
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExternalClientConfig implements ClientConfig {
|
||||
|
||||
@Override
|
||||
public ClientConfigBuilder configure(ClientConfigBuilder client) {
|
||||
ProtocolMapperRepresentation subMapper = new ProtocolMapperRepresentation();
|
||||
subMapper.setName("fixed-sub");
|
||||
subMapper.setProtocol("openid-connect");
|
||||
subMapper.setProtocolMapper("oidc-hardcoded-claim-mapper");
|
||||
subMapper.getConfig().put("claim.name", "sub");
|
||||
subMapper.getConfig().put("claim.value", "myclient");
|
||||
subMapper.getConfig().put("access.token.claim", "true");
|
||||
|
||||
ProtocolMapperRepresentation audMapper = new ProtocolMapperRepresentation();
|
||||
audMapper.setName("fixed-audience");
|
||||
audMapper.setProtocol("openid-connect");
|
||||
audMapper.setProtocolMapper("oidc-audience-mapper");
|
||||
audMapper.getConfig().put("included.custom.audience", "http://localhost:8080/realms/default");
|
||||
audMapper.getConfig().put("access.token.claim", "true");
|
||||
|
||||
return client.clientId("myclient")
|
||||
.defaultClientScopes()
|
||||
.serviceAccountsEnabled(true)
|
||||
.secret("mysecret")
|
||||
.protocolMappers(List.of(subMapper, audMapper));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,214 @@
|
||||
package org.keycloak.tests.client.authentication.external;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.IdentityProviderResource;
|
||||
import org.keycloak.authentication.authenticators.client.FederatedJWTClientAuthenticator;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectClient;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.realm.ClientConfig;
|
||||
import org.keycloak.testframework.realm.ClientConfigBuilder;
|
||||
import org.keycloak.testframework.realm.ManagedClient;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@KeycloakIntegrationTest(config = ClientAuthIdpServerConfig.class)
|
||||
public class FederatedClientAuthTest {
|
||||
|
||||
private static final String IDP_ALIAS = "myidp";
|
||||
|
||||
private static final String CLIENT_ID = "myclient";
|
||||
|
||||
@InjectRealm(config = ExernalClientAuthRealmConfig.class)
|
||||
protected ManagedRealm realm;
|
||||
|
||||
@InjectClient(config = ExernalClientAuthClientConfig.class)
|
||||
protected ManagedClient client;
|
||||
|
||||
@InjectOAuthClient
|
||||
OAuthClient oAuthClient;
|
||||
|
||||
@InjectOAuthIdentityProvider
|
||||
OAuthIdentityProvider identityProvider;
|
||||
|
||||
@Test
|
||||
public void testInvalidSignature() {
|
||||
OAuthIdentityProvider.OAuthIdentityProviderKeys keys = identityProvider.createKeys();
|
||||
String jws = identityProvider.encodeToken(createDefaultToken(), keys);
|
||||
Assertions.assertFalse(doClientGrant(jws));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidAssertionType() {
|
||||
String jws = identityProvider.encodeToken(createDefaultToken());
|
||||
AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws, "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe").send();
|
||||
Assertions.assertFalse(response.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidToken() {
|
||||
Assertions.assertTrue(doClientGrant(createDefaultToken()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidIssuer() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
token.issuer("http://invalid");
|
||||
Assertions.assertFalse(doClientGrant(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingIssuer() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
token.issuer(null);
|
||||
Assertions.assertFalse(doClientGrant(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidSub() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
token.subject("invalid");
|
||||
Assertions.assertFalse(doClientGrant(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidAud() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
token.audience("invalid");
|
||||
Assertions.assertFalse(doClientGrant(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleAud() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
token.audience(token.getAudience()[0], "invalid");
|
||||
Assertions.assertFalse(doClientGrant(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidNbf() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
token.nbf((long) (Time.currentTime() + 30));
|
||||
Assertions.assertFalse(doClientGrant(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingJti() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
token.id(null);
|
||||
Assertions.assertFalse(doClientGrant(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpired() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
token.exp((long) (Time.currentTime() - 30));
|
||||
Assertions.assertFalse(doClientGrant(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingExp() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
token.exp(null);
|
||||
Assertions.assertFalse(doClientGrant(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReuseNotPermitted() {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
Assertions.assertTrue(doClientGrant(token));
|
||||
Assertions.assertFalse(doClientGrant(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReusePermitted() {
|
||||
IdentityProviderResource idp = realm.admin().identityProviders().get(IDP_ALIAS);
|
||||
IdentityProviderRepresentation rep = idp.toRepresentation();
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTION_REUSE, "true");
|
||||
idp.update(rep);
|
||||
|
||||
JsonWebToken token = createDefaultToken();
|
||||
Assertions.assertTrue(doClientGrant(token));
|
||||
Assertions.assertTrue(doClientGrant(token));
|
||||
|
||||
rep.getConfig().remove(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTION_REUSE);
|
||||
idp.update(rep);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientAssertionsNotSupported() {
|
||||
IdentityProviderResource idp = realm.admin().identityProviders().get(IDP_ALIAS);
|
||||
IdentityProviderRepresentation rep = idp.toRepresentation();
|
||||
rep.getConfig().remove(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTIONS);
|
||||
idp.update(rep);
|
||||
|
||||
Assertions.assertFalse(doClientGrant(createDefaultToken()));
|
||||
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTIONS, "true");
|
||||
idp.update(rep);
|
||||
}
|
||||
|
||||
private boolean doClientGrant(JsonWebToken token) {
|
||||
String jws = identityProvider.encodeToken(token);
|
||||
return doClientGrant(jws);
|
||||
}
|
||||
|
||||
private boolean doClientGrant(String jws) {
|
||||
AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws).send();
|
||||
return response.isSuccess();
|
||||
}
|
||||
|
||||
private JsonWebToken createDefaultToken() {
|
||||
JsonWebToken token = new JsonWebToken();
|
||||
token.id(UUID.randomUUID().toString());
|
||||
token.issuer("http://127.0.0.1:8500");
|
||||
token.audience(oAuthClient.getEndpoints().getIssuer());
|
||||
token.exp((long) (Time.currentTime() + 300));
|
||||
token.subject(CLIENT_ID);
|
||||
return token;
|
||||
}
|
||||
|
||||
public static class ExernalClientAuthRealmConfig implements RealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
return realm.identityProvider(
|
||||
IdentityProviderBuilder.create()
|
||||
.providerId(OIDCIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias(IDP_ALIAS)
|
||||
.setAttribute("issuer", "http://127.0.0.1:8500")
|
||||
.setAttribute(OIDCIdentityProviderConfig.USE_JWKS_URL, "true")
|
||||
.setAttribute(OIDCIdentityProviderConfig.JWKS_URL, "http://127.0.0.1:8500/idp/jwks")
|
||||
.setAttribute(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, "true")
|
||||
.setAttribute(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTIONS, "true")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExernalClientAuthClientConfig implements ClientConfig {
|
||||
|
||||
@Override
|
||||
public ClientConfigBuilder configure(ClientConfigBuilder client) {
|
||||
return client.clientId(CLIENT_ID)
|
||||
.serviceAccountsEnabled(true)
|
||||
.authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -24,6 +24,10 @@ public abstract class AbstractHttpPostRequest<T, R> {
|
||||
|
||||
protected String clientSecret;
|
||||
|
||||
protected String clientAssertion;
|
||||
|
||||
protected String clientAssertionType;
|
||||
|
||||
protected HttpPost post;
|
||||
|
||||
protected Map<String, String> headers = new HashMap<>();
|
||||
@ -70,6 +74,18 @@ public abstract class AbstractHttpPostRequest<T, R> {
|
||||
return request();
|
||||
}
|
||||
|
||||
public T clientJwt(String clientAssertion) {
|
||||
this.clientAssertion = clientAssertion;
|
||||
this.clientAssertionType = OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT;
|
||||
return request();
|
||||
}
|
||||
|
||||
public T clientJwt(String clientAssertion, String clientAssertionType) {
|
||||
this.clientAssertion = clientAssertion;
|
||||
this.clientAssertionType = clientAssertionType;
|
||||
return request();
|
||||
}
|
||||
|
||||
protected void header(String name, String value) {
|
||||
if (value != null) {
|
||||
headers.put(name, value);
|
||||
@ -86,10 +102,13 @@ public abstract class AbstractHttpPostRequest<T, R> {
|
||||
String clientId = this.clientId != null ? this.clientId : client.config().getClientId();
|
||||
String clientSecret = this.clientId != null ? this.clientSecret : client.config().getClientSecret();
|
||||
|
||||
if (clientSecret != null) {
|
||||
if (clientAssertion != null && clientAssertionType != null) {
|
||||
parameter("client_assertion_type", clientAssertionType);
|
||||
parameter("client_assertion", clientAssertion);
|
||||
} else if (clientSecret != null) {
|
||||
String authorization = BasicAuthHelper.RFC6749.createHeader(clientId, clientSecret);
|
||||
header("Authorization", authorization);
|
||||
} else {
|
||||
} else if (clientId != null) {
|
||||
parameter("client_id", clientId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,10 @@ public class Endpoints {
|
||||
return asString(getBase().path(RealmsResource.class).path("{realm}/.well-known/openid-configuration"));
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return asString(getBase().path(RealmsResource.class).path("{realm}"));
|
||||
}
|
||||
|
||||
public String getAuthorization() {
|
||||
return asString(OIDCLoginProtocolService.authUrl(getBase()));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user