KEYCLOAK-15998 Keycloak OIDC adapter broken when Keycloak server is on http

This commit is contained in:
mposolda 2020-10-19 21:01:40 +02:00 committed by Marek Posolda
parent c8d0f2c59c
commit 7891daef73
5 changed files with 71 additions and 17 deletions

View file

@ -21,6 +21,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.ClientConnection;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
@ -41,6 +42,7 @@ import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.protocol.oidc.endpoints.TokenRevocationEndpoint;
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
import org.keycloak.protocol.oidc.ext.OIDCExtProvider;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors;
@ -215,6 +217,8 @@ public class OIDCLoginProtocolService {
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Response certs() {
checkSsl();
List<JWK> keys = new LinkedList<>();
for (KeyWrapper k : session.keys().getKeys(realm)) {
if (k.getStatus().isEnabled() && k.getUse().equals(KeyUse.SIG) && k.getPublicKey() != null) {
@ -308,4 +312,13 @@ public class OIDCLoginProtocolService {
throw new NotFoundException();
}
private void checkSsl() {
if (!session.getContext().getUri().getBaseUri().getScheme().equals("https")
&& realm.getSslRequired().isRequired(clientConnection)) {
Cors cors = Cors.add(request).auth().allowedMethods(request.getHttpMethod()).auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
throw new CorsErrorResponseException(cors.allowAllOrigins(), OAuthErrorException.INVALID_REQUEST, "HTTPS required",
Response.Status.FORBIDDEN);
}
}
}

View file

@ -98,9 +98,12 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setLogoutEndpoint(frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "logout").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
URI jwksUri = backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(),
OIDCLoginProtocol.LOGIN_PROTOCOL);
if (isHttps(jwksUri)) {
// NOTE: Don't hardcode HTTPS checks here. JWKS URI is exposed just in the development/testing environment. For the production environment, the OIDCWellKnownProvider
// is not exposed over "http" at all.
//if (isHttps(jwksUri)) {
config.setJwksUri(jwksUri.toString());
}
config.setCheckSessionIframe(frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "getLoginStatusIframe").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(backendUriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString());
@ -140,11 +143,13 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
URI revocationEndpoint = frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "revoke")
.build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL);
if (isHttps(revocationEndpoint)) {
// NOTE: Don't hardcode HTTPS checks here. JWKS URI is exposed just in the development/testing environment. For the production environment, the OIDCWellKnownProvider
// is not exposed over "http" at all.
//if (isHttps(jwksUri)) {
config.setRevocationEndpoint(revocationEndpoint.toString());
config.setRevocationEndpointAuthMethodsSupported(getClientAuthMethodsSupported());
config.setRevocationEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false));
}
config.setBackchannelLogoutSupported(true);
config.setBackchannelLogoutSessionSupported(true);
@ -215,8 +220,4 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
}
return result;
}
private boolean isHttps(URI uri) {
return uri.getScheme().equals("https");
}
}

View file

@ -19,6 +19,7 @@ package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.AuthorizationService;
import org.keycloak.common.ClientConnection;
@ -30,6 +31,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.clientregistration.ClientRegistrationService;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resource.RealmResourceProvider;
@ -245,7 +247,8 @@ public class RealmsResource {
@Produces(MediaType.APPLICATION_JSON)
public Response getWellKnown(final @PathParam("realm") String name,
final @PathParam("provider") String providerName) {
init(name);
RealmModel realm = init(name);
checkSsl(realm);
WellKnownProvider wellKnown = session.getProvider(WellKnownProvider.class, providerName);
@ -287,4 +290,13 @@ public class RealmsResource {
throw new NotFoundException();
}
private void checkSsl(RealmModel realm) {
if (!session.getContext().getUri().getBaseUri().getScheme().equals("https")
&& realm.getSslRequired().isRequired(clientConnection)) {
Cors cors = Cors.add(request).auth().allowedMethods(request.getHttpMethod()).auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
throw new CorsErrorResponseException(cors.allowAllOrigins(), OAuthErrorException.INVALID_REQUEST, "HTTPS required",
Response.Status.FORBIDDEN);
}
}
}

View file

@ -183,12 +183,12 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
try {
OIDCConfigurationRepresentation oidcConfig = getOIDCDiscoveryRepresentation(client, "http://localhost:8180/auth");
assertNull(oidcConfig.getJwksUri());
Assert.assertNotNull(oidcConfig.getJwksUri());
// Token Revocation
assertNull(oidcConfig.getRevocationEndpoint());
Assert.assertNull(oidcConfig.getRevocationEndpointAuthMethodsSupported());
Assert.assertNull(oidcConfig.getRevocationEndpointAuthSigningAlgValuesSupported());
Assert.assertNotNull(oidcConfig.getRevocationEndpoint());
Assert.assertNotNull(oidcConfig.getRevocationEndpointAuthMethodsSupported());
Assert.assertNotNull(oidcConfig.getRevocationEndpointAuthSigningAlgValuesSupported());
} finally {
client.close();
}

View file

@ -6,6 +6,7 @@ import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
@ -47,4 +48,31 @@ public class TLSTest extends AbstractTestRealmKeycloakTest {
Assert.assertTrue(config.getAuthorizationEndpoint().startsWith(AUTH_SERVER_ROOT_WITHOUT_TLS));
}
@Test
public void testSSLAlwaysRequired() throws Exception {
// Switch realm SSLRequired to Always
RealmRepresentation realmRep = testRealm().toRepresentation();
String origSslRequired = realmRep.getSslRequired();
realmRep.setSslRequired(SslRequired.ALL.toString());
testRealm().update(realmRep);
// Try access "WellKnown" endpoint unsecured. It should fail
oauth.baseUrl(AUTH_SERVER_ROOT_WITHOUT_TLS);
OIDCConfigurationRepresentation config = oauth.doWellKnownRequest("test");
Assert.assertNull(config.getAuthorizationEndpoint());
Assert.assertEquals("HTTPS required", config.getOtherClaims().get("error_description"));
// Try access "JWKS URL" unsecured. It should fail
try {
JSONWebKeySet keySet = oauth.doCertsRequest("test");
Assert.fail("This should not be successful");
} catch (Exception e) {
// Expected
}
// Revert SSLRequired
realmRep.setSslRequired(origSslRequired);
testRealm().update(realmRep);
}
}