Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
515ed226be
67 changed files with 1209 additions and 577 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
53
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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).
|
||||
* */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
47
services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java
Executable file → Normal file
47
services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java
Executable file → Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
69
services/src/main/java/org/keycloak/utils/TotpUtils.java
Normal file
69
services/src/main/java/org/keycloak/utils/TotpUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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/");
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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=="
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue