KEYCLOAK-5082 : Add new redirect-rewrite-rule parameters for the adapters (#4255)
* add rewrite rule config property * add subsystem support for redirect rewrite * update deployment unit test * add license headers * Optimize rewrite method
This commit is contained in:
parent
620bea3553
commit
500a21685f
17 changed files with 395 additions and 8 deletions
|
@ -91,6 +91,8 @@ public class KeycloakDeployment {
|
||||||
// https://tools.ietf.org/html/rfc7636
|
// https://tools.ietf.org/html/rfc7636
|
||||||
protected boolean pkce = false;
|
protected boolean pkce = false;
|
||||||
protected boolean ignoreOAuthQueryParameter;
|
protected boolean ignoreOAuthQueryParameter;
|
||||||
|
|
||||||
|
protected Map<String, String> redirectRewriteRules;
|
||||||
|
|
||||||
public KeycloakDeployment() {
|
public KeycloakDeployment() {
|
||||||
}
|
}
|
||||||
|
@ -446,4 +448,14 @@ public class KeycloakDeployment {
|
||||||
public boolean isOAuthQueryParameterEnabled() {
|
public boolean isOAuthQueryParameterEnabled() {
|
||||||
return !this.ignoreOAuthQueryParameter;
|
return !this.ignoreOAuthQueryParameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getRedirectRewriteRules() {
|
||||||
|
return redirectRewriteRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRewriteRedirectRules(Map<String, String> redirectRewriteRules) {
|
||||||
|
this.redirectRewriteRules = redirectRewriteRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,7 @@ public class KeycloakDeploymentBuilder {
|
||||||
deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests());
|
deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests());
|
||||||
deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl());
|
deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl());
|
||||||
deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter());
|
deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter());
|
||||||
|
deployment.setRewriteRedirectRules(adapterConfig.getRedirectRewriteRules());
|
||||||
|
|
||||||
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
|
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
|
||||||
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");
|
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");
|
||||||
|
|
|
@ -25,7 +25,6 @@ import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
import org.keycloak.adapters.spi.AuthOutcome;
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.Encode;
|
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
import org.keycloak.common.util.UriUtils;
|
import org.keycloak.common.util.UriUtils;
|
||||||
import org.keycloak.constants.AdapterConstants;
|
import org.keycloak.constants.AdapterConstants;
|
||||||
|
@ -38,7 +37,10 @@ import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -141,6 +143,7 @@ public class OAuthRequestAuthenticator {
|
||||||
protected String getRedirectUri(String state) {
|
protected String getRedirectUri(String state) {
|
||||||
String url = getRequestUrl();
|
String url = getRequestUrl();
|
||||||
log.debugf("callback uri: %s", url);
|
log.debugf("callback uri: %s", url);
|
||||||
|
|
||||||
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
|
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
|
||||||
int port = sslRedirectPort();
|
int port = sslRedirectPort();
|
||||||
if (port < 0) {
|
if (port < 0) {
|
||||||
|
@ -170,7 +173,7 @@ public class OAuthRequestAuthenticator {
|
||||||
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
|
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
|
||||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||||
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
||||||
.queryParam(OAuth2Constants.REDIRECT_URI, url)
|
.queryParam(OAuth2Constants.REDIRECT_URI, rewrittenRedirectUri(url))
|
||||||
.queryParam(OAuth2Constants.STATE, state)
|
.queryParam(OAuth2Constants.STATE, state)
|
||||||
.queryParam("login", "true");
|
.queryParam("login", "true");
|
||||||
if(loginHint != null && loginHint.length() > 0){
|
if(loginHint != null && loginHint.length() > 0){
|
||||||
|
@ -320,10 +323,11 @@ public class OAuthRequestAuthenticator {
|
||||||
|
|
||||||
AccessTokenResponse tokenResponse = null;
|
AccessTokenResponse tokenResponse = null;
|
||||||
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
|
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For COOKIE store we don't have httpSessionId and single sign-out won't be available
|
// For COOKIE store we don't have httpSessionId and single sign-out won't be available
|
||||||
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null;
|
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null;
|
||||||
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId);
|
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, rewrittenRedirectUri(strippedOauthParametersRequestUri), httpSessionId);
|
||||||
} catch (ServerRequest.HttpFailure failure) {
|
} catch (ServerRequest.HttpFailure failure) {
|
||||||
log.error("failed to turn code into token");
|
log.error("failed to turn code into token");
|
||||||
log.error("status from server: " + failure.getStatus());
|
log.error("status from server: " + failure.getStatus());
|
||||||
|
@ -375,6 +379,23 @@ public class OAuthRequestAuthenticator {
|
||||||
.replaceQueryParam(OAuth2Constants.STATE, null);
|
.replaceQueryParam(OAuth2Constants.STATE, null);
|
||||||
return builder.build().toString();
|
return builder.build().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String rewrittenRedirectUri(String originalUri) {
|
||||||
|
Map<String, String> rewriteRules = deployment.getRedirectRewriteRules();
|
||||||
|
if(rewriteRules != null && !rewriteRules.isEmpty()) {
|
||||||
|
try {
|
||||||
|
URL url = new URL(originalUri);
|
||||||
|
Map.Entry<String, String> rule = rewriteRules.entrySet().iterator().next();
|
||||||
|
StringBuilder redirectUriBuilder = new StringBuilder(url.getProtocol());
|
||||||
|
redirectUriBuilder.append("://"+ url.getAuthority());
|
||||||
|
redirectUriBuilder.append(url.getPath().replaceFirst(rule.getKey(), rule.getValue()));
|
||||||
|
return redirectUriBuilder.toString();
|
||||||
|
} catch (MalformedURLException ex) {
|
||||||
|
log.error("Not a valid request url");
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalUri;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ public class KeycloakDeploymentBuilderTest {
|
||||||
assertEquals(10, deployment.getTokenMinimumTimeToLive());
|
assertEquals(10, deployment.getTokenMinimumTimeToLive());
|
||||||
assertEquals(20, deployment.getMinTimeBetweenJwksRequests());
|
assertEquals(20, deployment.getMinTimeBetweenJwksRequests());
|
||||||
assertEquals(120, deployment.getPublicKeyCacheTtl());
|
assertEquals(120, deployment.getPublicKeyCacheTtl());
|
||||||
|
assertEquals("/api/$1", deployment.getRedirectRewriteRules().get("^/wsmaster/api/(.*)$"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -33,5 +33,8 @@
|
||||||
"token-minimum-time-to-live": 10,
|
"token-minimum-time-to-live": 10,
|
||||||
"min-time-between-jwks-requests": 20,
|
"min-time-between-jwks-requests": 20,
|
||||||
"public-key-cache-ttl": 120,
|
"public-key-cache-ttl": 120,
|
||||||
"ignore-oauth-query-parameter": true
|
"ignore-oauth-query-parameter": true,
|
||||||
|
"redirect-rewrite-rules" : {
|
||||||
|
"^/wsmaster/api/(.*)$" : "/api/$1"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -37,6 +37,8 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD
|
||||||
public final class KeycloakAdapterConfigService {
|
public final class KeycloakAdapterConfigService {
|
||||||
|
|
||||||
private static final String CREDENTIALS_JSON_NAME = "credentials";
|
private static final String CREDENTIALS_JSON_NAME = "credentials";
|
||||||
|
|
||||||
|
private static final String REDIRECT_REWRITE_RULE_JSON_NAME = "redirect-rewrite-rule";
|
||||||
|
|
||||||
private static final KeycloakAdapterConfigService INSTANCE = new KeycloakAdapterConfigService();
|
private static final KeycloakAdapterConfigService INSTANCE = new KeycloakAdapterConfigService();
|
||||||
|
|
||||||
|
@ -129,6 +131,56 @@ public final class KeycloakAdapterConfigService {
|
||||||
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
|
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
|
||||||
return deployment.get(CREDENTIALS_JSON_NAME);
|
return deployment.get(CREDENTIALS_JSON_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addRedirectRewriteRule(ModelNode operation, ModelNode model) {
|
||||||
|
ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
|
||||||
|
if (!redirectRewritesRules.isDefined()) {
|
||||||
|
redirectRewritesRules = new ModelNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
String redirectRewriteRuleName = redirectRewriteRule(operation);
|
||||||
|
if (!redirectRewriteRuleName.contains(".")) {
|
||||||
|
redirectRewritesRules.get(redirectRewriteRuleName).set(model.get("value").asString());
|
||||||
|
} else {
|
||||||
|
String[] parts = redirectRewriteRuleName.split("\\.");
|
||||||
|
String provider = parts[0];
|
||||||
|
String property = parts[1];
|
||||||
|
ModelNode redirectRewriteRule = redirectRewritesRules.get(provider);
|
||||||
|
if (!redirectRewriteRule.isDefined()) {
|
||||||
|
redirectRewriteRule = new ModelNode();
|
||||||
|
}
|
||||||
|
redirectRewriteRule.get(property).set(model.get("value").asString());
|
||||||
|
redirectRewritesRules.set(provider, redirectRewriteRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
|
||||||
|
deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME).set(redirectRewritesRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeRedirectRewriteRule(ModelNode operation) {
|
||||||
|
ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
|
||||||
|
if (!redirectRewritesRules.isDefined()) {
|
||||||
|
throw new RuntimeException("Can not remove redirect rewrite rule. No rules defined for deployment in op " + operation.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
String ruleName = credentialNameFromOp(operation);
|
||||||
|
redirectRewritesRules.remove(ruleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateRedirectRewriteRule(ModelNode operation, String attrName, ModelNode resolvedValue) {
|
||||||
|
ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation);
|
||||||
|
if (!redirectRewritesRules.isDefined()) {
|
||||||
|
throw new RuntimeException("Can not update redirect rewrite rule. No rules defined for deployment in op " + operation.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
String ruleName = credentialNameFromOp(operation);
|
||||||
|
redirectRewritesRules.get(ruleName).set(resolvedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModelNode redirectRewriteRuleFromOp(ModelNode operation) {
|
||||||
|
ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation));
|
||||||
|
return deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
private String realmNameFromOp(ModelNode operation) {
|
private String realmNameFromOp(ModelNode operation) {
|
||||||
return valueFromOpAddress(RealmDefinition.TAG_NAME, operation);
|
return valueFromOpAddress(RealmDefinition.TAG_NAME, operation);
|
||||||
|
@ -141,6 +193,10 @@ public final class KeycloakAdapterConfigService {
|
||||||
private String credentialNameFromOp(ModelNode operation) {
|
private String credentialNameFromOp(ModelNode operation) {
|
||||||
return valueFromOpAddress(CredentialDefinition.TAG_NAME, operation);
|
return valueFromOpAddress(CredentialDefinition.TAG_NAME, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String redirectRewriteRule(ModelNode operation) {
|
||||||
|
return valueFromOpAddress(RedirecRewritetRuleDefinition.TAG_NAME, operation);
|
||||||
|
}
|
||||||
|
|
||||||
private String valueFromOpAddress(String addrElement, ModelNode operation) {
|
private String valueFromOpAddress(String addrElement, ModelNode operation) {
|
||||||
String deploymentName = getValueOfAddrElement(operation.get(ADDRESS), addrElement);
|
String deploymentName = getValueOfAddrElement(operation.get(ADDRESS), addrElement);
|
||||||
|
|
|
@ -48,6 +48,7 @@ public class KeycloakExtension implements Extension {
|
||||||
static final RealmDefinition REALM_DEFINITION = new RealmDefinition();
|
static final RealmDefinition REALM_DEFINITION = new RealmDefinition();
|
||||||
static final SecureDeploymentDefinition SECURE_DEPLOYMENT_DEFINITION = new SecureDeploymentDefinition();
|
static final SecureDeploymentDefinition SECURE_DEPLOYMENT_DEFINITION = new SecureDeploymentDefinition();
|
||||||
static final CredentialDefinition CREDENTIAL_DEFINITION = new CredentialDefinition();
|
static final CredentialDefinition CREDENTIAL_DEFINITION = new CredentialDefinition();
|
||||||
|
static final RedirecRewritetRuleDefinition REDIRECT_RULE_DEFINITON = new RedirecRewritetRuleDefinition();
|
||||||
|
|
||||||
public static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) {
|
public static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) {
|
||||||
StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME);
|
StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME);
|
||||||
|
@ -77,6 +78,7 @@ public class KeycloakExtension implements Extension {
|
||||||
registration.registerSubModel(REALM_DEFINITION);
|
registration.registerSubModel(REALM_DEFINITION);
|
||||||
ManagementResourceRegistration secureDeploymentRegistration = registration.registerSubModel(SECURE_DEPLOYMENT_DEFINITION);
|
ManagementResourceRegistration secureDeploymentRegistration = registration.registerSubModel(SECURE_DEPLOYMENT_DEFINITION);
|
||||||
secureDeploymentRegistration.registerSubModel(CREDENTIAL_DEFINITION);
|
secureDeploymentRegistration.registerSubModel(CREDENTIAL_DEFINITION);
|
||||||
|
secureDeploymentRegistration.registerSubModel(REDIRECT_RULE_DEFINITON);
|
||||||
|
|
||||||
subsystem.registerXMLElementWriter(PARSER);
|
subsystem.registerXMLElementWriter(PARSER);
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,12 +96,17 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
||||||
PathElement.pathElement(SecureDeploymentDefinition.TAG_NAME, name));
|
PathElement.pathElement(SecureDeploymentDefinition.TAG_NAME, name));
|
||||||
addSecureDeployment.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
|
addSecureDeployment.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
|
||||||
List<ModelNode> credentialsToAdd = new ArrayList<ModelNode>();
|
List<ModelNode> credentialsToAdd = new ArrayList<ModelNode>();
|
||||||
|
List<ModelNode> redirectRulesToAdd = new ArrayList<ModelNode>();
|
||||||
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
|
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
|
||||||
String tagName = reader.getLocalName();
|
String tagName = reader.getLocalName();
|
||||||
if (tagName.equals(CredentialDefinition.TAG_NAME)) {
|
if (tagName.equals(CredentialDefinition.TAG_NAME)) {
|
||||||
readCredential(reader, addr, credentialsToAdd);
|
readCredential(reader, addr, credentialsToAdd);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (tagName.equals(RedirecRewritetRuleDefinition.TAG_NAME)) {
|
||||||
|
readRewriteRule(reader, addr, redirectRulesToAdd);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
SimpleAttributeDefinition def = SecureDeploymentDefinition.lookup(tagName);
|
SimpleAttributeDefinition def = SecureDeploymentDefinition.lookup(tagName);
|
||||||
if (def == null) throw new XMLStreamException("Unknown secure-deployment tag " + tagName);
|
if (def == null) throw new XMLStreamException("Unknown secure-deployment tag " + tagName);
|
||||||
|
@ -111,6 +116,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
||||||
// Must add credentials after the deployment is added.
|
// Must add credentials after the deployment is added.
|
||||||
resourcesToAdd.add(addSecureDeployment);
|
resourcesToAdd.add(addSecureDeployment);
|
||||||
resourcesToAdd.addAll(credentialsToAdd);
|
resourcesToAdd.addAll(credentialsToAdd);
|
||||||
|
resourcesToAdd.addAll(redirectRulesToAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void readCredential(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> credentialsToAdd) throws XMLStreamException {
|
public void readCredential(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> credentialsToAdd) throws XMLStreamException {
|
||||||
|
@ -149,6 +155,43 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void readRewriteRule(XMLExtendedStreamReader reader, PathAddress parent, List<ModelNode> rewriteRuleToToAdd) throws XMLStreamException {
|
||||||
|
String name = readNameAttribute(reader);
|
||||||
|
|
||||||
|
Map<String, String> values = new HashMap<>();
|
||||||
|
String textValue = null;
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
int next = reader.next();
|
||||||
|
if (next == CHARACTERS) {
|
||||||
|
// text value of redirect rule element
|
||||||
|
String text = reader.getText();
|
||||||
|
if (text == null || text.trim().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
textValue = text;
|
||||||
|
} else if (next == START_ELEMENT) {
|
||||||
|
String key = reader.getLocalName();
|
||||||
|
reader.next();
|
||||||
|
String value = reader.getText();
|
||||||
|
reader.next();
|
||||||
|
|
||||||
|
values.put(key, value);
|
||||||
|
} else if (next == END_ELEMENT) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textValue != null) {
|
||||||
|
ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name, textValue);
|
||||||
|
rewriteRuleToToAdd.add(addRedirectRule);
|
||||||
|
} else {
|
||||||
|
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||||
|
ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name + "." + entry.getKey(), entry.getValue());
|
||||||
|
rewriteRuleToToAdd.add(addRedirectRule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ModelNode getCredentialToAdd(PathAddress parent, String name, String value) {
|
private ModelNode getCredentialToAdd(PathAddress parent, String name, String value) {
|
||||||
ModelNode addCredential = new ModelNode();
|
ModelNode addCredential = new ModelNode();
|
||||||
|
@ -158,6 +201,15 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
||||||
addCredential.get(CredentialDefinition.VALUE.getName()).set(value);
|
addCredential.get(CredentialDefinition.VALUE.getName()).set(value);
|
||||||
return addCredential;
|
return addCredential;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ModelNode getRedirectRuleToAdd(PathAddress parent, String name, String value) {
|
||||||
|
ModelNode addRedirectRule = new ModelNode();
|
||||||
|
addRedirectRule.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD);
|
||||||
|
PathAddress addr = PathAddress.pathAddress(parent, PathElement.pathElement(RedirecRewritetRuleDefinition.TAG_NAME, name));
|
||||||
|
addRedirectRule.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode());
|
||||||
|
addRedirectRule.get(RedirecRewritetRuleDefinition.VALUE.getName()).set(value);
|
||||||
|
return addRedirectRule;
|
||||||
|
}
|
||||||
|
|
||||||
// expects that the current tag will have one single attribute called "name"
|
// expects that the current tag will have one single attribute called "name"
|
||||||
private String readNameAttribute(XMLExtendedStreamReader reader) throws XMLStreamException {
|
private String readNameAttribute(XMLExtendedStreamReader reader) throws XMLStreamException {
|
||||||
|
@ -219,6 +271,11 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
||||||
if (credentials.isDefined()) {
|
if (credentials.isDefined()) {
|
||||||
writeCredentials(writer, credentials);
|
writeCredentials(writer, credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModelNode redirectRewriteRule = deploymentElements.get(RedirecRewritetRuleDefinition.TAG_NAME);
|
||||||
|
if (redirectRewriteRule.isDefined()) {
|
||||||
|
writeRedirectRules(writer, redirectRewriteRule);
|
||||||
|
}
|
||||||
|
|
||||||
writer.writeEndElement();
|
writer.writeEndElement();
|
||||||
}
|
}
|
||||||
|
@ -265,6 +322,34 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
||||||
writer.writeEndElement();
|
writer.writeEndElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void writeRedirectRules(XMLExtendedStreamWriter writer, ModelNode redirectRules) throws XMLStreamException {
|
||||||
|
Map<String, Object> parsed = new LinkedHashMap<>();
|
||||||
|
for (Property redirectRule : redirectRules.asPropertyList()) {
|
||||||
|
String ruleName = redirectRule.getName();
|
||||||
|
String ruleValue = redirectRule.getValue().get(RedirecRewritetRuleDefinition.VALUE.getName()).asString();
|
||||||
|
parsed.put(ruleName, ruleValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, Object> entry : parsed.entrySet()) {
|
||||||
|
writer.writeStartElement(RedirecRewritetRuleDefinition.TAG_NAME);
|
||||||
|
writer.writeAttribute("name", entry.getKey());
|
||||||
|
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof String) {
|
||||||
|
writeCharacters(writer, (String) value);
|
||||||
|
} else {
|
||||||
|
Map<String, String> redirectRulesProps = (Map<String, String>) value;
|
||||||
|
for (Map.Entry<String, String> prop : redirectRulesProps.entrySet()) {
|
||||||
|
writer.writeStartElement(prop.getKey());
|
||||||
|
writeCharacters(writer, prop.getValue());
|
||||||
|
writer.writeEndElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.writeEndElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// code taken from org.jboss.as.controller.AttributeMarshaller
|
// code taken from org.jboss.as.controller.AttributeMarshaller
|
||||||
private void writeCharacters(XMLExtendedStreamWriter writer, String content) throws XMLStreamException {
|
private void writeCharacters(XMLExtendedStreamWriter writer, String content) throws XMLStreamException {
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.subsystem.adapter.extension;
|
||||||
|
|
||||||
|
import org.jboss.as.controller.AttributeDefinition;
|
||||||
|
import org.jboss.as.controller.PathElement;
|
||||||
|
import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
|
||||||
|
import org.jboss.as.controller.SimpleResourceDefinition;
|
||||||
|
import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
|
||||||
|
import org.jboss.as.controller.operations.validation.StringLengthValidator;
|
||||||
|
import org.jboss.as.controller.registry.ManagementResourceRegistration;
|
||||||
|
import org.jboss.dmr.ModelType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author sblanc
|
||||||
|
*/
|
||||||
|
public class RedirecRewritetRuleDefinition extends SimpleResourceDefinition {
|
||||||
|
|
||||||
|
public static final String TAG_NAME = "redirect-rewrite-rule";
|
||||||
|
|
||||||
|
protected static final AttributeDefinition VALUE =
|
||||||
|
new SimpleAttributeDefinitionBuilder("value", ModelType.STRING, false)
|
||||||
|
.setAllowExpression(true)
|
||||||
|
.setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, false, true))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public RedirecRewritetRuleDefinition() {
|
||||||
|
super(PathElement.pathElement(TAG_NAME),
|
||||||
|
KeycloakExtension.getResourceDescriptionResolver(TAG_NAME),
|
||||||
|
new RedirectRewriteRuleAddHandler(VALUE),
|
||||||
|
RedirectRewriteRuleRemoveHandler.INSTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerOperations(ManagementResourceRegistration resourceRegistration) {
|
||||||
|
super.registerOperations(resourceRegistration);
|
||||||
|
resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
|
||||||
|
super.registerAttributes(resourceRegistration);
|
||||||
|
resourceRegistration.registerReadWriteAttribute(VALUE, null, new RedirectRewriteRuleReadWriteAttributeHandler());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.subsystem.adapter.extension;
|
||||||
|
|
||||||
|
import org.jboss.as.controller.AbstractAddStepHandler;
|
||||||
|
import org.jboss.as.controller.AttributeDefinition;
|
||||||
|
import org.jboss.as.controller.OperationContext;
|
||||||
|
import org.jboss.as.controller.OperationFailedException;
|
||||||
|
import org.jboss.dmr.ModelNode;
|
||||||
|
|
||||||
|
public class RedirectRewriteRuleAddHandler extends AbstractAddStepHandler {
|
||||||
|
|
||||||
|
public RedirectRewriteRuleAddHandler(AttributeDefinition... attributes) {
|
||||||
|
super(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
|
||||||
|
KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
|
||||||
|
ckService.addRedirectRewriteRule(operation, context.resolveExpressions(model));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.subsystem.adapter.extension;
|
||||||
|
|
||||||
|
import org.jboss.as.controller.AbstractWriteAttributeHandler;
|
||||||
|
import org.jboss.as.controller.OperationContext;
|
||||||
|
import org.jboss.as.controller.OperationFailedException;
|
||||||
|
import org.jboss.dmr.ModelNode;
|
||||||
|
|
||||||
|
public class RedirectRewriteRuleReadWriteAttributeHandler extends AbstractWriteAttributeHandler<KeycloakAdapterConfigService> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName,
|
||||||
|
ModelNode resolvedValue, ModelNode currentValue, AbstractWriteAttributeHandler.HandbackHolder<KeycloakAdapterConfigService> hh) throws OperationFailedException {
|
||||||
|
|
||||||
|
KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
|
||||||
|
ckService.updateRedirectRewriteRule(operation, attributeName, resolvedValue);
|
||||||
|
|
||||||
|
hh.setHandback(ckService);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName,
|
||||||
|
ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException {
|
||||||
|
ckService.updateRedirectRewriteRule(operation, attributeName, valueToRestore);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.subsystem.adapter.extension;
|
||||||
|
|
||||||
|
import org.jboss.as.controller.AbstractRemoveStepHandler;
|
||||||
|
import org.jboss.as.controller.OperationContext;
|
||||||
|
import org.jboss.as.controller.OperationFailedException;
|
||||||
|
import org.jboss.dmr.ModelNode;
|
||||||
|
|
||||||
|
public class RedirectRewriteRuleRemoveHandler extends AbstractRemoveStepHandler {
|
||||||
|
|
||||||
|
public static RedirectRewriteRuleRemoveHandler INSTANCE = new RedirectRewriteRuleRemoveHandler();
|
||||||
|
|
||||||
|
private RedirectRewriteRuleRemoveHandler() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
|
||||||
|
KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance();
|
||||||
|
ckService.removeRedirectRewriteRule(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -65,6 +65,7 @@ keycloak.secure-deployment.connection-pool-size=Connection pool size for the cli
|
||||||
keycloak.secure-deployment.resource=Application name
|
keycloak.secure-deployment.resource=Application name
|
||||||
keycloak.secure-deployment.use-resource-role-mappings=Use resource level permissions from token
|
keycloak.secure-deployment.use-resource-role-mappings=Use resource level permissions from token
|
||||||
keycloak.secure-deployment.credentials=Adapter credentials
|
keycloak.secure-deployment.credentials=Adapter credentials
|
||||||
|
keycloak.secure-deployment.redirect-rewrite-rule=Apply a rewrite rule for the redirect URI
|
||||||
keycloak.secure-deployment.bearer-only=Bearer Token Auth only
|
keycloak.secure-deployment.bearer-only=Bearer Token Auth only
|
||||||
keycloak.secure-deployment.enable-basic-auth=Enable Basic Authentication
|
keycloak.secure-deployment.enable-basic-auth=Enable Basic Authentication
|
||||||
keycloak.secure-deployment.public-client=Public client
|
keycloak.secure-deployment.public-client=Public client
|
||||||
|
@ -94,4 +95,9 @@ keycloak.secure-deployment.credential=Credential value
|
||||||
keycloak.credential=Credential
|
keycloak.credential=Credential
|
||||||
keycloak.credential.value=Credential value
|
keycloak.credential.value=Credential value
|
||||||
keycloak.credential.add=Credential add
|
keycloak.credential.add=Credential add
|
||||||
keycloak.credential.remove=Credential remove
|
keycloak.credential.remove=Credential remove
|
||||||
|
|
||||||
|
keycloak.redirect-rewrite-rule=redirect-rewrite-rule
|
||||||
|
keycloak.redirect-rewrite-rule.value=redirect-rewrite-rule value
|
||||||
|
keycloak.redirect-rewrite-rule.add=redirect-rewrite-rule add
|
||||||
|
keycloak.redirect-rewrite-rule.remove=redirect-rewrite-rule remove
|
|
@ -101,6 +101,7 @@
|
||||||
<xs:element name="ssl-required" type="xs:string" minOccurs="0" maxOccurs="1" />
|
<xs:element name="ssl-required" type="xs:string" minOccurs="0" maxOccurs="1" />
|
||||||
<xs:element name="realm-public-key" type="xs:string" minOccurs="1" maxOccurs="1"/>
|
<xs:element name="realm-public-key" type="xs:string" minOccurs="1" maxOccurs="1"/>
|
||||||
<xs:element name="credential" type="credential-type" minOccurs="1" maxOccurs="1"/>
|
<xs:element name="credential" type="credential-type" minOccurs="1" maxOccurs="1"/>
|
||||||
|
<xs:element name="redirect-rewrite-rule" type="redirect-rewrite-rule-type" minOccurs="1" maxOccurs="1"/>
|
||||||
<xs:element name="auth-server-url-for-backend-requests" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
<xs:element name="auth-server-url-for-backend-requests" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||||
<xs:element name="always-refresh-token" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
|
<xs:element name="always-refresh-token" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
|
||||||
<xs:element name="register-node-at-startup" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
|
<xs:element name="register-node-at-startup" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
|
||||||
|
@ -127,4 +128,10 @@
|
||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
<xs:attribute name="name" type="xs:string" use="required" />
|
<xs:attribute name="name" type="xs:string" use="required" />
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
<xs:complexType name="redirect-rewrite-rule-type" mixed="true">
|
||||||
|
<xs:sequence maxOccurs="unbounded" minOccurs="0">
|
||||||
|
<xs:any processContents="lax"></xs:any>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="name" type="xs:string" use="required" />
|
||||||
|
</xs:complexType>
|
||||||
</xs:schema>
|
</xs:schema>
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
<auth-server-url>http://localhost:8080/auth</auth-server-url>
|
<auth-server-url>http://localhost:8080/auth</auth-server-url>
|
||||||
<ssl-required>EXTERNAL</ssl-required>
|
<ssl-required>EXTERNAL</ssl-required>
|
||||||
<credential name="secret">0aa31d98-e0aa-404c-b6e0-e771dba1e798</credential>
|
<credential name="secret">0aa31d98-e0aa-404c-b6e0-e771dba1e798</credential>
|
||||||
|
<redirect-rewrite-rule name="^/wsmaster/api/(.*)$">api/$1/</redirect-rewrite-rule>
|
||||||
</secure-deployment>
|
</secure-deployment>
|
||||||
<secure-deployment name="http-endpoint">
|
<secure-deployment name="http-endpoint">
|
||||||
<realm>master</realm>
|
<realm>master</realm>
|
||||||
|
@ -66,5 +67,6 @@
|
||||||
<credential name="jwt">
|
<credential name="jwt">
|
||||||
<client-keystore-file>/tmp/keystore.jks</client-keystore-file>
|
<client-keystore-file>/tmp/keystore.jks</client-keystore-file>
|
||||||
</credential>
|
</credential>
|
||||||
|
<redirect-rewrite-rule name="^/wsmaster/api/(.*)$">/api/$1/</redirect-rewrite-rule>
|
||||||
</secure-deployment>
|
</secure-deployment>
|
||||||
</subsystem>
|
</subsystem>
|
|
@ -62,7 +62,8 @@ public class BaseAdapterConfig extends BaseRealmConfig {
|
||||||
protected boolean publicClient;
|
protected boolean publicClient;
|
||||||
@JsonProperty("credentials")
|
@JsonProperty("credentials")
|
||||||
protected Map<String, Object> credentials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
protected Map<String, Object> credentials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||||
|
@JsonProperty("redirect-rewrite-rules")
|
||||||
|
protected Map<String, String> redirectRewriteRules;
|
||||||
|
|
||||||
public boolean isUseResourceRoleMappings() {
|
public boolean isUseResourceRoleMappings() {
|
||||||
return useResourceRoleMappings;
|
return useResourceRoleMappings;
|
||||||
|
@ -167,4 +168,14 @@ public class BaseAdapterConfig extends BaseRealmConfig {
|
||||||
public void setPublicClient(boolean publicClient) {
|
public void setPublicClient(boolean publicClient) {
|
||||||
this.publicClient = publicClient;
|
this.publicClient = publicClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getRedirectRewriteRules() {
|
||||||
|
return redirectRewriteRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRedirectRewriteRules(Map<String, String> redirectRewriteRules) {
|
||||||
|
this.redirectRewriteRules = redirectRewriteRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -258,6 +258,7 @@ public class TokenEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
event.user(userSession.getUser());
|
event.user(userSession.getUser());
|
||||||
|
|
||||||
event.session(userSession.getId());
|
event.session(userSession.getId());
|
||||||
|
|
||||||
String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM);
|
String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM);
|
||||||
|
|
Loading…
Reference in a new issue