Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2016-04-12 15:19:58 -04:00
commit 515ed226be
67 changed files with 1209 additions and 577 deletions

View file

@ -192,7 +192,7 @@
redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'prompt=' + options.prompt;
}
sessionStorage.oauthState = JSON.stringify({ state: state, nonce: nonce, redirectUri: encodeURIComponent(redirectUri) });
localStorage.oauthState = JSON.stringify({ state: state, nonce: nonce, redirectUri: encodeURIComponent(redirectUri) });
var action = 'auth';
if (options && options.action == 'register') {
@ -689,10 +689,10 @@
function parseCallback(url) {
var oauth = new CallbackParser(url, kc.responseMode).parseUri();
var sessionState = sessionStorage.oauthState && JSON.parse(sessionStorage.oauthState);
var sessionState = localStorage.oauthState && JSON.parse(localStorage.oauthState);
if (sessionState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token) && oauth.state && oauth.state == sessionState.state) {
delete sessionStorage.oauthState;
delete localStorage.oauthState;
oauth.redirectUri = sessionState.redirectUri;
oauth.storedNonce = sessionState.nonce;

View file

@ -27,9 +27,17 @@ import java.util.Map;
*/
public class AuthenticatorConfigRepresentation implements Serializable {
private String id;
private String alias;
private Map<String, String> config = new HashMap<String, String>();
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getAlias() {
return alias;
@ -39,8 +47,6 @@ public class AuthenticatorConfigRepresentation implements Serializable {
this.alias = alias;
}
public Map<String, String> getConfig() {
return config;
}

View file

@ -20,7 +20,7 @@
<para>
There are multiple ways you can logout from a web application. For Java EE servlet containers, you can call
HttpServletRequest.logout().
For any other browser application, you can point the browser at the url <literal>http://auth-server/auth/realms/{realm-name}/tokens/logout?redirect_uri=encodedRedirectUri</literal>.
For any other browser application, you can point the browser at the url <literal>http://auth-server/auth/realms/{realm-name}/protocol/openid-connect/logout?redirect_uri=encodedRedirectUri</literal>.
This will log you out if you have an SSO session with your browser.
</para>
</section>

View file

@ -46,7 +46,7 @@ public class Keycloak {
Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient) {
config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType);
client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().build();
client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().connectionPoolSize(10).build();
tokenManager = new TokenManager(config, client);

View file

@ -35,6 +35,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
/**
* @author rodrigo.sasaki@icarros.com.br
@ -132,4 +133,13 @@ public interface UserResource {
@Path("role-mappings")
public RoleMappingResource roles();
@GET
@Path("consents")
public List<Map<String, Object>> getConsents();
@DELETE
@Path("consents/{client}")
public void revokeConsent(@PathParam("client") String clientId);
}

View file

@ -19,13 +19,7 @@ package org.keycloak.admin.client.resource;
import org.keycloak.representations.idm.UserRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
@ -59,4 +53,7 @@ public interface UsersResource {
@Path("{id}")
UserResource get(@PathParam("id") String id);
@Path("{id}")
@DELETE
Response delete(@PathParam("id") String id);
}

View file

@ -39,6 +39,7 @@ import org.keycloak.Config;
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase;
import org.keycloak.connections.jpa.updater.liquibase.lock.CustomInsertLockRecordGenerator;
import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockDatabaseChangeLogGenerator;
import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockService;
import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService;
import org.keycloak.models.KeycloakSession;
@ -68,6 +69,7 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
protected void baseLiquibaseInitialization() {
ServiceLocator sl = ServiceLocator.getInstance();
sl.setResourceAccessor(new ClassLoaderResourceAccessor(getClass().getClassLoader()));
if (!System.getProperties().containsKey("liquibase.scan.packages")) {
if (sl.getPackages().remove("liquibase.core")) {
@ -84,6 +86,10 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
sl.getPackages().remove("liquibase.ext");
sl.getPackages().remove("liquibase.sdk");
String lockPackageName = DummyLockService.class.getPackage().getName();
logger.debugf("Added package %s to liquibase", lockPackageName);
sl.addPackageToScan(lockPackageName);
}
LogFactory.setInstance(new LogWrapper());
@ -93,6 +99,9 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
// Change command for creating lock and drop DELETE lock record from it
SqlGeneratorFactory.getInstance().register(new CustomInsertLockRecordGenerator());
// Use "SELECT FOR UPDATE" for locking database
SqlGeneratorFactory.getInstance().register(new CustomLockDatabaseChangeLogGenerator());
}
@ -125,10 +134,6 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
String changelog = (database instanceof DB2Database) ? LiquibaseJpaUpdaterProvider.DB2_CHANGELOG : LiquibaseJpaUpdaterProvider.CHANGELOG;
logger.debugf("Using changelog file: %s", changelog);
// We wrap liquibase update in CustomLockService provided by DBLockProvider. No need to lock inside liquibase itself.
// NOTE: This can't be done in baseLiquibaseInitialization() as liquibase always restarts lock service
LockServiceFactory.getInstance().register(new DummyLockService());
return new Liquibase(changelog, new ClassLoaderResourceAccessor(getClass().getClassLoader()), database);
}

View file

@ -0,0 +1,86 @@
/*
* 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.connections.jpa.updater.liquibase.lock;
import liquibase.database.Database;
import liquibase.database.core.DB2Database;
import liquibase.database.core.H2Database;
import liquibase.database.core.MSSQLDatabase;
import liquibase.database.core.MySQLDatabase;
import liquibase.database.core.OracleDatabase;
import liquibase.database.core.PostgresDatabase;
import liquibase.sql.Sql;
import liquibase.sql.UnparsedSql;
import liquibase.sqlgenerator.SqlGeneratorChain;
import liquibase.sqlgenerator.core.LockDatabaseChangeLogGenerator;
import liquibase.statement.core.LockDatabaseChangeLogStatement;
import org.jboss.logging.Logger;
/**
* We use "SELECT FOR UPDATE" pessimistic locking (Same algorithm like Hibernate LockMode.PESSIMISTIC_WRITE )
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CustomLockDatabaseChangeLogGenerator extends LockDatabaseChangeLogGenerator {
private static final Logger logger = Logger.getLogger(CustomLockDatabaseChangeLogGenerator.class);
@Override
public int getPriority() {
return super.getPriority() + 1; // Ensure bigger priority than LockDatabaseChangeLogGenerator
}
@Override
public Sql[] generateSql(LockDatabaseChangeLogStatement statement, Database database, SqlGeneratorChain sqlGeneratorChain) {
Sql selectForUpdateSql = generateSelectForUpdate(database);
return new Sql[] { selectForUpdateSql };
}
private Sql generateSelectForUpdate(Database database) {
String catalog = database.getLiquibaseCatalogName();
String schema = database.getLiquibaseSchemaName();
String rawLockTableName = database.getDatabaseChangeLogLockTableName();
String lockTableName = database.escapeTableName(catalog, schema, rawLockTableName);
String idColumnName = database.escapeColumnName(catalog, schema, rawLockTableName, "ID");
String sqlBase = "SELECT " + idColumnName + " FROM " + lockTableName;
String sqlWhere = " WHERE " + idColumnName + "=1";
String sql;
if (database instanceof MySQLDatabase || database instanceof PostgresDatabase || database instanceof H2Database ||
database instanceof OracleDatabase) {
sql = sqlBase + sqlWhere + " FOR UPDATE";
} else if (database instanceof MSSQLDatabase) {
sql = sqlBase + " WITH (UPDLOCK, ROWLOCK)" + sqlWhere;
} else if (database instanceof DB2Database) {
sql = sqlBase + sqlWhere + " FOR READ ONLY WITH RS USE AND KEEP UPDATE LOCKS";
} else {
sql = sqlBase + sqlWhere;
logger.warnf("No direct support for database %s . Database lock may not work correctly", database.getClass().getName());
}
logger.debugf("SQL command for pessimistic lock: %s", sql);
return new UnparsedSql(sql);
}
}

View file

@ -18,25 +18,16 @@
package org.keycloak.connections.jpa.updater.liquibase.lock;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import liquibase.database.Database;
import liquibase.database.core.DerbyDatabase;
import liquibase.exception.DatabaseException;
import liquibase.exception.LockException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.lockservice.DatabaseChangeLogLock;
import liquibase.lockservice.StandardLockService;
import liquibase.logging.LogFactory;
import liquibase.sql.visitor.AbstractSqlVisitor;
import liquibase.sql.visitor.SqlVisitor;
import liquibase.statement.core.CreateDatabaseChangeLogLockTableStatement;
import liquibase.statement.core.DropTableStatement;
import liquibase.statement.core.InitializeDatabaseChangeLogLockTableStatement;
import liquibase.statement.core.LockDatabaseChangeLogStatement;
import liquibase.statement.core.RawSqlStatement;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
@ -51,24 +42,6 @@ public class CustomLockService extends StandardLockService {
private static final Logger log = Logger.getLogger(CustomLockService.class);
private long changeLogLocRecheckTimeMillis = -1;
@Override
public void setChangeLogLockRecheckTime(long changeLogLocRecheckTime) {
super.setChangeLogLockRecheckTime(changeLogLocRecheckTime);
this.changeLogLocRecheckTimeMillis = changeLogLocRecheckTime;
}
// Bug in StandardLockService.getChangeLogLockRecheckTime()
@Override
public Long getChangeLogLockRecheckTime() {
if (changeLogLocRecheckTimeMillis == -1) {
return super.getChangeLogLockRecheckTime();
} else {
return changeLogLocRecheckTimeMillis;
}
}
@Override
public void init() throws DatabaseException {
boolean createdTable = false;
@ -84,8 +57,8 @@ public class CustomLockService extends StandardLockService {
database.commit();
} catch (DatabaseException de) {
log.warn("Failed to create lock table. Maybe other transaction created in the meantime. Retrying...");
if (log.isDebugEnabled()) {
log.debug(de.getMessage(), de); //Log details at debug level
if (log.isTraceEnabled()) {
log.trace(de.getMessage(), de); //Log details at trace level
}
database.rollback();
throw new LockRetryException(de);
@ -115,8 +88,8 @@ public class CustomLockService extends StandardLockService {
} catch (DatabaseException de) {
log.warn("Failed to insert first record to the lock table. Maybe other transaction inserted in the meantime. Retrying...");
if (log.isDebugEnabled()) {
log.debug(de.getMessage(), de); // Log details at debug level
if (log.isTraceEnabled()) {
log.trace(de.getMessage(), de); // Log details at trace level
}
database.rollback();
throw new LockRetryException(de);
@ -140,34 +113,88 @@ public class CustomLockService extends StandardLockService {
}
@Override
public void waitForLock() throws LockException {
public void waitForLock() {
boolean locked = false;
long startTime = Time.toMillis(Time.currentTime());
long timeToGiveUp = startTime + (getChangeLogLockWaitTime());
boolean nextAttempt = true;
while (!locked && Time.toMillis(Time.currentTime()) < timeToGiveUp) {
while (nextAttempt) {
locked = acquireLock();
if (!locked) {
int remainingTime = ((int)(timeToGiveUp / 1000)) - Time.currentTime();
log.debugf("Waiting for changelog lock... Remaining time: %d seconds", remainingTime);
try {
Thread.sleep(getChangeLogLockRecheckTime());
} catch (InterruptedException e) {
e.printStackTrace();
if (remainingTime > 0) {
log.debugf("Will try to acquire log another time. Remaining time: %d seconds", remainingTime);
} else {
nextAttempt = false;
}
} else {
nextAttempt = false;
}
}
if (!locked) {
DatabaseChangeLogLock[] locks = listLocks();
String lockedBy;
if (locks.length > 0) {
DatabaseChangeLogLock lock = locks[0];
lockedBy = lock.getLockedBy() + " since " + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(lock.getLockGranted());
} else {
lockedBy = "UNKNOWN";
int timeout = ((int)(getChangeLogLockWaitTime() / 1000));
throw new IllegalStateException("Could not acquire change log lock within specified timeout " + timeout + " seconds. Currently locked by other transaction");
}
}
@Override
public boolean acquireLock() {
if (hasChangeLogLock) {
// We already have a lock
return true;
}
Executor executor = ExecutorService.getInstance().getExecutor(database);
try {
database.rollback();
// Ensure table created and lock record inserted
this.init();
} catch (DatabaseException de) {
throw new IllegalStateException("Failed to retrieve lock", de);
}
try {
log.debug("Trying to lock database");
executor.execute(new LockDatabaseChangeLogStatement());
log.debug("Successfully acquired database lock");
hasChangeLogLock = true;
database.setCanCacheLiquibaseTableInfo(true);
return true;
} catch (DatabaseException de) {
log.warn("Lock didn't yet acquired. Will possibly retry to acquire lock. Details: " + de.getMessage());
if (log.isTraceEnabled()) {
log.debug(de.getMessage(), de);
}
return false;
}
}
@Override
public void releaseLock() {
try {
if (hasChangeLogLock) {
log.debug("Going to release database lock");
database.commit();
} else {
log.warn("Attempt to release lock, which is not owned by current transaction");
}
} catch (Exception e) {
log.error("Database error during release lock", e);
} finally {
try {
hasChangeLogLock = false;
database.setCanCacheLiquibaseTableInfo(false);
database.rollback();
} catch (DatabaseException e) {
;
}
throw new LockException("Could not acquire change log lock. Currently locked by " + lockedBy);
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.connections.jpa.updater.liquibase.lock;
import liquibase.exception.DatabaseException;
import liquibase.exception.LockException;
import liquibase.lockservice.StandardLockService;
@ -27,6 +28,15 @@ import liquibase.lockservice.StandardLockService;
*/
public class DummyLockService extends StandardLockService {
@Override
public int getPriority() {
return Integer.MAX_VALUE;
}
@Override
public void init() throws DatabaseException {
}
@Override
public void waitForLock() throws LockException {
}

View file

@ -46,7 +46,7 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
private final LiquibaseDBLockProviderFactory factory;
private final KeycloakSession session;
private LockService lockService;
private CustomLockService lockService;
private Connection dbConnection;
private int maxAttempts = DEFAULT_MAX_ATTEMPTS;
@ -69,7 +69,6 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
this.lockService = new CustomLockService();
lockService.setChangeLogLockWaitTime(factory.getLockWaitTimeoutMillis());
lockService.setChangeLogLockRecheckTime(factory.getLockRecheckTimeMillis());
lockService.setDatabase(liquibase.getDatabase());
} catch (LiquibaseException exception) {
safeRollbackConnection();
@ -94,16 +93,15 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
lockService.waitForLock();
this.maxAttempts = DEFAULT_MAX_ATTEMPTS;
return;
} catch (LockException le) {
if (le.getCause() != null && le.getCause() instanceof LockRetryException) {
} catch (LockRetryException le) {
// Indicates we should try to acquire lock again in different transaction
safeRollbackConnection();
restart();
maxAttempts--;
} else {
throw new IllegalStateException("Failed to retrieve lock", le);
// TODO: Possibility to forcefully retrieve lock after timeout instead of just give-up?
}
} catch (RuntimeException re) {
safeRollbackConnection();
safeCloseConnection();
throw re;
}
}
}
@ -111,14 +109,16 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
@Override
public void releaseLock() {
try {
lockService.releaseLock();
} catch (LockException e) {
logger.error("Could not release lock", e);
}
lockService.reset();
}
@Override
public boolean supportsForcedUnlock() {
// Implementation based on "SELECT FOR UPDATE" can't force unlock as it's locked by other transaction
return false;
}
@Override
public void destroyLockInfo() {
try {

View file

@ -31,24 +31,17 @@ public class LiquibaseDBLockProviderFactory implements DBLockProviderFactory {
private static final Logger logger = Logger.getLogger(LiquibaseDBLockProviderFactory.class);
private long lockRecheckTimeMillis;
private long lockWaitTimeoutMillis;
protected long getLockRecheckTimeMillis() {
return lockRecheckTimeMillis;
}
protected long getLockWaitTimeoutMillis() {
return lockWaitTimeoutMillis;
}
@Override
public void init(Config.Scope config) {
int lockRecheckTime = config.getInt("lockRecheckTime", 2);
int lockWaitTimeout = config.getInt("lockWaitTimeout", 900);
this.lockRecheckTimeMillis = Time.toMillis(lockRecheckTime);
this.lockWaitTimeoutMillis = Time.toMillis(lockWaitTimeout);
logger.debugf("Liquibase lock provider configured with lockWaitTime: %d seconds, lockRecheckTime: %d seconds", lockWaitTimeout, lockRecheckTime);
logger.debugf("Liquibase lock provider configured with lockWaitTime: %d seconds", lockWaitTimeout);
}
@Override
@ -63,7 +56,6 @@ public class LiquibaseDBLockProviderFactory implements DBLockProviderFactory {
@Override
public void setTimeouts(long lockRecheckTimeMillis, long lockWaitTimeoutMillis) {
this.lockRecheckTimeMillis = lockRecheckTimeMillis;
this.lockWaitTimeoutMillis = lockWaitTimeoutMillis;
}

View file

@ -129,6 +129,10 @@ public class JpaRealmProvider implements RealmProvider {
em.refresh(realm);
RealmAdapter adapter = new RealmAdapter(session, em, realm);
session.users().preRemove(adapter);
realm.getDefaultGroups().clear();
em.flush();
int num = em.createNamedQuery("deleteGroupRoleMappingsByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupAttributesByRealm")

View file

@ -124,6 +124,11 @@ public class MongoDBLockProvider implements DBLockProvider {
}
}
@Override
public boolean supportsForcedUnlock() {
return true;
}
@Override
public void destroyLockInfo() {
db.getCollection(DB_LOCK_COLLECTION).remove(new BasicDBObject());

53
pom.xml
View file

@ -1,19 +1,19 @@
<!--
~ 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.
-->
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
@ -107,6 +107,7 @@
<minify.plugin.version>1.7.2</minify.plugin.version>
<osgi.bundle.plugin.version>2.3.7</osgi.bundle.plugin.version>
<wildfly.plugin.version>1.0.1.Final</wildfly.plugin.version>
<nexus.staging.plugin.version>1.6.5</nexus.staging.plugin.version>
<!-- Surefire Settings -->
<surefire.memory.settings>-Xms512m -Xmx2048m -XX:MetaspaceSize=96m -XX:MaxMetaspaceSize=256m</surefire.memory.settings>
@ -1229,6 +1230,16 @@
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${nexus.staging.plugin.version}</version>
<extensions>true</extensions>
<configuration>
<nexusUrl>https://repository.jboss.org/nexus</nexusUrl>
<serverId>jboss-releases-repository</serverId>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
@ -1367,5 +1378,17 @@
</plugins>
</build>
</profile>
<profile>
<id>nexus-staging</id>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View file

@ -20,8 +20,10 @@ package org.keycloak.models;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderEventManager;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -30,6 +32,8 @@ import java.util.List;
public interface KeycloakSessionFactory extends ProviderEventManager {
KeycloakSession create();
Set<Spi> getSpis();
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz);
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String id);

View file

@ -34,9 +34,18 @@ public interface DBLockProvider extends Provider {
void waitForLock();
/**
* Release previously acquired lock
*/
void releaseLock();
/**
* @return true if provider supports forced unlock at startup
*/
boolean supportsForcedUnlock();
/**
* Will destroy whole state of DB lock (drop table/collection to track locking).
* */

View file

@ -717,6 +717,7 @@ public class ModelToRepresentation {
public static AuthenticatorConfigRepresentation toRepresentation(AuthenticatorConfigModel model) {
AuthenticatorConfigRepresentation rep = new AuthenticatorConfigRepresentation();
rep.setId(model.getId());
rep.setAlias(model.getAlias());
rep.setConfig(model.getConfig());
return rep;

View file

@ -25,6 +25,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Transport;
@ -54,6 +55,7 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
@Override
public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
Transport transport = null;
try {
String address = user.getEmail();
Map<String, String> config = realm.getSmtpConfig();
@ -114,7 +116,7 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
msg.saveChanges();
msg.setSentDate(new Date());
Transport transport = session.getTransport("smtp");
transport = session.getTransport("smtp");
if (auth) {
transport.connect(config.get("user"), config.get("password"));
} else {
@ -124,6 +126,14 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
} catch (Exception e) {
logger.failedToSendEmail(e);
throw new EmailException(e);
} finally {
if (transport != null) {
try {
transport.close();
} catch (MessagingException e) {
logger.warn("Failed to close transport", e);
}
}
}
}

View file

@ -188,7 +188,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
switch (page) {
case TOTP:
attributes.put("totp", new TotpBean(session, realm, user, baseUri));
attributes.put("totp", new TotpBean(session, realm, user));
break;
case FEDERATED_IDENTITY:
attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker));

View file

@ -14,12 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.Base32;
import org.keycloak.models.utils.HmacOTP;
import org.keycloak.utils.TotpUtils;
import java.io.UnsupportedEncodingException;
import java.net.URI;
@ -34,35 +37,15 @@ public class TotpBean {
private final String totpSecret;
private final String totpSecretEncoded;
private final String totpSecretQrCode;
private final boolean enabled;
private final String contextUrl;
private final String keyUri;
public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, URI baseUri) {
public TotpBean(KeycloakSession session, RealmModel realm, UserModel user) {
this.enabled = session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user);
this.contextUrl = baseUri.getPath();
this.totpSecret = randomString(20);
this.totpSecretEncoded = Base32.encode(totpSecret.getBytes());
this.keyUri = realm.getOTPPolicy().getKeyURI(realm, user, this.totpSecret);
}
private static String randomString(int length) {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW1234567890";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
char c = chars.charAt(random.nextInt(chars.length()));
sb.append(c);
}
return sb.toString();
}
private static final SecureRandom random;
static
{
random = new SecureRandom();
random.nextInt();
this.totpSecret = HmacOTP.generateSecret(20);
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
}
public boolean isEnabled() {
@ -74,19 +57,11 @@ public class TotpBean {
}
public String getTotpSecretEncoded() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < totpSecretEncoded.length(); i += 4) {
sb.append(totpSecretEncoded.substring(i, i + 4 < totpSecretEncoded.length() ? i + 4 : totpSecretEncoded.length()));
if (i + 4 < totpSecretEncoded.length()) {
sb.append(" ");
}
}
return sb.toString();
return totpSecretEncoded;
}
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
String contents = URLEncoder.encode(keyUri, "utf-8");
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
public String getTotpSecretQrCode() {
return totpSecretQrCode;
}
}

View file

@ -279,7 +279,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
switch (page) {
case LOGIN_CONFIG_TOTP:
attributes.put("totp", new TotpBean(realm, user, baseUri));
attributes.put("totp", new TotpBean(realm, user));
break;
case LOGIN_UPDATE_PROFILE:
UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);

View file

@ -20,6 +20,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.Base32;
import org.keycloak.models.utils.HmacOTP;
import org.keycloak.utils.TotpUtils;
import java.io.UnsupportedEncodingException;
import java.net.URI;
@ -32,17 +33,15 @@ public class TotpBean {
private final String totpSecret;
private final String totpSecretEncoded;
private final String totpSecretQrCode;
private final boolean enabled;
private final String contextUrl;
private final String keyUri;
public TotpBean(RealmModel realm, UserModel user, URI baseUri) {
public TotpBean(RealmModel realm, UserModel user) {
this.enabled = user.isOtpEnabled();
this.contextUrl = baseUri.getPath();
this.totpSecret = HmacOTP.generateSecret(20);
this.totpSecretEncoded = Base32.encode(totpSecret.getBytes());
this.keyUri = realm.getOTPPolicy().getKeyURI(realm, user, this.totpSecret);
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
}
public boolean isEnabled() {
@ -54,19 +53,11 @@ public class TotpBean {
}
public String getTotpSecretEncoded() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < totpSecretEncoded.length(); i += 4) {
sb.append(totpSecretEncoded.substring(i, i + 4 < totpSecretEncoded.length() ? i + 4 : totpSecretEncoded.length()));
if (i + 4 < totpSecretEncoded.length()) {
sb.append(" ");
}
}
return sb.toString();
return totpSecretEncoded;
}
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
String contents = URLEncoder.encode(keyUri, "utf-8");
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
public String getTotpSecretQrCode() {
return totpSecretQrCode;
}
}

View file

@ -40,6 +40,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
private Set<Spi> spis = new HashSet<>();
private Map<Class<? extends Provider>, String> provider = new HashMap<Class<? extends Provider>, String>();
private Map<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap = new HashMap<Class<? extends Provider>, Map<String, ProviderFactory>>();
protected CopyOnWriteArrayList<ProviderEventListener> listeners = new CopyOnWriteArrayList<ProviderEventListener>();
@ -80,6 +81,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
protected void loadSPIs(ProviderManager pm, ServiceLoader<Spi> load) {
for (Spi spi : load) {
spis.add(spi);
Map<String, ProviderFactory> factories = new HashMap<String, ProviderFactory>();
factoriesMap.put(spi.getProviderClass(), factories);
@ -138,6 +141,11 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
return provider.get(clazz);
}
@Override
public Set<Spi> getSpis() {
return spis;
}
@Override
public <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz) {
return getProviderFactory(clazz, provider.get(clazz));

View file

@ -17,18 +17,17 @@
package org.keycloak.services.clientregistration;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import javax.ws.rs.*;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@ -52,12 +51,7 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi
event.event(EventType.CLIENT_INFO);
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
if (auth.isAuthenticated()) {
auth.requireView(client);
} else {
authenticateClient(client);
}
ClientManager clientManager = new ClientManager(new RealmManager(session));
Object rep = clientManager.toInstallationRepresentation(session.getContext().getRealm(), client, session.getContext().getAuthServerUrl());
@ -80,29 +74,4 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi
public void close() {
}
private void authenticateClient(ClientModel client) {
if (client.isPublicClient()) {
return;
}
AuthenticationProcessor processor = AuthorizeClientUtil.getAuthenticationProcessor(session, event);
Response response = processor.authenticateClient();
if (response != null) {
event.client(client.getClientId()).error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
ClientModel authClient = processor.getClient();
if (client == null) {
event.client(client.getClientId()).error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
if (!authClient.getClientId().equals(client.getClientId())) {
event.client(client.getClientId()).error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
}
}

View file

@ -17,20 +17,28 @@
package org.keycloak.services.clientregistration;
import com.sun.xml.bind.v2.runtime.reflect.opt.Const;
import org.jboss.resteasy.spi.Failure;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.util.Time;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.*;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ForbiddenException;
import org.keycloak.util.TokenUtil;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -49,8 +57,6 @@ public class ClientRegistrationAuth {
public ClientRegistrationAuth(KeycloakSession session, EventBuilder event) {
this.session = session;
this.event = event;
init();
}
private void init() {
@ -67,41 +73,39 @@ public class ClientRegistrationAuth {
return;
}
jwt = ClientRegistrationTokenUtils.parseToken(realm, uri, split[1]);
jwt = ClientRegistrationTokenUtils.verifyToken(realm, uri, split[1]);
if (jwt == null) {
throw unauthorized();
}
if (isInitialAccessToken()) {
initialAccessModel = session.sessions().getClientInitialAccessModel(session.getContext().getRealm(), jwt.getId());
if (initialAccessModel == null) {
throw new ForbiddenException();
throw unauthorized();
}
}
}
public boolean isAuthenticated() {
return jwt != null;
}
public boolean isBearerToken() {
return TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType());
private boolean isBearerToken() {
return jwt != null && TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType());
}
public boolean isInitialAccessToken() {
return ClientRegistrationTokenUtils.TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType());
return jwt != null && ClientRegistrationTokenUtils.TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType());
}
public boolean isRegistrationAccessToken() {
return ClientRegistrationTokenUtils.TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType());
return jwt != null && ClientRegistrationTokenUtils.TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType());
}
public void requireCreate() {
if (!isAuthenticated()) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
init();
if (isBearerToken()) {
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.CREATE_CLIENT)) {
return;
} else {
throw forbidden();
}
} else if (isInitialAccessToken()) {
if (initialAccessModel.getRemainingCount() > 0) {
@ -111,58 +115,55 @@ public class ClientRegistrationAuth {
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
throw unauthorized();
}
public void requireView(ClientModel client) {
if (!isAuthenticated()) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (client == null) {
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
init();
if (isBearerToken()) {
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.VIEW_CLIENTS)) {
if (client == null) {
throw notFound();
}
return;
} else {
throw forbidden();
}
} else if (isRegistrationAccessToken()) {
if (client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) {
if (client.getRegistrationToken() != null && client != null && client.getRegistrationToken().equals(jwt.getId())) {
return;
}
} else if (isInitialAccessToken()) {
throw unauthorized();
} else {
if (authenticateClient(client)) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
throw unauthorized();
}
public void requireUpdate(ClientModel client) {
if (!isAuthenticated()) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (client == null) {
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
init();
if (isBearerToken()) {
if (hasRole(AdminRoles.MANAGE_CLIENTS)) {
if (client == null) {
throw notFound();
}
return;
} else {
throw forbidden();
}
} else if (isRegistrationAccessToken()) {
if (client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) {
if (client.getRegistrationToken() != null && client != null && client.getRegistrationToken().equals(jwt.getId())) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
throw unauthorized();
}
public ClientInitialAccessModel getInitialAccessModel() {
@ -207,4 +208,46 @@ public class ClientRegistrationAuth {
}
}
private boolean authenticateClient(ClientModel client) {
if (client.isPublicClient()) {
return true;
}
AuthenticationProcessor processor = AuthorizeClientUtil.getAuthenticationProcessor(session, event);
Response response = processor.authenticateClient();
if (response != null) {
event.client(client.getClientId()).error(Errors.NOT_ALLOWED);
throw unauthorized();
}
ClientModel authClient = processor.getClient();
if (client == null) {
event.client(client.getClientId()).error(Errors.NOT_ALLOWED);
throw unauthorized();
}
if (!authClient.getClientId().equals(client.getClientId())) {
event.client(client.getClientId()).error(Errors.NOT_ALLOWED);
throw unauthorized();
}
return true;
}
private Failure unauthorized() {
event.error(Errors.NOT_ALLOWED);
return new UnauthorizedException();
}
private Failure forbidden() {
event.error(Errors.NOT_ALLOWED);
return new ForbiddenException();
}
private Failure notFound() {
event.error(Errors.NOT_ALLOWED);
return new NotFoundException("Client not found");
}
}

View file

@ -28,7 +28,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.Urls;
import org.keycloak.util.TokenUtil;
@ -57,37 +56,37 @@ public class ClientRegistrationTokenUtils {
return createToken(realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getExpiration() > 0 ? model.getTimestamp() + model.getExpiration() : 0);
}
public static JsonWebToken parseToken(RealmModel realm, UriInfo uri, String token) {
public static JsonWebToken verifyToken(RealmModel realm, UriInfo uri, String token) {
JWSInput input;
try {
input = new JWSInput(token);
} catch (JWSInputException e) {
throw new ForbiddenException(e);
return null;
}
if (!RSAProvider.verify(input, realm.getPublicKey())) {
throw new ForbiddenException("Invalid signature");
return null;
}
JsonWebToken jwt;
try {
jwt = input.readJsonContent(JsonWebToken.class);
} catch (JWSInputException e) {
throw new ForbiddenException(e);
return null;
}
if (!getIssuer(realm, uri).equals(jwt.getIssuer())) {
throw new ForbiddenException("Issuer doesn't match");
return null;
}
if (!jwt.isActive()) {
throw new ForbiddenException("Expired token");
return null;
}
if (!(TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType()) ||
TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType()) ||
TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType()))) {
throw new ForbiddenException("Invalid token type");
return null;
}
return jwt;

View file

@ -34,52 +34,38 @@ public class DBLockManager {
protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
public void waitForLock(KeycloakSessionFactory sessionFactory) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
private final KeycloakSession session;
@Override
public void run(KeycloakSession session) {
DBLockProvider lock = getDBLock(session);
lock.waitForLock();
}
});
public DBLockManager(KeycloakSession session) {
this.session = session;
}
public void releaseLock(KeycloakSessionFactory sessionFactory) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
DBLockProvider lock = getDBLock(session);
lock.releaseLock();
}
});
}
public void checkForcedUnlock(KeycloakSessionFactory sessionFactory) {
public void checkForcedUnlock() {
if (Boolean.getBoolean("keycloak.dblock.forceUnlock")) {
DBLockProvider lock = getDBLock();
if (lock.supportsForcedUnlock()) {
logger.forcedReleaseDBLock();
releaseLock(sessionFactory);
lock.releaseLock();
} else {
throw new IllegalStateException("Forced unlock requested, but provider " + lock + " doesn't support it");
}
}
}
// Try to detect ID from realmProvider
public DBLockProvider getDBLock(KeycloakSession session) {
String realmProviderId = getRealmProviderId(session);
public DBLockProvider getDBLock() {
String realmProviderId = getRealmProviderId();
return session.getProvider(DBLockProvider.class, realmProviderId);
}
public DBLockProviderFactory getDBLockFactory(KeycloakSession session) {
String realmProviderId = getRealmProviderId(session);
public DBLockProviderFactory getDBLockFactory() {
String realmProviderId = getRealmProviderId();
return (DBLockProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(DBLockProvider.class, realmProviderId);
}
private String getRealmProviderId(KeycloakSession session) {
private String getRealmProviderId() {
RealmProviderFactory realmProviderFactory = (RealmProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(RealmProvider.class);
return realmProviderFactory.getId();
}

View file

@ -154,6 +154,10 @@ public class Messages {
public static final String IDENTITY_PROVIDER_LINK_SUCCESS = "identityProviderLinkSuccess";
public static final String STALE_CODE = "staleCodeMessage";
public static final String STALE_CODE_ACCOUNT = "staleCodeAccountMessage";
public static final String IDENTITY_PROVIDER_NOT_UNIQUE = "identityProviderNotUniqueMessage";
public static final String REALM_SUPPORTS_NO_CREDENTIALS = "realmSupportsNoCredentialsMessage";

View file

@ -16,7 +16,6 @@
*/
package org.keycloak.services.resources;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.OAuth2Constants;
@ -33,6 +32,7 @@ import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.broker.provider.IdentityProviderMapper;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
@ -143,7 +143,12 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
try {
ClientSessionCode clientSessionCode = parseClientSessionCode(code);
ParsedCodeContext parsedCode = parseClientSessionCode(code);
if (parsedCode.response != null) {
return parsedCode.response;
}
ClientSessionCode clientSessionCode = parsedCode.clientSessionCode;
IdentityProvider identityProvider = getIdentityProvider(session, realmModel, providerId);
Response response = identityProvider.performLogin(createAuthenticationRequest(providerId, clientSessionCode));
@ -242,14 +247,14 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
public Response authenticated(BrokeredIdentityContext context) {
ClientSessionCode clientCode = null;
IdentityProviderModel identityProviderConfig = context.getIdpConfig();
try {
clientCode = parseClientSessionCode(context.getCode());
} catch (Exception e) {
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_AUTHENTICATION_FAILED, e, identityProviderConfig.getProviderId());
ParsedCodeContext parsedCode = parseClientSessionCode(context.getCode());
if (parsedCode.response != null) {
return parsedCode.response;
}
ClientSessionCode clientCode = parsedCode.clientSessionCode;
String providerId = identityProviderConfig.getAlias();
if (!identityProviderConfig.isStoreToken()) {
if (isDebugEnabled()) {
@ -315,6 +320,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return Response.status(302).location(redirect).build();
} else {
Response response = validateUser(federatedUser, realmModel);
if (response != null) {
return response;
}
updateFederatedIdentity(context, federatedUser);
clientSession.setAuthenticatedUser(federatedUser);
@ -322,12 +332,27 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
}
public Response validateUser(UserModel user, RealmModel realm) {
if (!user.isEnabled()) {
event.error(Errors.USER_DISABLED);
return ErrorPage.error(session, Messages.ACCOUNT_DISABLED);
}
if (realm.isBruteForceProtected()) {
event.error(Errors.USER_TEMPORARILY_DISABLED);
return ErrorPage.error(session, Messages.ACCOUNT_DISABLED);
}
return null;
}
// Callback from LoginActionsService after first login with broker was done and Keycloak account is successfully linked/created
@GET
@Path("/after-first-broker-login")
public Response afterFirstBrokerLogin(@QueryParam("code") String code) {
ClientSessionCode clientCode = parseClientSessionCode(code);
ClientSessionModel clientSession = clientCode.getClientSession();
ParsedCodeContext parsedCode = parseClientSessionCode(code);
if (parsedCode.response != null) {
return parsedCode.response;
}
ClientSessionModel clientSession = parsedCode.clientSessionCode.getClientSession();
try {
this.event.detail(Details.CODE_ID, clientSession.getId())
@ -440,8 +465,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@GET
@Path("/after-post-broker-login")
public Response afterPostBrokerLoginFlow(@QueryParam("code") String code) {
ClientSessionCode clientCode = parseClientSessionCode(code);
ClientSessionModel clientSession = clientCode.getClientSession();
ParsedCodeContext parsedCode = parseClientSessionCode(code);
if (parsedCode.response != null) {
return parsedCode.response;
}
ClientSessionModel clientSession = parsedCode.clientSessionCode.getClientSession();
try {
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
@ -527,9 +555,15 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@Override
public Response cancelled(String code) {
ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel);
if (clientCode.getClientSession() == null || !clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return redirectToErrorPage(Messages.INVALID_CODE);
ParsedCodeContext parsedCode = parseClientSessionCode(code);
if (parsedCode.response != null) {
return parsedCode.response;
}
ClientSessionCode clientCode = parsedCode.clientSessionCode;
Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), Messages.CONSENT_DENIED);
if (accountManagementFailedLinking != null) {
return accountManagementFailedLinking;
}
return browserAuthentication(clientCode.getClientSession(), null);
@ -537,10 +571,17 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@Override
public Response error(String code, String message) {
ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel);
if (clientCode.getClientSession() == null || !clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return redirectToErrorPage(Messages.INVALID_CODE);
ParsedCodeContext parsedCode = parseClientSessionCode(code);
if (parsedCode.response != null) {
return parsedCode.response;
}
ClientSessionCode clientCode = parsedCode.clientSessionCode;
Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), message);
if (accountManagementFailedLinking != null) {
return accountManagementFailedLinking;
}
return browserAuthentication(clientCode.getClientSession(), message);
}
@ -601,36 +642,60 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
private ClientSessionCode parseClientSessionCode(String code) {
private ParsedCodeContext parseClientSessionCode(String code) {
ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel);
if (clientCode != null && clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
if (clientCode != null) {
ClientSessionModel clientSession = clientCode.getClientSession();
if (clientSession != null) {
if (clientSession.getUserSession() != null) {
this.event.session(clientSession.getUserSession());
}
ClientModel client = clientSession.getClient();
if (client == null) {
throw new IdentityBrokerException("Invalid client");
}
if (client != null) {
logger.debugf("Got authorization code from client [%s].", client.getClientId());
this.event.client(client);
this.session.getContext().setClient(client);
if (clientSession.getUserSession() != null) {
this.event.session(clientSession.getUserSession());
}
if (!clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
logger.debugf("Authorization code is not valid. Client session ID: %s, Client session's action: %s", clientSession.getId(), clientSession.getAction());
// Check if error happened during login or during linking from account management
Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), Messages.STALE_CODE_ACCOUNT);
Response staleCodeError = (accountManagementFailedLinking != null) ? accountManagementFailedLinking : redirectToErrorPage(Messages.STALE_CODE);
return ParsedCodeContext.response(staleCodeError);
}
if (isDebugEnabled()) {
logger.debugf("Authorization code is valid.");
}
return clientCode;
return ParsedCodeContext.clientSessionCode(clientCode);
}
}
throw new IdentityBrokerException("Invalid code, please login again through your client.");
logger.debugf("Authorization code is not valid. Code: %s", code);
Response staleCodeError = redirectToErrorPage(Messages.STALE_CODE);
return ParsedCodeContext.response(staleCodeError);
}
private Response checkAccountManagementFailedLinking(ClientSessionModel clientSession, String error, Object... parameters) {
if (clientSession.getUserSession() != null && clientSession.getClient() != null && clientSession.getClient().getClientId().equals(ACCOUNT_MANAGEMENT_CLIENT_ID)) {
this.event.event(EventType.FEDERATED_IDENTITY_LINK);
UserModel user = clientSession.getUserSession().getUser();
this.event.user(user);
this.event.detail(Details.USERNAME, user.getUsername());
return redirectToAccountErrorPage(clientSession, error, parameters);
} else {
return null;
}
}
private AuthenticationRequest createAuthenticationRequest(String providerId, ClientSessionCode clientSessionCode) {
@ -805,4 +870,22 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
this.session.getTransaction().rollback();
}
}
private static class ParsedCodeContext {
private ClientSessionCode clientSessionCode;
private Response response;
public static ParsedCodeContext clientSessionCode(ClientSessionCode clientSessionCode) {
ParsedCodeContext ctx = new ParsedCodeContext();
ctx.clientSessionCode = clientSessionCode;
return ctx;
}
public static ParsedCodeContext response(Response response) {
ParsedCodeContext ctx = new ParsedCodeContext();
ctx.response = response;
return ctx;
}
}
}

View file

@ -25,6 +25,7 @@ import org.keycloak.Config;
import org.keycloak.exportimport.ExportImportManager;
import org.keycloak.migration.MigrationModelManager;
import org.keycloak.models.*;
import org.keycloak.models.dblock.DBLockProvider;
import org.keycloak.services.managers.DBLockManager;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.models.utils.RepresentationToModel;
@ -82,7 +83,6 @@ public class KeycloakApplication extends Application {
singletons.add(new ServerVersionResource());
singletons.add(new RealmsResource());
singletons.add(new AdminRoot());
classes.add(QRCodeResource.class);
classes.add(ThemeResource.class);
classes.add(JsResource.class);
@ -92,9 +92,10 @@ public class KeycloakApplication extends Application {
ExportImportManager exportImportManager;
DBLockManager dbLockManager = new DBLockManager();
dbLockManager.checkForcedUnlock(sessionFactory);
dbLockManager.waitForLock(sessionFactory);
DBLockManager dbLockManager = new DBLockManager(sessionFactory.create());
dbLockManager.checkForcedUnlock();
DBLockProvider dbLock = dbLockManager.getDBLock();
dbLock.waitForLock();
try {
migrateModel();
@ -131,7 +132,7 @@ public class KeycloakApplication extends Application {
importAddUser();
} finally {
dbLockManager.releaseLock(sessionFactory);
dbLock.releaseLock();
}
if (exportImportManager.isRunExport()) {

View file

@ -1,106 +0,0 @@
/*
* 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.services.resources;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import org.keycloak.services.util.CacheControlUtil;
import javax.servlet.ServletException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import java.io.IOException;
import java.io.OutputStream;
/**
* Create a barcode image
*
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@Path("/qrcode")
public class QRCodeResource {
/**
* Create a bar code image
*
* @param contents
* @param size
* @return
* @throws ServletException
* @throws IOException
* @throws WriterException
*/
@GET
@Produces("image/png")
public Response createQrCode(@QueryParam("contents") String contents, @QueryParam("size") String size) throws ServletException, IOException, WriterException {
int width = 256;
int height = 256;
if (size != null) {
String[] s = size.split("x");
try {
width = Integer.parseInt(s[0]);
height = Integer.parseInt(s[1]);
} catch (Throwable t) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
}
if (contents == null) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
if (width > 1000 || height > 1000 || contents.length() > 1000) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
QRCodeWriter writer = new QRCodeWriter();
final BitMatrix bitMatrix = writer.encode(contents, BarcodeFormat.QR_CODE, width, height);
StreamingOutput stream = new StreamingOutput() {
@Override
public void write(OutputStream os) throws IOException,
WebApplicationException {
MatrixToImageWriter.writeToStream(bitMatrix, "png", os);
}
};
/*
* This response is served with extra headers that tell the browser to not do any caching.
* The reason is that this page will include a QR code that can give an attacker access to
* the time based tokens, so it's best to take precautions and make sure there are no copies
* of the QR code lost in a cache.
*/
CacheControl cacheControl = CacheControlUtil.noCache();
return Response.ok(stream) //
.cacheControl(cacheControl) //
.header("Pragma","no-cache") //
.header("Expires", "0") //
.build();
}
}

View file

@ -389,10 +389,16 @@ public class AuthenticationManagementResource {
String provider = data.get("provider");
// make sure provider is one of the registered providers
ProviderFactory f = session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, provider);
ProviderFactory f;
if (parentFlow.getProviderId().equals(AuthenticationFlow.CLIENT_FLOW)) {
f = session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, provider);
} else {
f = session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, provider);
}
if (f == null) {
throw new BadRequestException("No authentication provider found for id: " + provider);
}
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(parentFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);

View file

@ -95,10 +95,7 @@ public class ServerInfoAdminResource {
private void setProviders(ServerInfoRepresentation info) {
LinkedHashMap<String, SpiInfoRepresentation> spiReps = new LinkedHashMap<>();
List<Spi> spis = new LinkedList<>();
for (Spi spi : ServiceLoader.load(Spi.class)) {
spis.add(spi);
}
List<Spi> spis = new LinkedList<>(session.getKeycloakSessionFactory().getSpis());
Collections.sort(spis, new Comparator<Spi>() {
@Override
public int compare(Spi s1, Spi s2) {

View file

@ -0,0 +1,69 @@
/*
* 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.utils;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import org.keycloak.common.util.Base64;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.Base32;
import java.io.ByteArrayOutputStream;
import java.net.URLEncoder;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class TotpUtils {
public static String encode(String totpSecret) {
String encoded = Base32.encode(totpSecret.getBytes());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < encoded.length(); i += 4) {
sb.append(encoded.substring(i, i + 4 < encoded.length() ? i + 4 : encoded.length()));
if (i + 4 < encoded.length()) {
sb.append(" ");
}
}
return sb.toString();
}
public static String qrCode(String totpSecret, RealmModel realm, UserModel user) {
try {
String keyUri = realm.getOTPPolicy().getKeyURI(realm, user, totpSecret);
int width = 246;
int height = 246;
QRCodeWriter writer = new QRCodeWriter();
final BitMatrix bitMatrix = writer.encode(keyUri, BarcodeFormat.QR_CODE, width, height);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "png", bos);
bos.close();
return Base64.encodeBytes(bos.toByteArray());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View file

@ -74,7 +74,7 @@ public class URLProvider extends URLResourceProvider {
}
try {
if ("true".equals(System.getProperty("app.server.eap6"))) {
if ("eap6".equals(System.getProperty("app.server"))) {
if (url == null) {
url = new URL("http://localhost:8080/");
}

View file

@ -32,7 +32,7 @@ public abstract class AbstractPageWithInjectedUrl extends AbstractPage {
//EAP6 URL fix
protected URL createInjectedURL(String url) {
if (System.getProperty("app.server.eap6","false").equals("false")) {
if (!System.getProperty("app.server").equals("eap6")) {
return null;
}
try {

View file

@ -48,6 +48,11 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest {
.addAsWebInfResource(keycloakJSON, "keycloak.json")
.addAsWebInfResource(jbossDeploymentStructure, JBOSS_DEPLOYMENT_STRUCTURE_XML);
URL keystore = AbstractServletsAdapterTest.class.getResource(webInfPath + "keystore.jks");
if (keystore != null) {
deployment.addAsWebInfResource(keystore, "classes/keystore.jks");
}
addContextXml(deployment, name);
return deployment;

View file

@ -344,16 +344,8 @@ public abstract class AbstractDemoExampleAdapterTest extends AbstractExampleAdap
String serverLogPath = null;
if (System.getProperty("app.server.wildfly", "false").equals("true")) {
serverLogPath = System.getProperty("app.server.wildfly.home") + "/standalone/log/server.log";
}
if (System.getProperty("app.server.eap6", "false").equals("true")) {
serverLogPath = System.getProperty("app.server.eap6.home") + "/standalone/log/server.log";
}
if (System.getProperty("app.server.eap7", "false").equals("true")) {
serverLogPath = System.getProperty("app.server.eap7.home") + "/standalone/log/server.log";
if (System.getProperty("app.server").equals("wildfly") || System.getProperty("app.server").equals("eap6") || System.getProperty("app.server").equals("eap")) {
serverLogPath = System.getProperty("app.server.home") + "/standalone/log/server.log";
}
String appServerUrl;
@ -364,6 +356,7 @@ public abstract class AbstractDemoExampleAdapterTest extends AbstractExampleAdap
}
if (serverLogPath != null) {
log.info("Checking app server log at: " + serverLogPath);
File serverLog = new File(serverLogPath);
String serverLogContent = FileUtils.readFileToString(serverLog);
UserRepresentation bburke = ApiUtil.findUserByUsername(testRealmResource(), "bburke@redhat.com");
@ -373,6 +366,8 @@ public abstract class AbstractDemoExampleAdapterTest extends AbstractExampleAdap
assertTrue(matcher.find());
assertTrue(serverLogContent.contains("User '" + bburke.getId() + "' invoking '" + appServerUrl + "database/customers' on client 'database-service'"));
} else {
log.info("Checking app server log on app-server: \"" + System.getProperty("app.server") + "\" is not supported.");
}
}
}

View file

@ -168,6 +168,7 @@ public abstract class AbstractJSConsoleExampleAdapterTest extends AbstractExampl
jsConsoleExamplePage.init();
jsConsoleExamplePage.getProfile();
pause(500);
assertTrue(jsConsoleExamplePage.getOutputText().contains("Failed to load profile"));
jsConsoleExamplePage.logIn();
@ -306,7 +307,7 @@ public abstract class AbstractJSConsoleExampleAdapterTest extends AbstractExampl
logInAndInit("implicit");
pause(5000);
pause(6000);
assertTrue(jsConsoleExamplePage.getEventsText().contains("Access token expired"));
}

View file

@ -25,7 +25,6 @@ import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Version;
import org.keycloak.common.util.Time;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.VersionRepresentation;
@ -49,6 +48,7 @@ import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
import static org.keycloak.testsuite.util.WaitUtils.pause;
/**
*
@ -226,7 +226,8 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
demoRealmRep.setSsoSessionIdleTimeout(1);
testRealmResource().update(demoRealmRep);
// Thread.sleep(2000);
pause(2000);
productPortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
@ -253,16 +254,16 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
demoRealmRep.setSsoSessionIdleTimeout(1);
testRealmResource().update(demoRealmRep);
Time.setOffset(2);
pause(2000);
productPortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
// need to cleanup so other tests don't fail, so invalidate http sessions on remote clients.
demoRealmRep.setSsoSessionIdleTimeout(originalIdle);
testRealmResource().update(demoRealmRep);
// note: sessions invalidated after each test, see: AbstractKeycloakTest.afterAbstractKeycloakTest()
Time.setOffset(0);
}
@Test

View file

@ -0,0 +1,65 @@
/*
* 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.testsuite.admin;
import org.junit.Test;
import org.keycloak.common.Version;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.info.ServerInfoRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import java.util.List;
import static org.junit.Assert.*;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ServerInfoTest extends AbstractKeycloakTest {
@Test
public void testServerInfo() {
ServerInfoRepresentation info = adminClient.serverInfo().getInfo();
assertNotNull(info);
assertNotNull(info.getProviders());
assertNotNull(info.getProviders().get("realm"));
assertNotNull(info.getProviders().get("user"));
assertNotNull(info.getProviders().get("authenticator"));
assertNotNull(info.getThemes());
assertNotNull(info.getThemes().get("account"));
assertNotNull(info.getThemes().get("admin"));
assertNotNull(info.getThemes().get("email"));
assertNotNull(info.getThemes().get("login"));
assertNotNull(info.getThemes().get("welcome"));
assertNotNull(info.getEnums());
assertNotNull(info.getMemoryInfo());
assertNotNull(info.getSystemInfo());
assertEquals(Version.VERSION, info.getSystemInfo().getVersion());
assertNotNull(info.getSystemInfo().getServerTime());
assertNotNull(info.getSystemInfo().getUptime());
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
}
}

View file

@ -39,6 +39,7 @@ import java.io.IOException;
import java.net.URI;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
@ -189,19 +190,6 @@ public class GroupTest extends AbstractGroupTest {
Assert.assertEquals(1, level3Group.getRealmRoles().size());
Assert.assertTrue(level3Group.getRealmRoles().contains("level3Role"));
try {
GroupRepresentation notFound = realm.getGroupByPath("/notFound");
Assert.fail();
} catch (NotFoundException e) {
}
try {
GroupRepresentation notFound = realm.getGroupByPath("/top/notFound");
Assert.fail();
} catch (NotFoundException e) {
}
UserRepresentation user = realm.users().search("direct-login", -1, -1).get(0);
realm.users().get(user.getId()).joinGroup(level3Group.getId());
List<GroupRepresentation> membership = realm.users().get(user.getId()).groups();
@ -232,5 +220,27 @@ public class GroupTest extends AbstractGroupTest {
realm.removeDefaultGroup(level3Group.getId());
defaultGroups = realm.getDefaultGroups();
Assert.assertEquals(0, defaultGroups.size());
realm.groups().group(topGroup.getId()).remove();
try {
realm.getGroupByPath("/top/level2/level3");
Assert.fail("Group should not have been found");
}
catch (NotFoundException e) {}
try {
realm.getGroupByPath("/top/level2");
Assert.fail("Group should not have been found");
}
catch (NotFoundException e) {}
try {
realm.getGroupByPath("/top");
Assert.fail("Group should not have been found");
}
catch (NotFoundException e) {}
Assert.assertNull(login("direct-login", "resource-owner", "secret", user.getId()).getRealmAccess());
}
}

View file

@ -103,9 +103,9 @@ public class AdapterInstallationConfigTest extends AbstractClientRegistrationTes
try {
reg.getAdapterConfig(client.getClientId());
fail("Expected 403");
fail("Expected 401");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
@ -115,9 +115,9 @@ public class AdapterInstallationConfigTest extends AbstractClientRegistrationTes
try {
reg.getAdapterConfig(client2.getClientId());
fail("Expected 403");
fail("Expected 401");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}

View file

@ -126,8 +126,14 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
@Test
public void getClientNotFound() throws ClientRegistrationException {
authManageClients();
assertNull(reg.get("invalid"));
}
@Test
public void getClientNotFoundNoAccess() throws ClientRegistrationException {
authNoAccess();
try {
reg.get(CLIENT_ID);
reg.get("invalid");
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
@ -181,10 +187,14 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
public void updateClientNotFound() throws ClientRegistrationException {
authManageClients();
try {
updateClient();
fail("Expected 403");
ClientRepresentation client = new ClientRepresentation();
client.setClientId("invalid");
reg.update(client);
fail("Expected 404");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
assertEquals(404, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}

View file

@ -58,7 +58,7 @@ public class InitialAccessTokenTest extends AbstractClientRegistrationTest {
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
Assert.assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
@ -79,7 +79,7 @@ public class InitialAccessTokenTest extends AbstractClientRegistrationTest {
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
Assert.assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
@ -113,7 +113,7 @@ public class InitialAccessTokenTest extends AbstractClientRegistrationTest {
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
Assert.assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}

View file

@ -59,8 +59,8 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
try {
reg.get(client.getClientId());
fail("Expected 403");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
} catch (Exception e) {
assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
return null;
@ -82,9 +82,9 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
reg.auth(Auth.token("invalid"));
try {
reg.get(client.getClientId());
fail("Expected 403");
fail("Expected 401");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
@ -109,9 +109,9 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
reg.auth(Auth.token("invalid"));
try {
reg.update(client);
fail("Expected 403");
fail("Expected 401");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
assertEquals("http://root", getClient(client.getId()).getRootUrl());
@ -128,9 +128,9 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
reg.auth(Auth.token("invalid"));
try {
reg.delete(client);
fail("Expected 403");
fail("Expected 401");
} catch (ClientRegistrationException e) {
assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
assertNotNull(getClient(client.getId()));
}

View file

@ -174,8 +174,8 @@
"redirectUris": [
"/secure-portal/*"
],
"attributes": {
"jwt.credential.certificate": "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="
"attributes" : {
"jwt.credential.certificate" : "MIICqTCCAZECBgFT0Ngs/DANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zZWN1cmUtcG9ydGFsMB4XDTE2MDQwMTA4MDA0MVoXDTI2MDQwMTA4MDIyMVowGDEWMBQGA1UEAwwNc2VjdXJlLXBvcnRhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJa4GixpmzP511AmI0eLPLORyJwXS8908MUvdG3hmh8jMOIhe28XjIFeZSY09vFxh22F2SUMjxU/B2Hw4PDJUkebuNR7rXhOIYCJAo6eEZzjSBY/wngFtfm74zJ/eLCobBtDvIld7jobdHTfE1Oz9+GzvtG0k7cm7ubrLT0J4I1UsFZj3b//3wa+O0vNaTwHC1Jz/m59VbtXqyO4xEzIdl416cnGCmEmk5qd5h1de2UoLi/CTad8HftIJhzN1qhlySzW/9Ha70aYlDH2hiibDsXDTrNaMdaaLik7I8Rv/nIbggysG863PKZo8wknDe62QctH5VYSSktiy4gjSJkGh7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZnnx+AHQ8txugGcFK8gWjildDgk+v31fBHBDvmLQaSzsUaIOJaK4wnlwUI+VfR46HmBXhjlDCobFLUptd+kz0G7xapcIn3b5jLrySUUD7L+LAp1vNOQU4mKhTGS3IEvNB73D3GH9rQ+M3KEcoN3f99fNKqKsUdxbmZqGf4VOQ57PUfLBw4PJJGlROPosBc7ivPRyeYnKekhoCTynq30BAD1FA1BA8ppcY4ZVGADPTAgMJxpglpFY9LiqCwdLAGW1ttnsyIJ7DpT+kybhhk7c+MU7gyQdv8xPnMR0bSCB9hndowgBn5oZ393aMscwMNCzwJ0aWBs1sUyn3X0RIsu9Jg=="
}
},
{

View file

@ -1,10 +1,17 @@
{
"realm" : "demo",
"resource" : "secure-portal",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url" : "http://localhost:8180/auth",
"ssl-required" : "external",
"credentials" : {
"secret": "password"
"realm": "demo",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "http://localhost:8180/auth",
"ssl-required": "external",
"resource": "secure-portal",
"credentials": {
"jwt": {
"client-key-password": "password",
"client-keystore-file": "classpath:keystore.jks",
"client-keystore-password": "password",
"client-key-alias": "secure-portal",
"token-timeout": 10,
"client-keystore-type": "jks"
}
}
}

View file

@ -754,7 +754,7 @@
<dependency>
<groupId>org.jboss.arquillian.container</groupId>
<artifactId>undertow-embedded</artifactId>
<version>1.0.0.Alpha1-SNAPSHOT</version>
<version>1.0.0.Alpha2</version>
</dependency>
<dependency>

View file

@ -27,6 +27,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.services.managers.RealmManager;
@ -35,6 +36,7 @@ import org.keycloak.util.JsonSerialization;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
@ -441,4 +443,22 @@ public class RealmTest extends AbstractClientTest {
assertEquals(certificate, rep.getCertificate());
}
@Test
// KEYCLOAK-2700
public void deleteRealmWithDefaultGroups() throws IOException {
RealmRepresentation rep = new RealmRepresentation();
rep.setRealm("foo");
GroupRepresentation group = new GroupRepresentation();
group.setName("default1");
group.setPath("/default1");
rep.setGroups(Collections.singletonList(group));
rep.setDefaultGroups(Collections.singletonList("/default1"));
keycloak.realms().create(rep);
keycloak.realm(rep.getRealm()).remove();
}
}

View file

@ -1,61 +0,0 @@
/*
* 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.testsuite.admin;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.common.Version;
import org.keycloak.representations.info.ServerInfoRepresentation;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ServerInfoTest extends AbstractClientTest {
@Rule
public WebRule webRule = new WebRule(this);
@WebResource
protected WebDriver driver;
@WebResource
protected OAuthClient oauth;
@Test
public void testServerInfo() {
ServerInfoRepresentation info = keycloak.serverInfo().getInfo();
Assert.assertNotNull(info);
Assert.assertNotNull(info.getProviders());
Assert.assertNotNull(info.getThemes());
Assert.assertNotNull(info.getEnums());
Assert.assertNotNull(info.getMemoryInfo());
Assert.assertNotNull(info.getSystemInfo());
Assert.assertEquals(Version.VERSION, info.getSystemInfo().getVersion());
Assert.assertNotNull(info.getSystemInfo().getServerTime());
Assert.assertNotNull(info.getSystemInfo().getUptime());
}
}

View file

@ -239,6 +239,20 @@ public class UserTest extends AbstractClientTest {
assertEquals(9, count.intValue());
}
@Test
public void delete() {
Response response = realm.users().delete( createUser() );
assertEquals(204, response.getStatus());
response.close();
}
@Test
public void deleteNonExistent() {
Response response = realm.users().delete( "does-not-exist" );
assertEquals(404, response.getStatus());
response.close();
}
@Test
public void searchPaginated() {
createUsers();

View file

@ -40,6 +40,7 @@ import org.keycloak.testsuite.broker.util.UserSessionStatusServlet.UserSessionSt
import org.keycloak.testsuite.pages.AccountFederatedIdentityPage;
import org.keycloak.testsuite.pages.AccountPasswordPage;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
@ -114,6 +115,9 @@ public abstract class AbstractIdentityProviderTest {
@WebResource
protected AccountFederatedIdentityPage accountFederatedIdentityPage;
@WebResource
protected ErrorPage errorPage;
protected KeycloakSession session;
protected int logoutTimeOffset = 0;
@ -173,10 +177,6 @@ public abstract class AbstractIdentityProviderTest {
Time.setOffset(0);
}
String afterLogoutUrl = driver.getCurrentUrl();
String afterLogoutPageSource = driver.getPageSource();
System.out.println("afterLogoutUrl: " + afterLogoutUrl);
//System.out.println("after logout page source: " + afterLogoutPageSource);
driver.navigate().to("http://localhost:8081/test-app");
@ -220,7 +220,6 @@ public abstract class AbstractIdentityProviderTest {
String currentUrl = this.driver.getCurrentUrl();
assertTrue(currentUrl.startsWith("http://localhost:8082/auth/"));
System.out.println(this.driver.getCurrentUrl());
// log in to identity provider
this.loginPage.login(username, "password");
doAfterProviderAuthentication();

View file

@ -38,6 +38,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserFederationProviderModel;
@ -69,6 +70,73 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
Assert.assertEquals("617-666-7777", user.getFirstAttribute("mobile"));
}
@Test
public void testDisabledUser() {
setUpdateProfileFirstLogin(session.realms().getRealmByName("realm-with-broker"), IdentityProviderRepresentation.UPFLM_OFF);
driver.navigate().to("http://localhost:8081/test-app");
loginPage.clickSocial(getProviderId());
loginPage.login("test-user", "password");
System.out.println(driver.getPageSource());
driver.navigate().to("http://localhost:8081/test-app/logout");
try {
KeycloakSession session = brokerServerRule.startSession();
session.users().getUserByUsername("test-user", session.realms().getRealmByName("realm-with-broker")).setEnabled(false);
brokerServerRule.stopSession(session, true);
driver.navigate().to("http://localhost:8081/test-app");
loginPage.clickSocial(getProviderId());
loginPage.login("test-user", "password");
assertTrue(errorPage.isCurrent());
assertEquals("Account is disabled, contact admin.", errorPage.getError());
} finally {
KeycloakSession session = brokerServerRule.startSession();
session.users().getUserByUsername("test-user", session.realms().getRealmByName("realm-with-broker")).setEnabled(true);
brokerServerRule.stopSession(session, true);
}
}
@Test
public void testTemporarilyDisabledUser() {
setUpdateProfileFirstLogin(session.realms().getRealmByName("realm-with-broker"), IdentityProviderRepresentation.UPFLM_OFF);
driver.navigate().to("http://localhost:8081/test-app");
loginPage.clickSocial(getProviderId());
loginPage.login("test-user", "password");
driver.navigate().to("http://localhost:8081/test-app/logout");
try {
KeycloakSession session = brokerServerRule.startSession();
RealmModel brokerRealm = session.realms().getRealmByName("realm-with-broker");
brokerRealm.setBruteForceProtected(true);
brokerRealm.setFailureFactor(2);
brokerServerRule.stopSession(session, true);
driver.navigate().to("http://localhost:8081/test-app");
loginPage.login("test-user", "fail");
loginPage.login("test-user", "fail");
driver.navigate().to("http://localhost:8081/test-app");
assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
loginPage.clickSocial(getProviderId());
loginPage.login("test-user", "password");
assertTrue(errorPage.isCurrent());
assertEquals("Account is disabled, contact admin.", errorPage.getError());
} finally {
KeycloakSession session = brokerServerRule.startSession();
RealmModel brokerRealm = session.realms().getRealmByName("realm-with-broker");
brokerRealm.setBruteForceProtected(false);
brokerRealm.setFailureFactor(0);
brokerServerRule.stopSession(session, true);
}
}
@Test
public void testSuccessfulAuthenticationUpdateProfileOnMissing_nothingMissing() {
IdentityProviderModel identityProviderModel = getIdentityProviderModel();
@ -362,7 +430,6 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
revokeGrant();
// Logout from account management
System.out.println("*** logout from account management");
accountFederatedIdentityPage.logout();
assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
@ -502,7 +569,7 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
driver.navigate().to("http://localhost:8081/test-app/logout");
String currentUrl = this.driver.getCurrentUrl();
System.out.println("after logout currentUrl: " + currentUrl);
// System.out.println("after logout currentUrl: " + currentUrl);
assertTrue(currentUrl.startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
unconfigureUserRetrieveToken("test-user");

View file

@ -20,7 +20,6 @@ package org.keycloak.testsuite.broker;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.AccessTokenResponse;
@ -30,7 +29,6 @@ import org.keycloak.services.Urls;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.pages.AccountApplicationsPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.KeycloakServer;
@ -38,7 +36,6 @@ import org.keycloak.util.JsonSerialization;
import org.openqa.selenium.NoSuchElementException;
import java.io.IOException;
import java.util.List;
import javax.ws.rs.core.UriBuilder;
@ -71,9 +68,6 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
}
};
@WebResource
private OAuthGrantPage grantPage;
@WebResource
protected AccountApplicationsPage accountApplicationsPage;
@ -122,6 +116,16 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
super.testSuccessfulAuthentication();
}
@Test
public void testDisabledUser() {
super.testDisabledUser();
}
@Test
public void testTemporarilyDisabledUser() {
super.testTemporarilyDisabledUser();
}
@Test
public void testLogoutWorksWithTokenTimeout() {
Keycloak keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);

View file

@ -0,0 +1,264 @@
/*
* 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.testsuite.broker;
import java.util.List;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.openqa.selenium.NoSuchElementException;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCKeycloakServerBrokerWithConsentTest extends AbstractIdentityProviderTest {
private static final int PORT = 8082;
private static Keycloak keycloak1;
private static Keycloak keycloak2;
@ClassRule
public static AbstractKeycloakRule oidcServerRule = new AbstractKeycloakRule() {
@Override
protected void configureServer(KeycloakServer server) {
server.getConfig().setPort(PORT);
}
@Override
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json"));
// Disable update profile
RealmModel realm = getRealm(session);
setUpdateProfileFirstLogin(realm, IdentityProviderRepresentation.UPFLM_OFF);
}
@Override
protected String[] getTestRealms() {
return new String[] { "realm-with-oidc-identity-provider" };
}
};
@BeforeClass
public static void before() {
keycloak1 = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
keycloak2 = Keycloak.getInstance("http://localhost:8082/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
// Require broker to show consent screen
RealmResource brokeredRealm = keycloak2.realm("realm-with-oidc-identity-provider");
List<ClientRepresentation> clients = brokeredRealm.clients().findByClientId("broker-app");
Assert.assertEquals(1, clients.size());
ClientRepresentation brokerApp = clients.get(0);
brokerApp.setConsentRequired(true);
brokeredRealm.clients().get(brokerApp.getId()).update(brokerApp);
// Change timeouts on realm-with-broker to lower values
RealmResource realmWithBroker = keycloak1.realm("realm-with-broker");
RealmRepresentation realmRep = realmWithBroker.toRepresentation();
realmRep.setAccessCodeLifespanLogin(30);;
realmRep.setAccessCodeLifespan(30);
realmRep.setAccessCodeLifespanUserAction(30);
realmWithBroker.update(realmRep);
}
@Override
protected String getProviderId() {
return "kc-oidc-idp";
}
// KEYCLOAK-2769
@Test
public void testConsentDeniedWithExpiredClientSession() throws Exception {
// Login to broker
loginIDP("test-user");
// Set time offset
Time.setOffset(60);
try {
// User rejected consent
grantPage.assertCurrent();
grantPage.cancel();
// Assert error page with backToApplication link displayed
errorPage.assertCurrent();
errorPage.clickBackToApplication();
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
} finally {
Time.setOffset(0);
}
}
// KEYCLOAK-2769
@Test
public void testConsentDeniedWithExpiredAndClearedClientSession() throws Exception {
// Login to broker again
loginIDP("test-user");
// Set time offset
Time.setOffset(60);
try {
// Manually remove expiredSessions TODO: Will require custom endpoint when migrate to integration-arquillian
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
session.sessions().removeExpired(getRealm());
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
// User rejected consent
grantPage.assertCurrent();
grantPage.cancel();
// Assert error page without backToApplication link (clientSession expired and was removed on the server)
errorPage.assertCurrent();
try {
errorPage.clickBackToApplication();
fail("Not expected to have link backToApplication available");
} catch (NoSuchElementException nsee) {
// Expected;
}
} finally {
Time.setOffset(0);
}
}
// KEYCLOAK-2801
@Test
public void testAccountManagementLinkingAndExpiredClientSession() throws Exception {
// Login as pedroigor to account management
loginToAccountManagement("pedroigor");
// Link my "pedroigor" identity with "test-user" from brokered Keycloak
accountFederatedIdentityPage.clickAddProvider(getProviderId());
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
this.loginPage.login("test-user", "password");
// Set time offset
Time.setOffset(60);
try {
// User rejected consent
grantPage.assertCurrent();
grantPage.cancel();
// Assert account error page with "staleCodeAccount" error displayed
accountFederatedIdentityPage.assertCurrent();
Assert.assertEquals("The page expired. Please try one more time.", accountFederatedIdentityPage.getError());
// Try to link one more time
accountFederatedIdentityPage.clickAddProvider(getProviderId());
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
this.loginPage.login("test-user", "password");
Time.setOffset(120);
// User granted consent
grantPage.assertCurrent();
grantPage.accept();
// Assert account error page with "staleCodeAccount" error displayed
accountFederatedIdentityPage.assertCurrent();
Assert.assertEquals("The page expired. Please try one more time.", accountFederatedIdentityPage.getError());
} finally {
Time.setOffset(0);
}
// Revoke consent
RealmResource brokeredRealm = keycloak2.realm("realm-with-oidc-identity-provider");
List<UserRepresentation> users = brokeredRealm.users().search("test-user", 0, 1);
brokeredRealm.users().get(users.get(0).getId()).revokeConsent("broker-app");
}
@Test
public void testLoginCancelConsent() throws Exception {
// Try to login
loginIDP("test-user");
// User rejected consent
grantPage.assertCurrent();
grantPage.cancel();
// Assert back on login page
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/"));
assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
}
// KEYCLOAK-2802
@Test
public void testAccountManagementLinkingCancelConsent() throws Exception {
// Login as pedroigor to account management
loginToAccountManagement("pedroigor");
// Link my "pedroigor" identity with "test-user" from brokered Keycloak
accountFederatedIdentityPage.clickAddProvider(getProviderId());
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
this.loginPage.login("test-user", "password");
// User rejected consent
grantPage.assertCurrent();
grantPage.cancel();
// Assert account error page with "consentDenied" error displayed
accountFederatedIdentityPage.assertCurrent();
Assert.assertEquals("Consent denied.", accountFederatedIdentityPage.getError());
}
private void loginToAccountManagement(String username) {
accountFederatedIdentityPage.realm("realm-with-broker");
accountFederatedIdentityPage.open();
assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
loginPage.login(username, "password");
assertTrue(accountFederatedIdentityPage.isCurrent());
}
}

View file

@ -54,17 +54,17 @@ public class DBLockTest extends AbstractModelTest {
super.before();
// Set timeouts for testing
DBLockManager lockManager = new DBLockManager();
DBLockProviderFactory lockFactory = lockManager.getDBLockFactory(session);
DBLockManager lockManager = new DBLockManager(session);
DBLockProviderFactory lockFactory = lockManager.getDBLockFactory();
lockFactory.setTimeouts(LOCK_RECHECK_MILLIS, LOCK_TIMEOUT_MILLIS);
// Drop lock table, just to simulate racing threads for create lock table and insert lock record into it.
lockManager.getDBLock(session).destroyLockInfo();
lockManager.getDBLock().destroyLockInfo();
commit();
}
// @Test // TODO: Running -Dtest=DBLockTest,UserModelTest might cause issues sometimes. Reenable this once DB lock is refactored.
@Test
public void testLockConcurrently() throws Exception {
long startupTime = System.currentTimeMillis();
@ -112,7 +112,7 @@ public class DBLockTest extends AbstractModelTest {
}
private void lock(KeycloakSession session, Semaphore semaphore) {
DBLockProvider dbLock = new DBLockManager().getDBLock(session);
DBLockProvider dbLock = new DBLockManager(session).getDBLock();
dbLock.waitForLock();
try {
semaphore.increase();

View file

@ -16,8 +16,10 @@
*/
package org.keycloak.testsuite.pages;
import org.junit.Assert;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.rule.WebResource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@ -32,12 +34,19 @@ public class ErrorPage extends AbstractPage {
@FindBy(className = "instruction")
private WebElement errorMessage;
@FindBy(id = "backToApplication")
private WebElement backToApplicationLink;
public String getError() {
return errorMessage.getText();
}
public void clickBackToApplication() {
backToApplicationLink.click();
}
public boolean isCurrent() {
return driver.getTitle().equals("We're sorry...");
return driver.getTitle() != null && driver.getTitle().equals("We're sorry...");
}
@Override

View file

@ -135,6 +135,8 @@ federatedIdentityRemovingLastProviderMessage=You can''t remove last federated id
identityProviderRedirectErrorMessage=Failed to redirect to identity provider.
identityProviderRemovedMessage=Identity provider removed successfully.
identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user.
staleCodeAccountMessage=The page expired. Please try one more time.
consentDenied=Consent denied.
accountDisabledMessage=Account is disabled, contact admin.

View file

@ -30,7 +30,7 @@
</li>
<li>
<p>${msg("totpStep2")}</p>
<img src="${totp.totpSecretQrCodeUrl}" alt="Figure: Barcode"><br/>
<img src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"><br/>
<span class="code">${totp.totpSecretEncoded}</span>
</li>
<li>

View file

@ -1,6 +1,8 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<div>
<h1 data-ng-show="parentFlow.providerId == 'basic-flow'">{{:: 'create-authenticator-execution' | translate}}</h1>
<h1 data-ng-show="parentFlow.providerId == 'client-flow'">{{:: 'create-authenticator-execution' | translate}}</h1>
<h1 data-ng-show="parentFlow.providerId == 'for-flow'">{{:: 'create-form-action-execution' | translate}}</h1>
</div>
<kc-tabs-authentication></kc-tabs-authentication>

View file

@ -8,7 +8,7 @@
<div id="kc-error-message">
<p class="instruction">${message.summary}</p>
<#if client?? && client.baseUrl?has_content>
<p><a href="${client.baseUrl}">${msg("backToApplication")}</a></p>
<p><a id="backToApplication" href="${client.baseUrl}">${msg("backToApplication")}</a></p>
</#if>
</div>
</#if>

View file

@ -34,7 +34,7 @@
</li>
<li>
<p>${msg("loginTotpStep2")}</p>
<img src="${totp.totpSecretQrCodeUrl}" alt="Figure: Barcode"><br/>
<img src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"><br/>
<span class="code">${totp.totpSecretEncoded}</span>
</li>
<li>

View file

@ -202,6 +202,7 @@ invalidCodeMessage=An error occurred, please login again through your applicatio
identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider
identityProviderNotFoundMessage=Could not find an identity provider with the identifier.
identityProviderLinkSuccess=Your account was successfully linked with {0} account {1} .
staleCodeMessage=This page is no longer valid, please go back to your application and login again
realmSupportsNoCredentialsMessage=Realm does not support any credential type.
identityProviderNotUniqueMessage=Realm supports multiple identity providers. Could not determine which identity provider should be used to authenticate with.
emailVerifiedMessage=Your email address has been verified.

View file

@ -204,17 +204,20 @@ ol li {
ol li img {
margin-top: 15px;
width: 180px;
margin-bottom: 5px;
border: 1px solid #eee;
}
ol li span {
bottom: 80px;
left: 200px;
padding: 15px;
background-color: #f5f5f5;
border: 1px solid #eee;
top: 46px;
left: 270px;
right: 50px;
position: absolute;
font-family: courier, monospace;
font-size: 13px;
font-size: 25px;
}
hr + .form-horizontal {