KEYCLOAK-15998 Keycloak OIDC adapter broken when Keycloak server is on http
This commit is contained in:
parent
c8d0f2c59c
commit
7891daef73
5 changed files with 71 additions and 17 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
config.setJwksUri(jwksUri.toString());
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
config.setRevocationEndpoint(revocationEndpoint.toString());
|
||||
config.setRevocationEndpointAuthMethodsSupported(getClientAuthMethodsSupported());
|
||||
config.setRevocationEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false));
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue