Make machine token signing key per-workspace & renew them after each ws restart

6.19.x
Max Shaposhnik 2018-08-20 09:51:04 +03:00 committed by GitHub
parent 6bc44e9512
commit 47b8ed328b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 997 additions and 378 deletions

View File

@ -43,6 +43,7 @@ import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.workspace.config.MachineConfig;
import org.eclipse.che.api.core.model.workspace.config.ServerConfig;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
@ -208,10 +209,13 @@ public class JwtProxyProvisioner {
k8sEnv.getMachines().put(JWT_PROXY_MACHINE_NAME, createJwtProxyMachine());
k8sEnv.getPods().put(JWT_PROXY_POD_NAME, createJwtProxyPod(identity));
KeyPair keyPair = signatureKeyManager.getKeyPair();
if (keyPair == null) {
KeyPair keyPair;
try {
keyPair = signatureKeyManager.getKeyPair(identity.getWorkspaceId());
} catch (ServerException e) {
throw new InternalInfrastructureException(
"Key pair for machine authentication does not exist");
"Signature key pair for machine authentication cannot be retrieved. Reason: "
+ e.getMessage());
}
Map<String, String> initConfigMapData = new HashMap<>();
initConfigMapData.put(

View File

@ -19,6 +19,7 @@ import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.
import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.PUBLIC_KEY_FOOTER;
import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.PUBLIC_KEY_HEADER;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@ -73,8 +74,8 @@ public class JwtProxyProvisionerTest {
private KubernetesEnvironment k8sEnv;
@BeforeMethod
public void setUp() {
when(signatureKeyManager.getKeyPair()).thenReturn(new KeyPair(publicKey, null));
public void setUp() throws Exception {
when(signatureKeyManager.getKeyPair(anyString())).thenReturn(new KeyPair(publicKey, null));
when(publicKey.getEncoded()).thenReturn("publickey".getBytes());
when(configBuilderFactory.create(any()))

View File

@ -138,6 +138,16 @@
<artifactId>che-core-sql-schema</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-api-authorization</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-api-authorization-impl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-api-organization</artifactId>
@ -158,6 +168,11 @@
<artifactId>che-multiuser-api-resource</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-machine-authentication</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-permission-workspace</artifactId>

View File

@ -20,6 +20,7 @@ import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjec
import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createFreeResourcesLimit;
import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createPreferences;
import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createProfile;
import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createSignatureKeyPair;
import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createSshPair;
import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createStack;
import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createUser;
@ -44,6 +45,7 @@ import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
import com.google.inject.persist.jpa.JpaPersistModule;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
@ -95,8 +97,13 @@ import org.eclipse.che.core.db.cascade.event.CascadeEvent;
import org.eclipse.che.core.db.schema.SchemaInitializer;
import org.eclipse.che.core.db.schema.impl.flyway.FlywaySchemaInitializer;
import org.eclipse.che.inject.lifecycle.InitModule;
import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
import org.eclipse.che.multiuser.api.permission.server.PermissionCheckerImpl;
import org.eclipse.che.multiuser.api.permission.server.PermissionsManager;
import org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions;
import org.eclipse.che.multiuser.api.permission.server.spi.PermissionsDao;
import org.eclipse.che.multiuser.machine.authentication.server.MachineAuthModule;
import org.eclipse.che.multiuser.machine.authentication.server.signature.spi.SignatureKeyDao;
import org.eclipse.che.multiuser.organization.api.OrganizationJpaModule;
import org.eclipse.che.multiuser.organization.api.OrganizationManager;
import org.eclipse.che.multiuser.organization.api.listener.RemoveOrganizationOnLastUserRemovedEventSubscriber;
@ -147,6 +154,7 @@ public class JpaEntitiesCascadeRemovalTest {
private FactoryDao factoryDao;
private StackDao stackDao;
private WorkerDao workerDao;
private SignatureKeyDao signatureKeyDao;
private JpaStackPermissionsDao stackPermissionsDao;
private FreeResourcesLimitDao freeResourcesLimitDao;
private OrganizationManager organizationManager;
@ -229,6 +237,7 @@ public class JpaEntitiesCascadeRemovalTest {
install(new FactoryJpaModule());
install(new OrganizationJpaModule());
install(new MultiuserWorkspaceJpaModule());
install(new MachineAuthModule());
bind(FreeResourcesLimitDao.class).to(JpaFreeResourcesLimitDao.class);
bind(RemoveFreeResourcesLimitSubscriber.class).asEagerSingleton();
@ -237,6 +246,8 @@ public class JpaEntitiesCascadeRemovalTest {
bind(WorkspaceStatusCache.class).to(DefaultWorkspaceStatusCache.class);
bind(RuntimeInfrastructure.class).toInstance(mock(RuntimeInfrastructure.class));
MapBinder.newMapBinder(binder(), String.class, InternalEnvironmentFactory.class);
bind(PermissionsManager.class);
bind(PermissionChecker.class).to(PermissionCheckerImpl.class);
bind(AccountManager.class);
bind(Boolean.class)
.annotatedWith(Names.named("che.workspace.auto_snapshot"))
@ -293,6 +304,7 @@ public class JpaEntitiesCascadeRemovalTest {
factoryDao = injector.getInstance(FactoryDao.class);
stackDao = injector.getInstance(StackDao.class);
workerDao = injector.getInstance(WorkerDao.class);
signatureKeyDao = injector.getInstance(SignatureKeyDao.class);
freeResourcesLimitDao = injector.getInstance(FreeResourcesLimitDao.class);
organizationManager = injector.getInstance(OrganizationManager.class);
memberDao = injector.getInstance(MemberDao.class);
@ -361,6 +373,10 @@ public class JpaEntitiesCascadeRemovalTest {
assertNull(notFoundToNull(() -> freeResourcesLimitDao.get(user.getId())));
assertNull(notFoundToNull(() -> freeResourcesLimitDao.get(user2.getId())));
// machine token keypairs
assertNull(notFoundToNull(() -> signatureKeyDao.get(workspace1.getId())));
assertNull(notFoundToNull(() -> signatureKeyDao.get(workspace2.getId())));
// distributed resources is removed
assertNull(
notFoundToNull(() -> organizationResourcesDistributor.get(childOrganization.getId())));
@ -395,6 +411,7 @@ public class JpaEntitiesCascadeRemovalTest {
assertNotNull(notFoundToNull(() -> organizationManager.getById(organization.getId())));
assertNotNull(notFoundToNull(() -> organizationManager.getById(childOrganization.getId())));
assertNotNull(notFoundToNull(() -> organizationManager.getById(organization2.getId())));
assertNotNull(notFoundToNull(() -> signatureKeyDao.get(workspace2.getId())));
assertFalse(
organizationResourcesDistributor.getResourcesCaps(childOrganization.getId()).isEmpty());
wipeTestData();
@ -408,7 +425,8 @@ public class JpaEntitiesCascadeRemovalTest {
};
}
private void createTestData() throws NotFoundException, ConflictException, ServerException {
private void createTestData()
throws NotFoundException, ConflictException, ServerException, NoSuchAlgorithmException {
userDao.create(user = createUser("bobby"));
accountDao.create(account = createAccount("bobby"));
// test permissions users
@ -435,6 +453,9 @@ public class JpaEntitiesCascadeRemovalTest {
workerDao.store(createWorker(user2.getId(), workspace3.getId()));
signatureKeyDao.create(createSignatureKeyPair(workspace1.getId()));
signatureKeyDao.create(createSignatureKeyPair(workspace2.getId()));
stackPermissionsDao.store(
new StackPermissionsImpl(
user2.getId(), stack1.getId(), asList(SET_PERMISSIONS, "read", "write")));
@ -511,6 +532,9 @@ public class JpaEntitiesCascadeRemovalTest {
sshDao.remove(sshPair1.getOwner(), sshPair1.getService(), sshPair1.getName());
sshDao.remove(sshPair2.getOwner(), sshPair2.getService(), sshPair2.getName());
signatureKeyDao.remove(workspace1.getId());
signatureKeyDao.remove(workspace2.getId());
workspaceDao.remove(workspace1.getId());
workspaceDao.remove(workspace2.getId());
workspaceDao.remove(workspace3.getId());

View File

@ -14,6 +14,9 @@ package org.eclipse.che.multiuser.integration.jpa.cascaderemoval;
import static java.util.Arrays.asList;
import com.google.common.collect.ImmutableMap;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@ -29,6 +32,7 @@ import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
import org.eclipse.che.api.workspace.server.model.impl.stack.StackComponentImpl;
import org.eclipse.che.api.workspace.server.model.impl.stack.StackImpl;
import org.eclipse.che.api.workspace.server.stack.image.StackIcon;
import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl;
import org.eclipse.che.multiuser.permission.workspace.server.model.impl.WorkerImpl;
import org.eclipse.che.multiuser.resource.spi.impl.FreeResourcesLimitImpl;
import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
@ -120,5 +124,14 @@ public final class TestObjectsFactory {
Arrays.asList(new ResourceImpl("test1", 123, "mb"), new ResourceImpl("test2", 234, "h")));
}
public static SignatureKeyPairImpl createSignatureKeyPair(String workspaceId)
throws NoSuchAlgorithmException {
final KeyPairGenerator kpg;
kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(512);
final KeyPair pair = kpg.generateKeyPair();
return new SignatureKeyPairImpl(workspaceId, pair.getPublic(), pair.getPrivate());
}
private TestObjectsFactory() {}
}

View File

@ -59,6 +59,9 @@
<class>org.eclipse.che.multiuser.organization.spi.impl.MemberImpl</class>
<class>org.eclipse.che.multiuser.organization.spi.impl.OrganizationDistributedResourcesImpl</class>
<class>org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyImpl</class>
<class>org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl</class>
<class>org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl</class>
<class>org.eclipse.che.multiuser.resource.spi.impl.FreeResourcesLimitImpl</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>

View File

@ -102,6 +102,11 @@
<artifactId>che-multiuser-api-permission</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-machine-authentication</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-permission-workspace</artifactId>

View File

@ -56,6 +56,9 @@ import org.eclipse.che.multiuser.api.permission.server.jpa.JpaSystemPermissionsD
import org.eclipse.che.multiuser.api.permission.server.model.impl.SystemPermissionsImpl;
import org.eclipse.che.multiuser.api.permission.server.spi.PermissionsDao;
import org.eclipse.che.multiuser.api.permission.server.spi.tck.SystemPermissionsDaoTest;
import org.eclipse.che.multiuser.machine.authentication.server.signature.jpa.JpaSignatureKeyDao;
import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl;
import org.eclipse.che.multiuser.machine.authentication.server.signature.spi.SignatureKeyDao;
import org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain;
import org.eclipse.che.multiuser.organization.spi.MemberDao;
import org.eclipse.che.multiuser.organization.spi.OrganizationDao;
@ -163,6 +166,10 @@ public class MultiuserPostgresqlTckModule extends TckModule {
bind(new TypeLiteral<TckRepository<FreeResourcesLimitImpl>>() {})
.toInstance(new JpaTckRepository<>(FreeResourcesLimitImpl.class));
// machine token keys
bind(new TypeLiteral<TckRepository<SignatureKeyPairImpl>>() {})
.toInstance(new JpaTckRepository<>(SignatureKeyPairImpl.class));
// dao
bind(OrganizationDao.class).to(JpaOrganizationDao.class);
bind(OrganizationDistributedResourcesDao.class)
@ -171,6 +178,7 @@ public class MultiuserPostgresqlTckModule extends TckModule {
bind(WorkerDao.class).to(JpaWorkerDao.class);
bind(MemberDao.class).to(JpaMemberDao.class);
bind(SignatureKeyDao.class).to(JpaSignatureKeyDao.class);
bind(new TypeLiteral<PermissionsDao<MemberImpl>>() {}).to(JpaMemberDao.class);
bind(new TypeLiteral<AbstractPermissionsDomain<MemberImpl>>() {}).to(OrganizationDomain.class);

View File

@ -110,6 +110,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-auth</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-inject</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-lang</artifactId>

View File

@ -11,52 +11,38 @@
*/
package org.eclipse.che.multiuser.keycloak.server;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import java.security.PublicKey;
import io.jsonwebtoken.JwtParser;
import javax.inject.Inject;
import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager;
/**
* Base abstract class for the Keycloak-related servlet filters.
*
* <p>In particular it defines commnon use-cases when the authentication / multi-user logic should
* be skipped
* <p>In particular it defines common use-cases when the authentication / multi-user logic should be
* skipped
*/
public abstract class AbstractKeycloakFilter implements Filter {
@Inject protected SignatureKeyManager signatureKeyManager;
@Inject protected JwtParser jwtParser;
/** when a request came from a machine with valid token then auth is not required */
protected boolean shouldSkipAuthentication(HttpServletRequest request, String token) {
boolean shouldSkipAuthentication(HttpServletRequest request, String token) {
if (token == null) {
if (request.getRequestURI() != null
&& request.getRequestURI().endsWith("api/keycloak/OIDCKeycloak.js")) {
return true;
}
return false;
return request.getRequestURI() != null
&& request.getRequestURI().endsWith("api/keycloak/OIDCKeycloak.js");
}
try {
final PublicKey publicKey = signatureKeyManager.getKeyPair().getPublic();
final Jwt jwt = Jwts.parser().setSigningKey(publicKey).parse(token);
return MACHINE_TOKEN_KIND.equals(jwt.getHeader().get("kind"));
} catch (ExpiredJwtException | MalformedJwtException | SignatureException ex) {
// given token is not signed by particular signature key so it must be checked in another way
jwtParser.parse(token);
return false;
} catch (MachineTokenJwtException e) {
return true;
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}

View File

@ -11,28 +11,12 @@
*/
package org.eclipse.che.multiuser.keycloak.server;
import com.auth0.jwk.GuavaCachedJwkProvider;
import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.UrlJwkProvider;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import io.jsonwebtoken.UnsupportedJwtException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Key;
import java.security.PublicKey;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@ -41,31 +25,19 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.che.commons.auth.token.RequestTokenExtractor;
import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class KeycloakAuthenticationFilter extends AbstractKeycloakFilter {
private static final Logger LOG = LoggerFactory.getLogger(KeycloakAuthenticationFilter.class);
private String jwksUrl;
private long allowedClockSkewSec;
private RequestTokenExtractor tokenExtractor;
private JwkProvider jwkProvider;
@Inject
public KeycloakAuthenticationFilter(
KeycloakSettings keycloakSettings,
@Named(KeycloakConstants.ALLOWED_CLOCK_SKEW_SEC) long allowedClockSkewSec,
RequestTokenExtractor tokenExtractor)
throws MalformedURLException {
this.jwksUrl = keycloakSettings.get().get(KeycloakConstants.JWKS_ENDPOINT_SETTING);
this.allowedClockSkewSec = allowedClockSkewSec;
public KeycloakAuthenticationFilter(RequestTokenExtractor tokenExtractor) {
this.tokenExtractor = tokenExtractor;
if (jwksUrl != null) {
this.jwkProvider = new GuavaCachedJwkProvider(new UrlJwkProvider(new URL(jwksUrl)));
}
}
@Override
@ -74,80 +46,34 @@ public class KeycloakAuthenticationFilter extends AbstractKeycloakFilter {
HttpServletRequest request = (HttpServletRequest) req;
final String token = tokenExtractor.getToken(request);
if (shouldSkipAuthentication(request, token)) {
chain.doFilter(req, res);
return;
}
if (token == null) {
send403(res, "Authorization token is missed");
send401(res, "Authorization token is missed");
return;
}
Jws<Claims> jwt;
try {
jwt =
Jwts.parser()
.setAllowedClockSkewSeconds(allowedClockSkewSec)
.setSigningKeyResolver(
new SigningKeyResolverAdapter() {
@Override
public Key resolveSigningKey(
@SuppressWarnings("rawtypes") JwsHeader header, Claims claims) {
try {
return getJwtPublicKey(header);
} catch (JwkException e) {
throw new JwtException(
"Error during the retrieval of the public key during JWT token validation",
e);
}
}
})
.parseClaimsJws(token);
if (shouldSkipAuthentication(request, token)) {
chain.doFilter(req, res);
return;
}
jwt = jwtParser.parseClaimsJws(token);
LOG.debug("JWT = ", jwt);
// OK, we can trust this JWT
} catch (SignatureException
| IllegalArgumentException
| MalformedJwtException
| UnsupportedJwtException e) {
send403(res, "The specified token is not a valid. " + e.getMessage());
return;
} catch (ExpiredJwtException e) {
send403(res, "The specified token is expired");
send401(res, "The specified token is expired");
return;
} catch (JwtException e) {
send401(res, "Token validation failed: " + e.getMessage());
return;
}
request.setAttribute("token", jwt);
chain.doFilter(req, res);
}
private synchronized PublicKey getJwtPublicKey(JwsHeader<?> header) throws JwkException {
String kid = header.getKeyId();
if (kid == null) {
LOG.warn(
"'kid' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys");
return null;
}
String alg = header.getAlgorithm();
if (alg == null) {
LOG.warn(
"'alg' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys");
return null;
}
if (jwkProvider == null) {
LOG.warn(
"JWK provider is not available: This is not possible to validate the token with OIDC provider keys.\n"
+ "Please look into the startup logs to find out the root cause");
return null;
}
Jwk jwk = jwkProvider.get(kid);
return jwk.getPublicKey();
}
private void send403(ServletResponse res, String message) throws IOException {
private void send401(ServletResponse res, String message) throws IOException {
HttpServletResponse response = (HttpServletResponse) res;
response.getOutputStream().write(message.getBytes());
response.setStatus(403);
response.setStatus(401);
}
}

View File

@ -44,9 +44,9 @@ import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants;
public class KeycloakEnvironmentInitalizationFilter extends AbstractKeycloakFilter {
private final KeycloakUserManager userManager;
private final KeycloakSettings settings;
private final RequestTokenExtractor tokenExtractor;
private final PermissionChecker permissionChecker;
private final KeycloakSettings keycloakSettings;
@Inject
public KeycloakEnvironmentInitalizationFilter(
@ -57,7 +57,7 @@ public class KeycloakEnvironmentInitalizationFilter extends AbstractKeycloakFilt
this.userManager = userManager;
this.tokenExtractor = tokenExtractor;
this.permissionChecker = permissionChecker;
this.settings = settings;
this.keycloakSettings = settings;
}
@Override
@ -82,7 +82,8 @@ public class KeycloakEnvironmentInitalizationFilter extends AbstractKeycloakFilt
try {
String username =
claims.get(settings.get().get(KeycloakConstants.USERNAME_CLAIM_SETTING), String.class);
claims.get(
keycloakSettings.get().get(KeycloakConstants.USERNAME_CLAIM_SETTING), String.class);
if (username == null) { // fallback to unique id promised by spec
// https://openid.net/specs/openid-connect-basic-1_0.html#ClaimStability
username = claims.getIssuer() + ":" + claims.getSubject();

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
import com.auth0.jwk.GuavaCachedJwkProvider;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.UrlJwkProvider;
import java.net.MalformedURLException;
import java.net.URL;
import javax.inject.Inject;
import javax.inject.Provider;
import org.eclipse.che.inject.ConfigurationException;
import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants;
/** Constructs {@link UrlJwkProvider} based on Jwk endpoint from keycloak settings */
public class KeycloakJwkProvider implements Provider<JwkProvider> {
private final JwkProvider jwkProvider;
@Inject
public KeycloakJwkProvider(KeycloakSettings keycloakSettings) throws MalformedURLException {
final String jwksUrl = keycloakSettings.get().get(KeycloakConstants.JWKS_ENDPOINT_SETTING);
if (jwksUrl == null) {
throw new ConfigurationException("Jwks endpoint url not found in keycloak settings");
}
this.jwkProvider = new GuavaCachedJwkProvider(new UrlJwkProvider(new URL(jwksUrl)));
}
@Override
public JwkProvider get() {
return jwkProvider;
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants;
/** Provides instance of {@link JwtParser} */
@Singleton
public class KeycloakJwtParserProvider implements Provider<JwtParser> {
private final JwtParser jwtParser;
@Inject
public KeycloakJwtParserProvider(
@Named(KeycloakConstants.ALLOWED_CLOCK_SKEW_SEC) long allowedClockSkewSec,
KeycloakSigningKeyResolver keycloakSigningKeyResolver) {
this.jwtParser =
Jwts.parser()
.setAllowedClockSkewSeconds(allowedClockSkewSec)
.setSigningKeyResolver(keycloakSigningKeyResolver);
}
@Override
public JwtParser get() {
return jwtParser;
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.JwkProvider;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import java.security.Key;
import java.security.PublicKey;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Resolves signing key based on id from JWT header */
@Singleton
public class KeycloakSigningKeyResolver extends SigningKeyResolverAdapter {
private final JwkProvider jwkProvider;
private static final Logger LOG = LoggerFactory.getLogger(KeycloakSigningKeyResolver.class);
@Inject
KeycloakSigningKeyResolver(JwkProvider jwkProvider) {
this.jwkProvider = jwkProvider;
}
@Override
public Key resolveSigningKey(JwsHeader header, String plaintext) {
if (MACHINE_TOKEN_KIND.equals(header.get("kind"))) {
throw new MachineTokenJwtException(); // machine token, doesn't need to verify
}
return getJwtPublicKey(header);
}
@Override
public Key resolveSigningKey(JwsHeader header, Claims claims) {
if (MACHINE_TOKEN_KIND.equals(header.get("kind"))) {
throw new MachineTokenJwtException(); // machine token, doesn't need to verify
}
return getJwtPublicKey(header);
}
private synchronized PublicKey getJwtPublicKey(JwsHeader<?> header) {
String kid = header.getKeyId();
if (header.getKeyId() == null) {
LOG.warn(
"'kid' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys");
throw new JwtException("'kid' is missing in the JWT token header.");
}
try {
return jwkProvider.get(kid).getPublicKey();
} catch (JwkException e) {
throw new JwtException(
"Error during the retrieval of the public key during JWT token validation", e);
}
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
import io.jsonwebtoken.JwtException;
public class MachineTokenJwtException extends JwtException {
public MachineTokenJwtException() {
super("This is a machine token");
}
}

View File

@ -11,12 +11,16 @@
*/
package org.eclipse.che.multiuser.keycloak.server.deploy;
import com.auth0.jwk.JwkProvider;
import com.google.inject.AbstractModule;
import io.jsonwebtoken.JwtParser;
import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
import org.eclipse.che.api.user.server.TokenValidator;
import org.eclipse.che.api.user.server.spi.ProfileDao;
import org.eclipse.che.multiuser.api.account.personal.PersonalAccountUserManager;
import org.eclipse.che.multiuser.keycloak.server.KeycloakConfigurationService;
import org.eclipse.che.multiuser.keycloak.server.KeycloakJwkProvider;
import org.eclipse.che.multiuser.keycloak.server.KeycloakJwtParserProvider;
import org.eclipse.che.multiuser.keycloak.server.KeycloakTokenValidator;
import org.eclipse.che.multiuser.keycloak.server.KeycloakUserManager;
import org.eclipse.che.multiuser.keycloak.server.dao.KeycloakProfileDao;
@ -32,6 +36,8 @@ public class KeycloakModule extends AbstractModule {
bind(KeycloakConfigurationService.class);
bind(ProfileDao.class).to(KeycloakProfileDao.class);
bind(JwkProvider.class).toProvider(KeycloakJwkProvider.class);
bind(JwtParser.class).toProvider(KeycloakJwtParserProvider.class);
bind(PersonalAccountUserManager.class).to(KeycloakUserManager.class);
bind(OAuthAPI.class).toProvider(OAuthAPIProvider.class);

View File

@ -11,26 +11,21 @@
*/
package org.eclipse.che.multiuser.keycloak.server;
import static io.jsonwebtoken.SignatureAlgorithm.RS256;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import io.jsonwebtoken.Jwts;
import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.util.HashMap;
import java.util.Map;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtParser;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
@ -45,27 +40,12 @@ import org.testng.annotations.Test;
public class AbstractKeycloakFilterTest {
@Mock private HttpServletRequest request;
@Mock private SignatureKeyManager signatureKeyManager;
@Mock private JwtParser jwtParser;
@InjectMocks private TestLoginFilter abstractKeycloakFilter;
private String machineToken;
@BeforeMethod
public void setup() throws Exception {
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(512);
final KeyPair keyPair = kpg.generateKeyPair();
final Map<String, Object> header = new HashMap<>();
header.put("kind", MACHINE_TOKEN_KIND);
machineToken =
Jwts.builder()
.setPayload("payload")
.setHeader(header)
.signWith(RS256, keyPair.getPrivate())
.compact();
when(signatureKeyManager.getKeyPair()).thenReturn(keyPair);
public void setup() {
when(request.getRequestURI()).thenReturn(null);
}
@ -82,18 +62,23 @@ public class AbstractKeycloakFilterTest {
@Test
public void testShouldNotSkipAuthWhenProvidedTokenIsNotMachine() {
assertFalse(abstractKeycloakFilter.shouldSkipAuthentication(request, "testToken"));
Jwt mock = Mockito.mock(Jwt.class);
doReturn(mock).when(jwtParser).parse(anyString());
assertFalse(abstractKeycloakFilter.shouldSkipAuthentication(request, "token"));
}
@Test
public void testAuthIsNotNeededWhenMachineTokenProvided() throws Exception {
assertTrue(abstractKeycloakFilter.shouldSkipAuthentication(request, machineToken));
public void testAuthIsNotNeededWhenMachineTokenProvided() {
when(jwtParser.parse(anyString())).thenThrow(MachineTokenJwtException.class);
assertTrue(abstractKeycloakFilter.shouldSkipAuthentication(request, "token"));
}
static class TestLoginFilter extends AbstractKeycloakFilter {
public TestLoginFilter() {}
@Override
public void doFilter(
ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {}
ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {}
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.JwtParser;
import java.lang.reflect.Field;
import javax.servlet.FilterChain;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.che.commons.auth.token.RequestTokenExtractor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class KeycloakAuthenticationFilterTest {
@Mock private RequestTokenExtractor tokenExtractor;
@Mock private JwtParser jwtParser;
@Mock private ServletOutputStream servletOutputStream;
@Mock private HttpServletRequest request;
@Mock private HttpServletResponse response;
@Mock private FilterChain chain;
private KeycloakAuthenticationFilter authenticationFilter;
@BeforeMethod
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
authenticationFilter = new KeycloakAuthenticationFilter(tokenExtractor);
Field parser = authenticationFilter.getClass().getSuperclass().getDeclaredField("jwtParser");
parser.setAccessible(true);
parser.set(authenticationFilter, jwtParser);
when(response.getOutputStream()).thenReturn(servletOutputStream);
}
@Test
public void shouldSend401IfNoTokenInRequest() throws Exception {
when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn(null);
authenticationFilter.doFilter(request, response, chain);
verify(response).setStatus(401);
verify(servletOutputStream).write(eq("Authorization token is missed".getBytes()));
verifyNoMoreInteractions(chain);
}
@Test
public void shouldSend401IfTokenIsExpired() throws Exception {
when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token");
when(jwtParser.parse(anyString())).thenThrow(ExpiredJwtException.class);
authenticationFilter.doFilter(request, response, chain);
verify(response).setStatus(401);
verify(servletOutputStream).write(eq("The specified token is expired".getBytes()));
verifyNoMoreInteractions(chain);
}
@Test
public void shouldSend401IfTokenIsCheckSignatureFailed() throws Exception {
when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token");
when(jwtParser.parse(anyString())).thenThrow(new JwtException("bad signature"));
authenticationFilter.doFilter(request, response, chain);
verify(response).setStatus(401);
verify(servletOutputStream).write(eq("Token validation failed: bad signature".getBytes()));
verifyNoMoreInteractions(chain);
}
}

View File

@ -11,7 +11,6 @@
*/
package org.eclipse.che.multiuser.keycloak.server;
import static io.jsonwebtoken.SignatureAlgorithm.RS512;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@ -23,12 +22,12 @@ import static org.mockito.Mockito.when;
import static org.testng.AssertJUnit.assertEquals;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.impl.DefaultClaims;
import io.jsonwebtoken.impl.DefaultHeader;
import io.jsonwebtoken.impl.DefaultJwt;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.HashMap;
@ -45,7 +44,6 @@ import org.eclipse.che.commons.subject.SubjectImpl;
import org.eclipse.che.multiuser.api.permission.server.AuthorizedSubject;
import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager;
import org.eclipse.che.multiuser.machine.authentication.shared.Constants;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@ -66,6 +64,7 @@ public class KeycloakEnvironmentInitalizationFilterTest {
@Mock private HttpServletRequest request;
@Mock private HttpServletResponse response;
@Mock private HttpSession session;
@Mock private JwtParser jwtParser;
private KeycloakEnvironmentInitalizationFilter filter;
@ -79,27 +78,17 @@ public class KeycloakEnvironmentInitalizationFilterTest {
filter =
new KeycloakEnvironmentInitalizationFilter(
userManager, tokenExtractor, permissionChecker, keycloakSettings);
filter.signatureKeyManager = keyManager;
Field parser = filter.getClass().getSuperclass().getDeclaredField("jwtParser");
parser.setAccessible(true);
parser.set(filter, jwtParser);
final KeyPair kp = new KeyPair(mock(PublicKey.class), mock(PrivateKey.class));
when(keyManager.getKeyPair()).thenReturn(kp);
when(keyManager.getKeyPair(anyString())).thenReturn(kp);
}
@Test
public void shouldSkipRequestsWithMachineTokens() throws Exception {
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(1024);
final KeyPair keyPair = kpg.generateKeyPair();
when(keyManager.getKeyPair()).thenReturn(keyPair);
final Map<String, Object> header = new HashMap<>();
header.put("kind", Constants.MACHINE_TOKEN_KIND);
final String token =
Jwts.builder()
.setPayload("payload")
.setHeader(header)
.signWith(RS512, keyPair.getPrivate())
.compact();
when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn(token);
when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("not_null_token");
when(jwtParser.parse(anyString())).thenThrow(MachineTokenJwtException.class);
// when
filter.doFilter(request, response, chain);
@ -113,13 +102,13 @@ public class KeycloakEnvironmentInitalizationFilterTest {
Subject existingSubject = new SubjectImpl("name", "id1", "token", false);
UserImpl user = new UserImpl("id2", "test2@test.com", "username2");
Subject expectedSubject = new SubjectImpl(user.getName(), user.getId(), "token2", false);
ArgumentCaptor<AuthorizedSubject> captor = ArgumentCaptor.forClass(AuthorizedSubject.class);
DefaultJwt<Claims> claims = createJwt();
Subject expectedSubject = new SubjectImpl(user.getName(), user.getId(), "token2", false);
// given
when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token2");
when(request.getAttribute("token")).thenReturn(createJwt());
when(request.getAttribute("token")).thenReturn(claims);
when(session.getAttribute(eq("che_subject"))).thenReturn(existingSubject);
when(userManager.getOrCreateUser(anyString(), anyString(), anyString())).thenReturn(user);
EnvironmentContext context = spy(EnvironmentContext.getCurrent());

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkProvider;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.impl.DefaultClaims;
import io.jsonwebtoken.impl.DefaultJwsHeader;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.util.HashMap;
import java.util.Map;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class KeycloakSigningKeyResolverTest {
@Mock private JwkProvider jwkProvider;
@InjectMocks private KeycloakSigningKeyResolver signingKeyResolver;
@Test(expectedExceptions = MachineTokenJwtException.class)
public void shouldThrowMachineTokenExceptionOnMachineTokensWithPlainText() {
final Map<String, Object> param = new HashMap<>();
param.put("kind", MACHINE_TOKEN_KIND);
DefaultJwsHeader header = new DefaultJwsHeader(param);
signingKeyResolver.resolveSigningKey(header, "plaintext");
verifyNoMoreInteractions(jwkProvider);
}
@Test(expectedExceptions = MachineTokenJwtException.class)
public void shouldThrowMachineTokenExceptionOnMachineTokensWithClaims() {
final Map<String, Object> param = new HashMap<>();
param.put("kind", MACHINE_TOKEN_KIND);
DefaultJwsHeader header = new DefaultJwsHeader(param);
signingKeyResolver.resolveSigningKey(header, new DefaultClaims());
verifyNoMoreInteractions(jwkProvider);
}
@Test(expectedExceptions = JwtException.class)
public void shouldThrowJwtExceptionifNoKeyIdHeader() {
signingKeyResolver.resolveSigningKey(new DefaultJwsHeader(), "plaintext");
verifyNoMoreInteractions(jwkProvider);
}
@Test
public void shouldReturnPublicKey() throws Exception {
final String kid = "123";
final Jwk jwk = mock(Jwk.class);
final Map<String, Object> param = new HashMap<>();
param.put("kid", kid);
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(1024);
final KeyPair keyPair = kpg.generateKeyPair();
when(jwk.getPublicKey()).thenReturn(keyPair.getPublic());
when(jwkProvider.get(eq(kid))).thenReturn(jwk);
Key actual = signingKeyResolver.resolveSigningKey(new DefaultJwsHeader(param), "plaintext");
assertEquals(actual, keyPair.getPublic());
}
}

View File

@ -62,6 +62,10 @@
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-account</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>

View File

@ -37,6 +37,8 @@ public class MachineAuthModule extends AbstractModule {
bind(SignatureKeyManager.class);
bind(SignatureKeyDao.class).to(JpaSignatureKeyDao.class);
bind(JpaSignatureKeyDao.RemoveKeyPairsBeforeWorkspaceRemovedEventSubscriber.class)
.asEagerSingleton();
final Multibinder<EnvVarProvider> envVarProviders =
Multibinder.newSetBinder(binder(), EnvVarProvider.class);
envVarProviders.addBinding().to(SignaturePublicKeyEnvProvider.class);

View File

@ -14,16 +14,12 @@ package org.eclipse.che.multiuser.machine.authentication.server;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.USER_ID_CLAIM;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import java.io.IOException;
import java.security.Principal;
import javax.inject.Inject;
@ -46,7 +42,6 @@ import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.commons.subject.SubjectImpl;
import org.eclipse.che.multiuser.api.permission.server.AuthorizedSubject;
import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager;
/**
* Handles requests that comes from machines with specific machine token.
@ -59,46 +54,36 @@ public class MachineLoginFilter implements Filter {
private final RequestTokenExtractor tokenExtractor;
private final UserManager userManager;
private final SignatureKeyManager keyManager;
private final PermissionChecker permissionChecker;
private final JwtParser jwtParser;
@Inject
public MachineLoginFilter(
RequestTokenExtractor tokenExtractor,
UserManager userManager,
SignatureKeyManager keyManager,
MachineSigningKeyResolver machineKeyResolver,
PermissionChecker permissionChecker) {
this.tokenExtractor = tokenExtractor;
this.userManager = userManager;
this.keyManager = keyManager;
this.permissionChecker = permissionChecker;
this.jwtParser = Jwts.parser().setSigningKeyResolver(machineKeyResolver);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
public void init(FilterConfig filterConfig) {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
final HttpServletRequest httpRequest = (HttpServletRequest) request;
final String token = tokenExtractor.getToken(httpRequest);
if (isNullOrEmpty(token)) {
chain.doFilter(request, response);
return;
}
// check token signature and verify is this token machine or not
try {
final Jws<Claims> jwt =
Jwts.parser().setSigningKey(keyManager.getKeyPair().getPublic()).parseClaimsJws(token);
final Claims claims = jwt.getBody();
if (!isMachineToken(jwt)) {
chain.doFilter(request, response);
return;
}
final Claims claims = jwtParser.parseClaimsJws(token).getBody();
try {
final String userId = claims.get(USER_ID_CLAIM, String.class);
// check if user with such id exists
@ -113,28 +98,20 @@ public class MachineLoginFilter implements Filter {
response,
SC_UNAUTHORIZED,
"Authentication with machine token failed because user for this token no longer exist.");
} catch (ServerException ex) {
sendErr(
response,
SC_UNAUTHORIZED,
format("Authentication with machine token failed cause: %s", ex.getMessage()));
} finally {
EnvironmentContext.reset();
}
} catch (UnsupportedJwtException
| MalformedJwtException
| SignatureException
| ExpiredJwtException ex) {
// signature check failed
} catch (NotMachineTokenJwtException ex) {
// not a machine token, bypass
chain.doFilter(request, response);
} catch (ServerException | JwtException e) {
sendErr(
response,
SC_UNAUTHORIZED,
format("Authentication with machine token failed cause: %s", e.getMessage()));
}
}
/** Checks whether given token from a machine. */
private boolean isMachineToken(Jws<Claims> jwt) {
return MACHINE_TOKEN_KIND.equals(jwt.getHeader().get("kind"));
}
/** Sets given error code with err message into give response. */
private static void sendErr(ServletResponse res, int errCode, String msg) throws IOException {
final HttpServletResponse response = (HttpServletResponse) res;

View File

@ -36,7 +36,7 @@ public class MachineSessionInvalidator implements EventSubscriber<WorkspaceStatu
@Override
public void onEvent(WorkspaceStatusEvent event) {
if (WorkspaceStatus.STOPPED.equals(event.getStatus())) {
tokenRegistry.removeTokens(event.getWorkspaceId()).values();
tokenRegistry.removeTokens(event.getWorkspaceId());
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.machine.authentication.server;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.WORKSPACE_ID_CLAIM;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import java.security.Key;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager;
/** Resolves signing key pair based on workspace Id claim of token. */
@Singleton
public class MachineSigningKeyResolver extends SigningKeyResolverAdapter {
private final SignatureKeyManager keyManager;
@Inject
public MachineSigningKeyResolver(SignatureKeyManager keyManager) {
this.keyManager = keyManager;
}
@Override
public Key resolveSigningKey(JwsHeader header, Claims claims) {
if (!MACHINE_TOKEN_KIND.equals(header.get("kind"))) {
throw new NotMachineTokenJwtException();
}
String wsId = claims.get(WORKSPACE_ID_CLAIM, String.class);
if (wsId == null) {
throw new JwtException(
"Unable to fetch signature key pair: no workspace id present in token");
}
try {
return keyManager.getKeyPair(wsId).getPublic();
} catch (ServerException e) {
throw new JwtException("Unable to fetch signature key pair:" + e.getMessage());
}
}
}

View File

@ -91,7 +91,7 @@ public class MachineTokenRegistry {
/** Creates new token with given data. */
private String createToken(String userId, String workspaceId)
throws NotFoundException, ServerException {
final PrivateKey privateKey = signatureKeyManager.getKeyPair().getPrivate();
final PrivateKey privateKey = signatureKeyManager.getKeyPair(workspaceId).getPrivate();
final User user = userManager.getById(userId);
final Map<String, Object> header = new HashMap<>(2);
header.put("kind", MACHINE_TOKEN_KIND);

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.machine.authentication.server;
import io.jsonwebtoken.JwtException;
public class NotMachineTokenJwtException extends JwtException {
public NotMachineTokenJwtException() {
super("This is not a machine token");
}
}

View File

@ -11,10 +11,13 @@
*/
package org.eclipse.che.multiuser.machine.authentication.server.signature;
import static org.eclipse.che.commons.lang.NameGenerator.generate;
import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STOPPED;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@ -25,13 +28,18 @@ import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Iterator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.notification.EventSubscriber;
import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.core.db.DBInitializer;
import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl;
@ -57,56 +65,95 @@ public class SignatureKeyManager {
private final String algorithm;
private final SignatureKeyDao signatureKeyDao;
private final EventService eventService;
private final EventSubscriber<?> workspaceEventsSubscriber;
@Inject
@SuppressWarnings("unused")
private DBInitializer dbInitializer;
private KeyPair cachedPair;
private LoadingCache<String, KeyPair> cachedPair;
@Inject
public SignatureKeyManager(
@Named("che.auth.signature_key_size") int keySize,
@Named("che.auth.signature_key_algorithm") String algorithm,
EventService eventService,
SignatureKeyDao signatureKeyDao) {
this.keySize = keySize;
this.algorithm = algorithm;
this.eventService = eventService;
this.signatureKeyDao = signatureKeyDao;
this.cachedPair =
CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterAccess(2, TimeUnit.HOURS)
.build(
new CacheLoader<String, KeyPair>() {
@Override
public KeyPair load(String key) throws Exception {
return loadKeyPair(key);
}
});
this.workspaceEventsSubscriber =
new EventSubscriber<WorkspaceStatusEvent>() {
@Override
public void onEvent(WorkspaceStatusEvent event) {
if (event.getStatus() == STOPPED) {
removeKeyPair(event.getWorkspaceId());
}
}
};
}
/** Returns cached instance of {@link KeyPair} or null when failed to load key pair. */
@Nullable
public KeyPair getKeyPair() {
if (cachedPair == null) {
loadKeyPair();
public KeyPair getKeyPair(String workspaceId) throws ServerException {
try {
return cachedPair.get(workspaceId);
} catch (ExecutionException e) {
throw new ServerException(e.getCause());
}
}
/** Removes key pair from cache and DB. */
public void removeKeyPair(String workspaceId) {
try {
cachedPair.invalidate(workspaceId);
signatureKeyDao.remove(workspaceId);
} catch (ServerException e) {
LOG.error(
"Unable to cleanup machine token signature keypairs for ws {}. Cause: {}",
workspaceId,
e.getMessage());
}
return cachedPair;
}
/** Loads signature key pair if no existing keys found then stores a newly generated key pair. */
@PostConstruct
@VisibleForTesting
void loadKeyPair() {
KeyPair loadKeyPair(String workspaceId) throws ServerException, ConflictException {
try {
final Iterator<SignatureKeyPairImpl> it = signatureKeyDao.getAll(1, 0).getItems().iterator();
if (it.hasNext()) {
cachedPair = toJavaKeyPair(it.next());
return;
return toJavaKeyPair(signatureKeyDao.get(workspaceId));
} catch (NotFoundException nfe) {
try {
return toJavaKeyPair(signatureKeyDao.create(generateKeyPair(workspaceId)));
} catch (ConflictException | ServerException ex) {
LOG.error(
"Failed to store signature keys for ws {}. Cause: {}", workspaceId, ex.getMessage());
throw ex;
}
} catch (ServerException ex) {
LOG.error("Failed to load signature keys. Cause: {}", ex.getMessage());
return;
}
try {
cachedPair = toJavaKeyPair(signatureKeyDao.create(generateKeyPair()));
} catch (ConflictException | ServerException ex) {
LOG.error("Failed to store signature keys. Cause: {}", ex.getMessage());
LOG.error(
"Failed to load signature keys for ws {}. Cause: {}", workspaceId, ex.getMessage());
throw ex;
}
}
@VisibleForTesting
SignatureKeyPairImpl generateKeyPair() throws ServerException {
SignatureKeyPairImpl generateKeyPair(String workspaceId) throws ServerException {
final KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance(algorithm);
@ -116,8 +163,11 @@ public class SignatureKeyManager {
kpg.initialize(keySize);
final KeyPair pair = kpg.generateKeyPair();
final SignatureKeyPairImpl kp =
new SignatureKeyPairImpl(generate("signatureKey", 16), pair.getPublic(), pair.getPrivate());
LOG.info("Generated signature key pair with id {} and algorithm {}.", kp.getId(), algorithm);
new SignatureKeyPairImpl(workspaceId, pair.getPublic(), pair.getPrivate());
LOG.debug(
"Generated signature key pair with ws id {} and algorithm {}.",
kp.getWorkspaceId(),
algorithm);
return kp;
}
@ -149,4 +199,10 @@ public class SignatureKeyManager {
String.format("Unsupported key spec '%s' for signature keys", key.getFormat()));
}
}
@VisibleForTesting
@PostConstruct
void subscribe() {
eventService.subscribe(workspaceEventsSubscriber);
}
}

View File

@ -22,8 +22,8 @@ import com.google.common.annotations.Beta;
@Beta
public interface SignatureKeyPair {
/** Returns unique identifier for this sign key pair. */
String getId();
/** Returns workspace identifier for this sign key pair. */
String getWorkspaceId();
/** Returns public part for this sign key pair. */
SignatureKey getPublicKey();

View File

@ -15,7 +15,9 @@ import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.
import java.util.Base64;
import javax.inject.Inject;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.provision.env.EnvVarProvider;
import org.eclipse.che.commons.lang.Pair;
@ -34,9 +36,21 @@ public class SignaturePublicKeyEnvProvider implements EnvVarProvider {
}
@Override
public Pair<String, String> get(RuntimeIdentity runtimeIdentity) {
return Pair.of(
SIGNATURE_PUBLIC_KEY_ENV,
new String(Base64.getEncoder().encode(keyManager.getKeyPair().getPublic().getEncoded())));
public Pair<String, String> get(RuntimeIdentity runtimeIdentity) throws InfrastructureException {
try {
return Pair.of(
SIGNATURE_PUBLIC_KEY_ENV,
new String(
Base64.getEncoder()
.encode(
keyManager
.getKeyPair(runtimeIdentity.getWorkspaceId())
.getPublic()
.getEncoded())));
} catch (ServerException e) {
throw new InfrastructureException(
"Signature key pair for machine authentication cannot be retrieved. Reason: "
+ e.getMessage());
}
}
}

View File

@ -11,20 +11,23 @@
*/
package org.eclipse.che.multiuser.machine.authentication.server.signature.jpa;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import com.google.inject.persist.Transactional;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.Page;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.workspace.server.event.BeforeWorkspaceRemovedEvent;
import org.eclipse.che.core.db.cascade.CascadeEventSubscriber;
import org.eclipse.che.core.db.jpa.DuplicateKeyException;
import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl;
import org.eclipse.che.multiuser.machine.authentication.server.signature.spi.SignatureKeyDao;
@ -52,7 +55,7 @@ public class JpaSignatureKeyDao implements SignatureKeyDao {
doCreate(keyPair);
} catch (DuplicateKeyException dkEx) {
throw new ConflictException(
format("Signature key pair with id '%s' already exists", keyPair.getId()));
format("Signature key pair for workspace '%s' already exists", keyPair.getWorkspaceId()));
} catch (RuntimeException ex) {
throw new ServerException(ex.getMessage(), ex);
}
@ -67,20 +70,20 @@ public class JpaSignatureKeyDao implements SignatureKeyDao {
}
@Override
public void remove(String id) throws ServerException {
requireNonNull(id, "Required non-null key pair");
public void remove(String workspaceId) throws ServerException {
requireNonNull(workspaceId, "Required non-null workspace Id");
try {
doRemove(id);
doRemove(workspaceId);
} catch (RuntimeException ex) {
throw new ServerException(ex.getMessage(), ex);
}
}
@Transactional
protected void doRemove(String id) {
final SignatureKeyPairImpl keyPair = managerProvider.get().find(SignatureKeyPairImpl.class, id);
protected void doRemove(String workspaceId) {
final EntityManager manager = managerProvider.get();
final SignatureKeyPairImpl keyPair = manager.find(SignatureKeyPairImpl.class, workspaceId);
if (keyPair != null) {
final EntityManager manager = managerProvider.get();
manager.remove(keyPair);
manager.flush();
}
@ -88,27 +91,41 @@ public class JpaSignatureKeyDao implements SignatureKeyDao {
@Override
@Transactional
public Page<SignatureKeyPairImpl> getAll(int maxItems, long skipCount) throws ServerException {
checkArgument(maxItems >= 0, "The number of items to return can't be negative.");
checkArgument(
skipCount >= 0,
"The number of items to skip can't be negative or greater than " + Integer.MAX_VALUE);
public SignatureKeyPairImpl get(String workspaceId) throws NotFoundException, ServerException {
final EntityManager manager = managerProvider.get();
try {
final EntityManager manager = managerProvider.get();
final List<SignatureKeyPairImpl> list =
return new SignatureKeyPairImpl(
manager
.createNamedQuery("SignKeyPair.getAll", SignatureKeyPairImpl.class)
.setMaxResults(maxItems)
.setFirstResult((int) skipCount)
.getResultList()
.stream()
.map(SignatureKeyPairImpl::new)
.collect(toList());
final long count =
manager.createNamedQuery("SignKeyPair.getAllCount", Long.class).getSingleResult();
return new Page<>(list, skipCount, maxItems, count);
.setParameter("workspaceId", workspaceId)
.getSingleResult());
} catch (NoResultException x) {
throw new NotFoundException(
format("Signature key pair for workspace '%s' doesn't exist", workspaceId));
} catch (RuntimeException ex) {
throw new ServerException(ex.getMessage(), ex);
}
}
@Singleton
public static class RemoveKeyPairsBeforeWorkspaceRemovedEventSubscriber
extends CascadeEventSubscriber<BeforeWorkspaceRemovedEvent> {
@Inject private EventService eventService;
@Inject private SignatureKeyDao signatureKeyDao;
@PostConstruct
public void subscribe() {
eventService.subscribe(this, BeforeWorkspaceRemovedEvent.class);
}
@PreDestroy
public void unsubscribe() {
eventService.unsubscribe(this, BeforeWorkspaceRemovedEvent.class);
}
@Override
public void onCascadeEvent(BeforeWorkspaceRemovedEvent event) throws Exception {
signatureKeyDao.remove(event.getWorkspace().getId());
}
}
}

View File

@ -23,20 +23,26 @@ import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyPair;
/** @author Anton Korneta */
@Entity(name = "SignKeyPair")
@Table(name = "che_sign_key_pair")
@NamedQueries({
@NamedQuery(name = "SignKeyPair.getAll", query = "SELECT kp FROM SignKeyPair kp"),
@NamedQuery(name = "SignKeyPair.getAllCount", query = "SELECT COUNT(kp) FROM SignKeyPair kp")
@NamedQuery(
name = "SignKeyPair.getAll",
query = "SELECT kp FROM SignKeyPair kp WHERE kp.workspaceId = :workspaceId"),
})
public class SignatureKeyPairImpl implements SignatureKeyPair {
@Id
@Column(name = "id")
private String id;
@Column(name = "workspace_id")
private String workspaceId;
@OneToOne
@JoinColumn(name = "workspace_id", insertable = false, updatable = false)
private WorkspaceImpl workspace;
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "public_key")
@ -49,26 +55,27 @@ public class SignatureKeyPairImpl implements SignatureKeyPair {
public SignatureKeyPairImpl() {}
public SignatureKeyPairImpl(SignatureKeyPairImpl keyPair) {
this(keyPair.getId(), keyPair.getPublicKey(), keyPair.getPrivateKey());
this(keyPair.getWorkspaceId(), keyPair.getPublicKey(), keyPair.getPrivateKey());
}
public SignatureKeyPairImpl(String id, PublicKey publicKey, PrivateKey privateKey) {
this(id, new SignatureKeyImpl(publicKey), new SignatureKeyImpl(privateKey));
public SignatureKeyPairImpl(String workspaceId, PublicKey publicKey, PrivateKey privateKey) {
this(workspaceId, new SignatureKeyImpl(publicKey), new SignatureKeyImpl(privateKey));
}
public SignatureKeyPairImpl(String id, SignatureKeyImpl publicKey, SignatureKeyImpl privateKey) {
this.id = id;
public SignatureKeyPairImpl(
String workspaceId, SignatureKeyImpl publicKey, SignatureKeyImpl privateKey) {
this.workspaceId = workspaceId;
this.publicKey = publicKey;
this.privateKey = privateKey;
}
@Override
public String getId() {
return id;
public String getWorkspaceId() {
return workspaceId;
}
public void setId(String id) {
this.id = id;
public void setWorkspaceId(String workspaceId) {
this.workspaceId = workspaceId;
}
@Override
@ -98,7 +105,7 @@ public class SignatureKeyPairImpl implements SignatureKeyPair {
return false;
}
final SignatureKeyPairImpl that = (SignatureKeyPairImpl) obj;
return Objects.equals(id, that.id)
return Objects.equals(workspaceId, that.workspaceId)
&& Objects.equals(publicKey, that.publicKey)
&& Objects.equals(privateKey, that.privateKey);
}
@ -106,7 +113,7 @@ public class SignatureKeyPairImpl implements SignatureKeyPair {
@Override
public int hashCode() {
int hash = 7;
hash = 31 * hash + Objects.hashCode(id);
hash = 31 * hash + Objects.hashCode(workspaceId);
hash = 31 * hash + Objects.hashCode(publicKey);
hash = 31 * hash + Objects.hashCode(privateKey);
return hash;
@ -115,8 +122,8 @@ public class SignatureKeyPairImpl implements SignatureKeyPair {
@Override
public String toString() {
return "SignatureKeyPairImpl{"
+ "id='"
+ id
+ "workspaceId='"
+ workspaceId
+ '\''
+ ", publicKey="
+ publicKey

View File

@ -13,7 +13,7 @@ package org.eclipse.che.multiuser.machine.authentication.server.signature.spi;
import com.google.common.annotations.Beta;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.Page;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl;
@ -36,21 +36,19 @@ public interface SignatureKeyDao {
throws ConflictException, ServerException;
/**
* Removes signature key pair with given id.
* Removes signature key pair with given workspace id.
*
* @param id signature key identifier
* @param workspaceId workspace identifier to remove keypair from
* @throws ServerException when any errors occur while removing signature key pair
*/
void remove(String id) throws ServerException;
void remove(String workspaceId) throws ServerException;
/**
* Returns all the signature key pairs.
* Returns signature key pair for given workspace id.
*
* @param skipCount the number of signature key pairs to skip
* @param maxItems the maximum number of signature key pairs to return
* @return list of signature key pairs or an empty list when no keys were found
* @throws ServerException when any errors occur while fetching the key pairs
* @throws IllegalArgumentException when {@code maxItems} or {@code skipCount} is negative
* @param workspaceId identifier of workspace which key pair belongs to
* @return signature key pair for the given workspace
* @throws NotFoundException when any errors occur while fetching the key pairs
*/
Page<SignatureKeyPairImpl> getAll(int maxItems, long skipCount) throws ServerException;
SignatureKeyPairImpl get(String workspaceId) throws NotFoundException, ServerException;
}

View File

@ -100,10 +100,13 @@ public class MachineLoginFilterTest {
.compact();
machineLoginFilter =
new MachineLoginFilter(
tokenExtractorMock, userManagerMock, keyManagerMock, permissionCheckerMock);
tokenExtractorMock,
userManagerMock,
new MachineSigningKeyResolver(keyManagerMock),
permissionCheckerMock);
when(tokenExtractorMock.getToken(any(HttpServletRequest.class))).thenReturn(token);
when(keyManagerMock.getKeyPair()).thenReturn(keyPair);
when(keyManagerMock.getKeyPair(eq(WORKSPACE_ID))).thenReturn(keyPair);
when(userMock.getName()).thenReturn(SUBJECT.getUserName());
when(userManagerMock.getById(SUBJECT.getUserId())).thenReturn(userMock);
@ -113,25 +116,27 @@ public class MachineLoginFilterTest {
public void testProcessRequestWithValidToken() throws Exception {
machineLoginFilter.doFilter(getRequestMock(), responseMock, chainMock);
verify(keyManagerMock).getKeyPair();
verify(keyManagerMock).getKeyPair(eq(WORKSPACE_ID));
verify(userManagerMock).getById(anyString());
verifyZeroInteractions(responseMock);
}
@Test
public void testProceedRequestWhenSignatureCheckIsFailed() throws Exception {
final String tokenWithInvalidSignature = "keycloak_token";
public void testNotProceedRequestWhenSignatureCheckIsFailed() throws Exception {
final HttpServletRequest requestMock = getRequestMock();
when(tokenExtractorMock.getToken(any(HttpServletRequest.class)))
.thenReturn(tokenWithInvalidSignature);
final KeyPairGenerator kpg = KeyPairGenerator.getInstance(SIGNATURE_ALGORITHM);
kpg.initialize(KEY_SIZE);
final KeyPair pair = kpg.generateKeyPair();
when(keyManagerMock.getKeyPair(eq(WORKSPACE_ID))).thenReturn(pair);
machineLoginFilter.doFilter(requestMock, responseMock, chainMock);
verify(tokenExtractorMock).getToken(any(HttpServletRequest.class));
verify(keyManagerMock).getKeyPair();
verify(chainMock).doFilter(requestMock, responseMock);
verifyZeroInteractions(userManagerMock);
verifyZeroInteractions(responseMock);
verify(responseMock)
.sendError(
401,
"Authentication with machine token failed cause: JWT signature does not match locally computed signature."
+ " JWT validity cannot be asserted and should not be trusted.");
}
@Test
@ -154,7 +159,7 @@ public class MachineLoginFilterTest {
machineLoginFilter.doFilter(getRequestMock(), responseMock, chainMock);
verify(keyManagerMock).getKeyPair();
verify(keyManagerMock).getKeyPair(eq(WORKSPACE_ID));
verify(userManagerMock).getById(anyString());
verify(responseMock)
.sendError(
@ -168,7 +173,7 @@ public class MachineLoginFilterTest {
machineLoginFilter.doFilter(getRequestMock(), responseMock, chainMock);
verify(keyManagerMock).getKeyPair();
verify(keyManagerMock).getKeyPair(eq(WORKSPACE_ID));
verify(userManagerMock).getById(anyString());
verify(responseMock)
.sendError(

View File

@ -69,7 +69,7 @@ public class MachineTokenRegistryTest {
keyPair = kpg.generateKeyPair();
mockUser(USER_ID, USER_NAME);
when(signatureKeyManager.getKeyPair()).thenReturn(keyPair);
when(signatureKeyManager.getKeyPair(anyString())).thenReturn(keyPair);
}
@Test
@ -89,7 +89,7 @@ public class MachineTokenRegistryTest {
assertEquals(subject.getUserName(), USER_NAME);
assertEquals(claims.get(WORKSPACE_ID_CLAIM, String.class), WORKSPACE_ID);
verify(userManager).getById(USER_ID);
verify(signatureKeyManager).getKeyPair();
verify(signatureKeyManager).getKeyPair(anyString());
assertNotNull(generatedToken);
}

View File

@ -11,26 +11,32 @@
*/
package org.eclipse.che.multiuser.machine.authentication.server.signature;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import org.eclipse.che.api.core.Page;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.workspace.WorkspaceStatus;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.notification.EventSubscriber;
import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent;
import org.eclipse.che.dto.server.DtoFactory;
import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyImpl;
import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl;
import org.eclipse.che.multiuser.machine.authentication.server.signature.spi.SignatureKeyDao;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.stubbing.Answer;
import org.mockito.testng.MockitoTestNGListener;
@ -50,6 +56,9 @@ public class SignatureKeyManagerTest {
private static final String ALGORITHM = "RSA";
@Mock SignatureKeyDao signatureKeyDao;
@Mock EventService eventService;
@Captor private ArgumentCaptor<EventSubscriber<WorkspaceStatusEvent>> captor;
private KeyPairGenerator kpg;
private SignatureKeyManager signatureKeyManager;
@ -58,18 +67,19 @@ public class SignatureKeyManagerTest {
public void createEntities() throws Exception {
kpg = KeyPairGenerator.getInstance(ALGORITHM);
kpg.initialize(KEY_SIZE);
signatureKeyManager = new SignatureKeyManager(KEY_SIZE, ALGORITHM, signatureKeyDao);
signatureKeyManager =
new SignatureKeyManager(KEY_SIZE, ALGORITHM, eventService, signatureKeyDao);
}
@Test
public void testLoadSignatureKeys() throws Exception {
final SignatureKeyPairImpl kp = newKeyPair("id_" + 1);
when(signatureKeyDao.getAll(anyInt(), anyLong()))
.thenReturn(new Page<>(singleton(kp), 0, 1, 1));
String wsId = "WS_id_1";
final SignatureKeyPairImpl kp = newKeyPair(wsId);
when(signatureKeyDao.get(anyString())).thenReturn(kp);
signatureKeyManager.loadKeyPair();
signatureKeyManager.loadKeyPair(wsId);
final KeyPair cachedPair = signatureKeyManager.getKeyPair();
final KeyPair cachedPair = signatureKeyManager.getKeyPair(wsId);
assertNotNull(cachedPair);
assertKeys(cachedPair.getPublic(), kp.getPublicKey());
assertKeys(cachedPair.getPrivate(), kp.getPrivateKey());
@ -77,51 +87,67 @@ public class SignatureKeyManagerTest {
@Test
public void testTriesToLoadKeysOnGettingKeyPairAndNoCachedKeyPair() throws Exception {
when(signatureKeyDao.getAll(anyInt(), anyLong()))
.thenThrow(new ServerException("unexpected end of stack"));
String wsId = "WS_id_1";
final SignatureKeyPairImpl kp = newKeyPair(wsId);
when(signatureKeyDao.create(any(SignatureKeyPairImpl.class))).thenReturn(kp);
when(signatureKeyDao.get(anyString())).thenThrow(new NotFoundException("not found"));
signatureKeyManager.getKeyPair();
signatureKeyManager.getKeyPair("ws1");
verify(signatureKeyDao).getAll(anyInt(), anyLong());
verify(signatureKeyDao).get(anyString());
verify(signatureKeyDao).create(any(SignatureKeyPairImpl.class));
}
@Test
public void testGeneratesNewKeyPairWhenNoExistingKeyPairFound() throws Exception {
doReturn(new Page<>(emptyList(), 0, 1, 0)).when(signatureKeyDao).getAll(anyInt(), anyLong());
doThrow(NotFoundException.class).when(signatureKeyDao).get(anyString());
when(signatureKeyDao.create(any(SignatureKeyPairImpl.class)))
.thenAnswer((Answer<SignatureKeyPairImpl>) invoke -> invoke.getArgument(0));
final KeyPair cachedPair = signatureKeyManager.getKeyPair();
final KeyPair cachedPair = signatureKeyManager.getKeyPair("ws1");
verify(signatureKeyDao).getAll(anyInt(), anyLong());
verify(signatureKeyDao).get(anyString());
verify(signatureKeyDao).create(any(SignatureKeyPairImpl.class));
assertNotNull(cachedPair);
}
@Test
public void testReturnNullKeyPairWhenFailedToLoadAndGenerateKeys() throws Exception {
doReturn(new Page<>(emptyList(), 0, 1, 0)).when(signatureKeyDao).getAll(anyInt(), anyLong());
@Test(expectedExceptions = ServerException.class)
public void testThrowsExceptionWhenFailedToLoadAndGenerateKeys() throws Exception {
doThrow(NotFoundException.class).when(signatureKeyDao).get(anyString());
when(signatureKeyDao.create(any(SignatureKeyPairImpl.class)))
.thenThrow(new ServerException("unexpected end of stack"));
final KeyPair cachedPair = signatureKeyManager.getKeyPair();
signatureKeyManager.getKeyPair("ws1");
verify(signatureKeyDao).getAll(anyInt(), anyLong());
verify(signatureKeyDao).get(anyString());
verify(signatureKeyDao).create(any(SignatureKeyPairImpl.class));
assertNull(cachedPair);
}
@Test
public void testReturnNullKeyPairWhenAlgorithmIsNotSupported() throws Exception {
@Test(expectedExceptions = ServerException.class)
public void testThrowsExceptionWhenAlgorithmIsNotSupported() throws Exception {
final SignatureKeyImpl publicKey = new SignatureKeyImpl(new byte[] {}, "ECDH", "PKCS#15");
final SignatureKeyImpl privateKey = new SignatureKeyImpl(new byte[] {}, "ECDH", "PKCS#3");
final SignatureKeyPairImpl kp = new SignatureKeyPairImpl("id_" + 1, publicKey, privateKey);
doReturn(new Page<>(singleton(kp), 0, 1, 1)).when(signatureKeyDao).getAll(anyInt(), anyLong());
doReturn(kp).when(signatureKeyDao).get(anyString());
final KeyPair cachedPair = signatureKeyManager.getKeyPair();
signatureKeyManager.getKeyPair("ws1");
verify(signatureKeyDao).getAll(anyInt(), anyLong());
assertNull(cachedPair);
verify(signatureKeyDao).get(anyString());
}
@Test
public void shouldRemoveKeyPairOnWorkspaceStop() throws Exception {
final String wsId = "ws123";
signatureKeyManager.subscribe();
verify(eventService).subscribe(captor.capture());
final EventSubscriber<WorkspaceStatusEvent> subscriber = captor.getValue();
subscriber.onEvent(
DtoFactory.newDto(WorkspaceStatusEvent.class)
.withStatus(WorkspaceStatus.STOPPED)
.withWorkspaceId(wsId));
verify(signatureKeyDao, times(1)).remove(eq(wsId));
}
private SignatureKeyPairImpl newKeyPair(String id) {

View File

@ -12,6 +12,20 @@
package org.eclipse.che.multiuser.machine.authentication.server.signature.jpa;
import com.google.inject.TypeLiteral;
import java.util.Collection;
import org.eclipse.che.account.spi.AccountImpl;
import org.eclipse.che.api.user.server.model.impl.UserImpl;
import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl;
import org.eclipse.che.api.workspace.server.model.impl.MachineConfigImpl;
import org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl;
import org.eclipse.che.api.workspace.server.model.impl.RecipeImpl;
import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl;
import org.eclipse.che.api.workspace.server.model.impl.SourceStorageImpl;
import org.eclipse.che.api.workspace.server.model.impl.VolumeImpl;
import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl;
import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
import org.eclipse.che.api.workspace.server.model.impl.stack.StackImpl;
import org.eclipse.che.commons.test.db.H2DBTestServer;
import org.eclipse.che.commons.test.db.H2JpaCleaner;
import org.eclipse.che.commons.test.db.PersistTestModuleBuilder;
@ -19,6 +33,7 @@ import org.eclipse.che.commons.test.tck.TckModule;
import org.eclipse.che.commons.test.tck.TckResourcesCleaner;
import org.eclipse.che.commons.test.tck.repository.JpaTckRepository;
import org.eclipse.che.commons.test.tck.repository.TckRepository;
import org.eclipse.che.commons.test.tck.repository.TckRepositoryException;
import org.eclipse.che.core.db.DBInitializer;
import org.eclipse.che.core.db.h2.jpa.eclipselink.H2ExceptionHandler;
import org.eclipse.che.core.db.schema.SchemaInitializer;
@ -37,7 +52,24 @@ public class SignatureKeyTckModule extends TckModule {
new PersistTestModuleBuilder()
.setDriver("org.h2.Driver")
.runningOn(server)
.addEntityClasses(SignatureKeyImpl.class, SignatureKeyPairImpl.class)
.addEntityClasses(
AccountImpl.class,
UserImpl.class,
SignatureKeyImpl.class,
SignatureKeyPairImpl.class,
WorkspaceImpl.class,
WorkspaceConfigImpl.class,
ProjectConfigImpl.class,
EnvironmentImpl.class,
MachineConfigImpl.class,
SourceStorageImpl.class,
ServerConfigImpl.class,
StackImpl.class,
CommandImpl.class,
RecipeImpl.class,
VolumeImpl.class)
.addEntityClass(
"org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl$Attribute")
.setExceptionHandler(H2ExceptionHandler.class)
.build());
@ -49,5 +81,23 @@ public class SignatureKeyTckModule extends TckModule {
bind(SignatureKeyDao.class).to(JpaSignatureKeyDao.class);
bind(new TypeLiteral<TckRepository<SignatureKeyPairImpl>>() {})
.toInstance(new JpaTckRepository<>(SignatureKeyPairImpl.class));
bind(new TypeLiteral<TckRepository<AccountImpl>>() {})
.toInstance(new JpaTckRepository<>(AccountImpl.class));
bind(new TypeLiteral<TckRepository<WorkspaceImpl>>() {}).toInstance(new WorkspaceRepository());
}
private static class WorkspaceRepository extends JpaTckRepository<WorkspaceImpl> {
public WorkspaceRepository() {
super(WorkspaceImpl.class);
}
@Override
public void createAll(Collection<? extends WorkspaceImpl> entities)
throws TckRepositoryException {
for (WorkspaceImpl entity : entities) {
entity.getConfig().getProjects().forEach(ProjectConfigImpl::prePersistAttributes);
}
super.createAll(entities);
}
}
}

View File

@ -14,16 +14,22 @@ package org.eclipse.che.multiuser.machine.authentication.server.signature.spi.tc
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.assertNotNull;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Stream;
import javax.inject.Inject;
import org.eclipse.che.account.spi.AccountImpl;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.Page;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl;
import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
import org.eclipse.che.commons.test.tck.TckListener;
import org.eclipse.che.commons.test.tck.repository.TckRepository;
import org.eclipse.che.commons.test.tck.repository.TckRepositoryException;
@ -47,9 +53,11 @@ public class SignatureKeyDaoTest {
public static final String ALGORITHM = "RSA";
public static final int KEY_SIZE = 512;
private static final int COUNT_KEY_PAIRS = 5;
private static final int COUNT_KEY_PAIRS = 3;
@Inject private TckRepository<SignatureKeyPairImpl> signatureKeyRepo;
@Inject private TckRepository<AccountImpl> accountRepository;
@Inject private TckRepository<WorkspaceImpl> workspaceRepository;
@Inject private SignatureKeyDao dao;
private SignatureKeyPairImpl[] storedKeyPairs;
@ -58,15 +66,39 @@ public class SignatureKeyDaoTest {
@AfterMethod
public void removeEntities() throws TckRepositoryException {
signatureKeyRepo.removeAll();
workspaceRepository.removeAll();
accountRepository.removeAll();
}
@BeforeMethod
public void createEntities() throws Exception {
AccountImpl account = new AccountImpl("account1", "accountName", "test");
accountRepository.createAll(Collections.singletonList(account));
workspaceRepository.createAll(
Arrays.asList(
new WorkspaceImpl(
"ws0",
account,
new WorkspaceConfigImpl("ws-name0", "", "cfg0", null, null, null, null)),
new WorkspaceImpl(
"ws1",
account,
new WorkspaceConfigImpl("ws-name1", "", "cfg1", null, null, null, null)),
new WorkspaceImpl(
"ws2",
account,
new WorkspaceConfigImpl("ws-name2", "", "cfg2", null, null, null, null)),
new WorkspaceImpl(
"id_10",
account,
new WorkspaceConfigImpl("ws-name10", "", "cfg1", null, null, null, null))));
storedKeyPairs = new SignatureKeyPairImpl[COUNT_KEY_PAIRS];
kpg = KeyPairGenerator.getInstance(ALGORITHM);
kpg.initialize(KEY_SIZE);
for (int i = 0; i < COUNT_KEY_PAIRS; i++) {
storedKeyPairs[i] = newKeyPair("id_" + i);
storedKeyPairs[i] = newKeyPair("ws" + i);
}
signatureKeyRepo.createAll(
Stream.of(storedKeyPairs).map(SignatureKeyPairImpl::new).collect(toList()));
@ -74,54 +106,48 @@ public class SignatureKeyDaoTest {
@Test
public void testGetsAllKeys() throws Exception {
final Page<SignatureKeyPairImpl> foundKeys = dao.getAll(COUNT_KEY_PAIRS, 0);
assertEquals(new HashSet<>(foundKeys.getItems()), new HashSet<>(asList(storedKeyPairs)));
assertEquals(foundKeys.getTotalItemsCount(), COUNT_KEY_PAIRS);
assertEquals(foundKeys.getItems().size(), COUNT_KEY_PAIRS);
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void testThrowsIllegalArgumentExceptionWhenMaxItemsIsNegative() throws Exception {
dao.getAll(-1, 0);
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void testThrowsIllegalArgumentExceptionWhenSkipCountIsNegative() throws Exception {
dao.getAll(1, -1);
List<SignatureKeyPairImpl> foundKeys = new ArrayList<>();
for (SignatureKeyPairImpl expected : storedKeyPairs) {
foundKeys.add(dao.get(expected.getWorkspaceId()));
}
assertEquals(new HashSet<>(foundKeys), new HashSet<>(asList(storedKeyPairs)));
assertEquals(foundKeys.size(), COUNT_KEY_PAIRS);
}
@Test(expectedExceptions = ConflictException.class)
public void throwsConflictExceptionWhenCreatingSignatureKeyPair() throws Exception {
final SignatureKeyPairImpl signKeyPair = newKeyPair(storedKeyPairs[0].getId());
final SignatureKeyPairImpl signKeyPair = newKeyPair(storedKeyPairs[0].getWorkspaceId());
dao.create(signKeyPair);
}
@Test(expectedExceptions = NotFoundException.class)
public void throwsNoResultExceptionWhenSearchingWrongWorkspace() throws Exception {
dao.get("unknown");
}
@Test
public void testCreatesSignatureKeyPair() throws Exception {
final SignatureKeyPairImpl signKeyPair = newKeyPair("id_" + 10);
dao.create(signKeyPair);
final Page<SignatureKeyPairImpl> keys = dao.getAll(COUNT_KEY_PAIRS + 1, 0);
assertTrue(keys.getItems().contains(signKeyPair));
assertEquals(keys.getTotalItemsCount(), COUNT_KEY_PAIRS + 1);
final SignatureKeyPairImpl kp = dao.get(signKeyPair.getWorkspaceId());
assertNotNull(kp);
assertEquals(kp, signKeyPair);
}
@Test
@Test(expectedExceptions = NotFoundException.class)
public void testRemovesSignatureKeyPair() throws Exception {
final SignatureKeyPairImpl toRemove = storedKeyPairs[0];
dao.remove(toRemove.getId());
dao.remove(toRemove.getWorkspaceId());
final Page<SignatureKeyPairImpl> keys = dao.getAll(COUNT_KEY_PAIRS, 0);
assertFalse(keys.getItems().contains(toRemove));
assertEquals(keys.getTotalItemsCount(), COUNT_KEY_PAIRS - 1);
dao.get(toRemove.getWorkspaceId());
}
private SignatureKeyPairImpl newKeyPair(String id) {
private SignatureKeyPairImpl newKeyPair(String workspaceId) {
final KeyPair pair = kpg.generateKeyPair();
return new SignatureKeyPairImpl(id, pair.getPublic(), pair.getPrivate());
return new SignatureKeyPairImpl(workspaceId, pair.getPublic(), pair.getPrivate());
}
}

View File

@ -0,0 +1,34 @@
--
-- Copyright (c) 2012-2018 Red Hat, Inc.
-- This program and the accompanying materials are made
-- available under the terms of the Eclipse Public License 2.0
-- which is available at https://www.eclipse.org/legal/epl-2.0/
--
-- SPDX-License-Identifier: EPL-2.0
--
-- Contributors:
-- Red Hat, Inc. - initial API and implementation
--
-- Rename old table
ALTER TABLE che_sign_key_pair RENAME TO che_sign_key_pair_old;
-- Create new key pair table
CREATE TABLE che_sign_key_pair (
workspace_id VARCHAR(255) NOT NULL,
public_key BIGINT NOT NULL,
private_key BIGINT NOT NULL,
PRIMARY KEY (workspace_id)
);
-- Constraint
ALTER TABLE che_sign_key_pair ADD CONSTRAINT fk_sign_workspace_id FOREIGN KEY (workspace_id) REFERENCES workspace (id);
-- Copy data
INSERT INTO che_sign_key_pair
SELECT r.workspace_id, k.public_key, k.private_key
FROM che_k8s_runtime AS r,
(SELECT public_key, private_key FROM che_sign_key_pair_old LIMIT 1) AS k;
-- Cleanup
DROP TABLE che_sign_key_pair_old CASCADE;