Improve handling for loopback redirect-uri validation (#195) (#33189)

Closes #33116

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-09-23 13:51:02 +02:00 committed by GitHub
parent a576f77910
commit af5eef57bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 51 additions and 20 deletions

View file

@ -43,8 +43,6 @@ public final class Constants {
public static final Collection<String> defaultClients = Arrays.asList(ACCOUNT_MANAGEMENT_CLIENT_ID, ADMIN_CLI_CLIENT_ID, BROKER_SERVICE_CLIENT_ID, REALM_MANAGEMENT_CLIENT_ID, ADMIN_CONSOLE_CLIENT_ID); public static final Collection<String> defaultClients = Arrays.asList(ACCOUNT_MANAGEMENT_CLIENT_ID, ADMIN_CLI_CLIENT_ID, BROKER_SERVICE_CLIENT_ID, REALM_MANAGEMENT_CLIENT_ID, ADMIN_CONSOLE_CLIENT_ID);
public static final String INSTALLED_APP_URN = "urn:ietf:wg:oauth:2.0:oob"; public static final String INSTALLED_APP_URN = "urn:ietf:wg:oauth:2.0:oob";
public static final String INSTALLED_APP_URL = "http://localhost";
public static final String INSTALLED_APP_LOOPBACK = "http://127.0.0.1";
public static final String READ_TOKEN_ROLE = "read-token"; public static final String READ_TOKEN_ROLE = "read-token";
public static final String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE}; public static final String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE};

View file

