Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
110783ff2f
14 changed files with 200 additions and 12 deletions
|
@ -112,6 +112,15 @@
|
|||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>public-client</term>
|
||||
<listitem>
|
||||
<para>
|
||||
If set to true, the adapter will not send credentials for the client to Keycloak.
|
||||
The default value is <emphasis>false</emphasis>.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>enable-cors</term>
|
||||
<listitem>
|
||||
|
@ -140,7 +149,19 @@
|
|||
<para>
|
||||
If CORS is enabled, this sets the value of the
|
||||
<literal>Access-Control-Allow-Methods</literal>
|
||||
header. This should be a JSON list of strings.
|
||||
header. This should be a comma-separated string.
|
||||
This is <emphasis>OPTIONAL</emphasis>. If not set, this header is not returned in CORS
|
||||
responses.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>cors-allowed-headers</term>
|
||||
<listitem>
|
||||
<para>
|
||||
If CORS is enabled, this sets the value of the
|
||||
<literal>Access-Control-Allow-Headers</literal>
|
||||
header. This should be a comma-separated string.
|
||||
This is <emphasis>OPTIONAL</emphasis>. If not set, this header is not returned in CORS
|
||||
responses.
|
||||
</para>
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk16</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
|
@ -51,6 +56,11 @@
|
|||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
|
|
|
@ -59,6 +59,8 @@ public class KeycloakDeploymentBuilder {
|
|||
deployment.setPublicClient(adapterConfig.isPublicClient());
|
||||
deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings());
|
||||
|
||||
deployment.setExposeToken(adapterConfig.isExposeToken());
|
||||
|
||||
if (adapterConfig.isCors()) {
|
||||
deployment.setCors(true);
|
||||
deployment.setCorsMaxAge(adapterConfig.getCorsMaxAge());
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package org.keycloak.adapters;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.keycloak.enums.SslRequired;
|
||||
import org.keycloak.enums.TokenStore;
|
||||
import org.keycloak.util.PemUtils;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class KeycloakDeploymentBuilderTest {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder folder = new TemporaryFolder();
|
||||
|
||||
@Before
|
||||
public void before() throws IOException {
|
||||
File dir = folder.newFolder();
|
||||
FileUtils.copyInputStreamToFile(getClass().getResourceAsStream("/cacerts.jks"), new File(dir, "cacerts.jks"));
|
||||
FileUtils.copyInputStreamToFile(getClass().getResourceAsStream("/keystore.jks"), new File(dir, "keystore.jks"));
|
||||
System.setProperty("testResources", dir.getAbsolutePath());
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
System.getProperties().remove("testResources");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void load() throws Exception {
|
||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak.json"));
|
||||
assertEquals("demo", deployment.getRealm());
|
||||
assertEquals("customer-portal", deployment.getResourceName());
|
||||
assertEquals(PemUtils.decodePublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"), deployment.getRealmKey());
|
||||
assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/login", deployment.getAuthUrl().build().toString());
|
||||
assertEquals(SslRequired.EXTERNAL, deployment.getSslRequired());
|
||||
assertTrue(deployment.isUseResourceRoleMappings());
|
||||
assertTrue(deployment.isCors());
|
||||
assertEquals(1000, deployment.getCorsMaxAge());
|
||||
assertEquals("POST, PUT, DELETE, GET", deployment.getCorsAllowedMethods());
|
||||
assertEquals("X-Custom, X-Custom2", deployment.getCorsAllowedHeaders());
|
||||
assertTrue(deployment.isBearerOnly());
|
||||
assertTrue(deployment.isPublicClient());
|
||||
assertTrue(deployment.isEnableBasicAuth());
|
||||
assertTrue(deployment.isExposeToken());
|
||||
assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret"));
|
||||
assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal());
|
||||
assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/refresh", deployment.getRefreshUrl());
|
||||
assertTrue(deployment.isAlwaysRefreshToken());
|
||||
assertTrue(deployment.isRegisterNodeAtStartup());
|
||||
assertEquals(1000, deployment.getRegisterNodePeriod());
|
||||
assertEquals(TokenStore.COOKIE, deployment.getTokenStore());
|
||||
assertEquals("email", deployment.getPrincipalAttribute());
|
||||
}
|
||||
|
||||
}
|
BIN
integration/adapter-core/src/test/resources/cacerts.jks
Normal file
BIN
integration/adapter-core/src/test/resources/cacerts.jks
Normal file
Binary file not shown.
33
integration/adapter-core/src/test/resources/keycloak.json
Normal file
33
integration/adapter-core/src/test/resources/keycloak.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"realm": "demo",
|
||||
"resource": "customer-portal",
|
||||
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||
"auth-server-url": "https://localhost:8443/auth",
|
||||
"ssl-required": "external",
|
||||
"use-resource-role-mappings": true,
|
||||
"enable-cors": true,
|
||||
"cors-max-age": 1000,
|
||||
"cors-allowed-methods": "POST, PUT, DELETE, GET",
|
||||
"cors-allowed-headers": "X-Custom, X-Custom2",
|
||||
"bearer-only": true,
|
||||
"public-client": true,
|
||||
"enable-basic-auth": true,
|
||||
"expose-token": true,
|
||||
"credentials": {
|
||||
"secret": "234234-234234-234234"
|
||||
},
|
||||
"connection-pool-size": 20,
|
||||
"disable-trust-manager": true,
|
||||
"allow-any-hostname": true,
|
||||
"truststore": "${testResources}/cacerts.jks",
|
||||
"truststore-password": "changeit",
|
||||
"client-keystore": "${testResources}/keystore.jks",
|
||||
"client-keystore-password": "changeit",
|
||||
"client-key-password": "password",
|
||||
"auth-server-url-for-backend-requests": "https://backend:8443/auth",
|
||||
"always-refresh-token": true,
|
||||
"register-node-at-startup": true,
|
||||
"register-node-period": 1000,
|
||||
"token-store": "cookie",
|
||||
"principal-attribute": "email"
|
||||
}
|
BIN
integration/adapter-core/src/test/resources/keystore.jks
Normal file
BIN
integration/adapter-core/src/test/resources/keystore.jks
Normal file
Binary file not shown.
|
@ -51,7 +51,7 @@
|
|||
var configPromise = loadConfig(config);
|
||||
|
||||
function processInit() {
|
||||
var callback = parseCallback(window.location.search);
|
||||
var callback = parseCallback(window.location.href);
|
||||
|
||||
if (callback) {
|
||||
window.history.replaceState({}, null, callback.newUrl);
|
||||
|
@ -534,6 +534,7 @@
|
|||
break;
|
||||
default:
|
||||
oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + p[0] + '=' + p[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -688,8 +689,9 @@
|
|||
} else if (kc.redirectUri) {
|
||||
return kc.redirectUri;
|
||||
} else {
|
||||
var redirectUri = location.href.substring(0, location.href.indexOf('#'));
|
||||
var redirectUri = location.href;
|
||||
if (location.hash) {
|
||||
redirectUri = redirectUri.substring(0, location.href.indexOf('#'));
|
||||
redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1));
|
||||
}
|
||||
return redirectUri;
|
||||
|
|
|
@ -62,8 +62,7 @@ public class CatalinaUserSessionManagement implements SessionListener {
|
|||
|
||||
public void sessionEvent(SessionEvent event) {
|
||||
// We only care about session destroyed events
|
||||
if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType())
|
||||
&& (!Session.SESSION_PASSIVATED_EVENT.equals(event.getType())))
|
||||
if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()))
|
||||
return;
|
||||
|
||||
// Look up the single session id associated with this session (if any)
|
||||
|
|
6
pom.xml
6
pom.xml
|
@ -303,6 +303,12 @@
|
|||
<version>4.11</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<scope>test</scope>
|
||||
<version>2.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-all</artifactId>
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
package org.keycloak.services.resources;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.email.EmailException;
|
||||
|
@ -45,7 +44,6 @@ import org.keycloak.models.UserSessionModel;
|
|||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.TimeBasedOTP;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OpenIDConnect;
|
||||
import org.keycloak.protocol.oidc.OpenIDConnectService;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.PasswordToken;
|
||||
|
@ -63,7 +61,6 @@ import javax.ws.rs.POST;
|
|||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Cookie;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
@ -73,7 +70,6 @@ import javax.ws.rs.core.UriInfo;
|
|||
import javax.ws.rs.ext.Providers;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
|
@ -605,16 +601,28 @@ public class LoginActionsService {
|
|||
user.setLastName(formData.getFirst("lastName"));
|
||||
|
||||
String email = formData.getFirst("email");
|
||||
|
||||
String oldEmail = user.getEmail();
|
||||
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
|
||||
|
||||
user.setEmail(email);
|
||||
if (emailChanged) {
|
||||
UserModel userByEmail = session.users().getUserByEmail(email, realm);
|
||||
|
||||
// check for duplicated email
|
||||
if (userByEmail != null && !userByEmail.getId().equals(user.getId())) {
|
||||
return Flows.forms(session, realm, null, uriInfo).setUser(user).setError(Messages.EMAIL_EXISTS)
|
||||
.setClientSessionCode(accessCode.getCode())
|
||||
.createResponse(RequiredAction.UPDATE_PROFILE);
|
||||
}
|
||||
|
||||
user.setEmail(email);
|
||||
user.setEmailVerified(false);
|
||||
}
|
||||
|
||||
user.removeRequiredAction(RequiredAction.UPDATE_PROFILE);
|
||||
|
||||
event.clone().event(EventType.UPDATE_PROFILE).success();
|
||||
|
||||
if (emailChanged) {
|
||||
user.setEmailVerified(false);
|
||||
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success();
|
||||
}
|
||||
|
||||
|
|
|
@ -147,4 +147,21 @@ public class RequiredActionUpdateProfileTest {
|
|||
events.assertEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateProfileDuplicatedEmail() {
|
||||
loginPage.open();
|
||||
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
updateProfilePage.assertCurrent();
|
||||
|
||||
updateProfilePage.update("New first", "New last", "keycloak-user@localhost");
|
||||
|
||||
updateProfilePage.assertCurrent();
|
||||
|
||||
Assert.assertEquals("Email already exists", updateProfilePage.getError());
|
||||
|
||||
events.assertEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,6 +29,20 @@
|
|||
"test-app": [ "customer-user" ],
|
||||
"account": [ "view-profile", "manage-account" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"username" : "keycloak-user@localhost",
|
||||
"enabled": true,
|
||||
"email" : "keycloak-user@localhost",
|
||||
"credentials" : [
|
||||
{ "type" : "password",
|
||||
"value" : "password" }
|
||||
],
|
||||
"realmRoles": ["user"],
|
||||
"applicationRoles": {
|
||||
"test-app": [ "customer-user" ],
|
||||
"account": [ "view-profile", "manage-account" ]
|
||||
}
|
||||
}
|
||||
],
|
||||
"oauthClients" : [
|
||||
|
|
|
@ -89,6 +89,11 @@
|
|||
<artifactId>resteasy-undertow</artifactId>
|
||||
<version>${resteasy.version.latest}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.jmeter</groupId>
|
||||
|
|
Loading…
Reference in a new issue