@ -18,6 +18,7 @@
package org.keycloak.protocol.oidc.utils; package org.keycloak.protocol.oidc.utils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.UriUtils; import org.keycloak.common.util.UriUtils;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
@ -28,7 +29,9 @@ import org.keycloak.services.Urls;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import java.net.URI; import java.net.URI;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -38,6 +41,8 @@ import java.util.regex.Pattern;
*/ */
public class RedirectUtils { public class RedirectUtils {
public static final Set<String> LOOPBACK_INTERFACES = new HashSet<>(Arrays.asList("localhost", "127.0.0.1", "[::1]"));
private static final Logger logger = Logger.getLogger(RedirectUtils.class); private static final Logger logger = Logger.getLogger(RedirectUtils.class);
public static String verifyRedirectUri(KeycloakSession session, String redirectUri, ClientModel client) { public static String verifyRedirectUri(KeycloakSession session, String redirectUri, ClientModel client) {
@ -95,20 +100,9 @@ public class RedirectUtils {
String valid = matchesRedirects(resolveValidRedirects, r, allowWildcards); String valid = matchesRedirects(resolveValidRedirects, r, allowWildcards);
if (valid == null && (r.startsWith(Constants.INSTALLED_APP_URL) || r.startsWith(Constants.INSTALLED_APP_LOOPBACK)) && r.indexOf(':', Constants.INSTALLED_APP_URL.length()) >= 0) { if (valid == null && "http".equals(originalRedirect.getScheme()) && LOOPBACK_INTERFACES.contains(originalRedirect.getHost())) {
int i = r.indexOf(':', Constants.INSTALLED_APP_URL.length()); String redirectWithDefaultPort = KeycloakUriBuilder.fromUri(originalRedirect).port(80).buildAsString();
valid = matchesRedirects(resolveValidRedirects, redirectWithDefaultPort, allowWildcards);
StringBuilder sb = new StringBuilder();
sb.append(r.substring(0, i));
i = r.indexOf('/', i);
if (i >= 0) {
sb.append(r.substring(i));
}
r = sb.toString();
valid = matchesRedirects(resolveValidRedirects, r, allowWildcards);
} }
if (valid != null && !originalRedirect.isAbsolute()) { if (valid != null && !originalRedirect.isAbsolute()) {

View file

@ -71,6 +71,45 @@ public class RedirectUtilsTest {
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.com/test", set, false)); Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.com/test", set, false));
} }
@Test
public void testVerifyRedirectUriNative() {
Set<String> set = Stream.of(
"http://127.0.0.1",
"http://127.0.0.1/callback",
"http://[::1]",
"http://[::1]/callback",
"http://127.0.0.1/callback",
"http://localhost",
"http://localhost/callback"
).collect(Collectors.toSet());
Assert.assertEquals("http://127.0.0.1", RedirectUtils.verifyRedirectUri(session, null, "http://127.0.0.1", set, false));
Assert.assertEquals("http://127.0.0.1:12324", RedirectUtils.verifyRedirectUri(session, null, "http://127.0.0.1:12324", set, false));
Assert.assertEquals("http://127.0.0.1/callback", RedirectUtils.verifyRedirectUri(session, null, "http://127.0.0.1/callback", set, false));
Assert.assertEquals("http://127.0.0.1:12324/callback", RedirectUtils.verifyRedirectUri(session, null, "http://127.0.0.1:12324/callback", set, false));
Assert.assertEquals("http://[::1]", RedirectUtils.verifyRedirectUri(session, null, "http://[::1]", set, false));
Assert.assertEquals("http://[::1]:12324", RedirectUtils.verifyRedirectUri(session, null, "http://[::1]:12324", set, false));
Assert.assertEquals("http://[::1]/callback", RedirectUtils.verifyRedirectUri(session, null, "http://[::1]/callback", set, false));
Assert.assertEquals("http://[::1]:12324/callback", RedirectUtils.verifyRedirectUri(session, null, "http://[::1]:12324/callback", set, false));
Assert.assertEquals("http://localhost", RedirectUtils.verifyRedirectUri(session, null, "http://localhost", set, false));
Assert.assertEquals("http://localhost:12324", RedirectUtils.verifyRedirectUri(session, null, "http://localhost:12324", set, false));
Assert.assertEquals("http://localhost/callback", RedirectUtils.verifyRedirectUri(session, null, "http://localhost/callback", set, false));
Assert.assertEquals("http://localhost:12324/callback", RedirectUtils.verifyRedirectUri(session, null, "http://localhost:12324/callback", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://127.0.0.1@invalid.com", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://127.0.0.1:12324@invalid.com", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://127.0.0.1@invalid.com/callback", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://127.0.0.1:12324@invalid.com/callback", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://[::1]@invalid.com", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://[::1]:12324@invalid.com", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://[::1]@invalid.com/callback", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://[::1]:12324@invalid.com/callback", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://localhost@invalid.com", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://localhost:12324@invalid.com", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://localhost@invalid.com/callback", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "http://localhost:12324@invalid.com/callback", set, false));
}
@Test @Test
public void testverifyRedirectUriMixedSchemes() { public void testverifyRedirectUriMixedSchemes() {
Set<String> set = Stream.of( Set<String> set = Stream.of(

View file

@ -106,12 +106,12 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest {
RealmBuilder realm = RealmBuilder.edit(realmRepresentation).testEventListener(); RealmBuilder realm = RealmBuilder.edit(realmRepresentation).testEventListener();
ClientBuilder installedApp = ClientBuilder.create().clientId("test-installed").name("test-installed") ClientBuilder installedApp = ClientBuilder.create().clientId("test-installed").name("test-installed")
.redirectUris(Constants.INSTALLED_APP_URN, Constants.INSTALLED_APP_URL) .redirectUris(Constants.INSTALLED_APP_URN, "http://localhost")
.secret("password"); .secret("password");
realm.client(installedApp); realm.client(installedApp);
ClientBuilder installedApp2 = ClientBuilder.create().clientId("test-installed2").name("test-installed2") ClientBuilder installedApp2 = ClientBuilder.create().clientId("test-installed2").name("test-installed2")
.redirectUris(Constants.INSTALLED_APP_URL + "/myapp") .redirectUris("http://localhost/myapp")
.secret("password"); .secret("password");
realm.client(installedApp2); realm.client(installedApp2);
@ -158,12 +158,12 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest {
realm.client(installedAppCustomScheme); realm.client(installedAppCustomScheme);
ClientBuilder installedAppLoopback = ClientBuilder.create().clientId("test-installed-loopback").name("test-installed-loopback") ClientBuilder installedAppLoopback = ClientBuilder.create().clientId("test-installed-loopback").name("test-installed-loopback")
.redirectUris(Constants.INSTALLED_APP_LOOPBACK) .redirectUris("http://127.0.0.1")
.secret("password"); .secret("password");
realm.client(installedAppLoopback); realm.client(installedAppLoopback);
ClientBuilder installedAppLoopback2 = ClientBuilder.create().clientId("test-installed-loopback2").name("test-installed-loopback2") ClientBuilder installedAppLoopback2 = ClientBuilder.create().clientId("test-installed-loopback2").name("test-installed-loopback2")
.redirectUris(Constants.INSTALLED_APP_LOOPBACK + "/myapp") .redirectUris("http://127.0.0.1/myapp")
.secret("password"); .secret("password");
realm.client(installedAppLoopback2); realm.client(installedAppLoopback2);