mirror of https://github.com/jacekkow/keycloak-protocol-cas

a78160f1d21ec4c5d60f89623ce5bca9e37d7029..d44fdd6cff6cddb609a468c352018fa89d0b4084
4 days ago github-actions
Update to Keycloak 26.7.2
d44fdd diff | tree
2026-08-06 github-actions
Update to Keycloak 26.7.1
717710 diff | tree
3 hours ago Jacek Kowalski
Switch to using custom RedirectUtils adapted from Keycloak
570027 diff | tree
3 hours ago Jacek Kowalski
Merge RedirectUtils.java from Keycloak repo
7c8f79 diff | tree
2026-06-18 jimmychakkalakal
Reject redirect URIs containing pre-loaded OIDC response parameters (#49959)
e44df4 diff | tree
2026-05-21 Kosiorkosa47
Tighten UNSAFE_PATH_PATTERN against encoded path-traversal terminators (#49...
6f46fb diff | tree
2026-05-20 Ricardo Martin
Wildcards should not be allowed if authority cannot be parsed (#607) (#49130)
368d47 diff | tree
2026-05-12 Ricardo Martin
Do not allow wildcards in the hostname for Valid Redirect Address (#48793)
5e0093 diff | tree
2026-04-07 Ricardo Martin
Consider path params in the unsafe path for RedirectUtils (#436) (#47788)
9b78be diff | tree
2026-02-10 Marie Daly
Redirect Wildcard changes and more https checks to secure-client-executor (...
7bc9d9 diff | tree
2025-11-14 Stian Thorgersen
Proposed import order (#43432)
1a850e diff | tree
2024-09-23 Stian Thorgersen
Improve handling for loopback redirect-uri validation (#195) (#33189)
512bcc diff | tree
2024-08-30 Jon Koops
Remove `redirect_uri` support from OIDC logout endpoint
91f764 diff | tree
2024-04-18 Ricardo Martin
Perform exact string match if redirect URI contains userinfo, encoded slash...
9aa92e diff | tree
2024-02-16 Marek Posolda
Remove random redirect after password reset (#27076)
758ec0 diff | tree
2024-01-09 Ricardo Martin
Escape action in the form_post.jwt and only decode path in RedirectUtils (#...
760bd7 diff | tree
2023-12-06 Ricardo Martin
Strip off user-info from redirect URI when validating using wildcard (#61) ...
994c30 diff | tree
2023-11-29 rmartinc
Remove lowercase for the hostname as recommended/advised by OAuth spec Clos...
660195 diff | tree
2023-11-29 rmartinc
Revert "Fix lowerCaseHostname to lower-case scheme and host properly"
20ca96 diff | tree
2023-11-17 rmartinc
Fix lowerCaseHostname to lower-case scheme and host properly Closes https:/...
065acd diff | tree
2023-09-19 rmartinc
verifyRedirectUri should return null when the passed redirectUri is invalid...
d5bdcb diff | tree
2023-08-16 rmartinc
RedirectUtils needs to use KeycloakUriBuilder with no parameter parsing Clo...
7b53ca diff | tree
2023-06-01 Ricardo Martin
Check the redirect URI is http(s) when used for a form Post (#22)
b3661a diff | tree
2023-03-29 mposolda
Regressions in redirect URL verification when redirect_uri has encoded path...
0eef2d diff | tree
2022-12-14 Stian Thorgersen
fixes from release/20 (#15982)
226dd1 diff | tree
2022-02-18 Hynek Mlnarik
Move realms, clients, groups, roles, clientscopes into legacy module
d99b4e diff | tree
2022-03-30 Marek Posolda
OIDC RP-Initiated logout endpoint (#10887)
8f62c5 diff | tree
2021-04-13 Hynek Mlnarik
KEYCLOAK-17748 Optimize validation of redirect URIs in logout endpoint
f3adcc diff | tree
2021-02-10 mposolda
KEYCLOAK-16517 Make sure that just real clients with standardFlow or implic...
2532c4 diff | tree
2021-01-07 mposolda
KEYCLOAK-14019 Improvements for request_uri parameter
8fbf16 diff | tree
2020-11-20 st
KEYCLOAK-11699 add support for 127.0.0.1 for native app
bb79a3 diff | tree
2020-07-29 Martin Kanis
KEYCLOAK-14901 Replace deprecated ClientProvider related methods across Key...
b290c2 diff | tree
2020-06-08 Martin Bartos
[KEYCLOAK-13202] Reset password redirects to account client
c85cab diff | tree
2020-02-20 Pedro Igor
[KEYCLOAK-11576] - Properly handling redirect_uri parser errors
edac0c diff | tree
2019-10-16 stianst
KEYCLOAK-11728 New default hostname provider
10c4c9 diff | tree
2018-09-17 vramik
KEYCLOAK-7707
02417b diff | tree
2018-09-13 Bruno Oliveira da Silva
Redirect URLs are not normalized
d0daeb diff | tree
2017-06-20 mposolda
KEYCLOAK-3316 Fixes for OAuth2 requests without 'scope=openid'
4535e9 diff | tree
2016-09-19 Bruno Oliveira
[KEYCLOAK-3086] - NPE when accessing Account with invalid clientId set as ...
b81acf diff | tree
2016-10-19 Stian Thorgersen
KEYCLOAK-3364 Fix for dns that ends with digit
737535 diff | tree
2016-10-17 Stian Thorgersen
KEYCLOAK-3635 Not possible to filter debug/trace logging
9d783e diff | tree
2016-10-04 Marek Posolda
Merge pull request #3254 from didiez/master
000ada diff | tree
2016-10-01 Bill Burke
Merge pull request #3275 from patriot1burke/master
30fe79 diff | tree
2016-09-28 Bill Burke
refactor mongo entities, optimize imports
117c91 diff | tree
2016-09-22 Diego Díez Ricondo
Merge pull request #1 from keycloak/master
cd2057 diff | tree
2016-09-05 Stian Thorgersen
Merge pull request #3150 from stoffus/bug-fix-in-reset-password-tpl
170cf7 diff | tree
2016-08-18 Christopher Svensson
Merge remote-tracking branch 'keycloak/master'
85c96d diff | tree
2016-08-12 Vaclav Muzikar
KEYCLOAK-3429 Fix behaviour of redirect_uri parameter with query components
bf2a84 diff | tree
2016-07-07 mposolda
KEYCLOAK-3147 Don't allow authRequest without redirect_uri parameter
fd2f33 diff | tree
2016-02-03 Stian Thorgersen
Merge pull request #2162 from stianst/master
624dde diff | tree
1 files added
4 files modified
299 ■■■■■ changed files
pom.xml 6 ●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java 2 ●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/endpoints/AuthorizationEndpoint.java 2 ●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/endpoints/LogoutEndpoint.java 2 ●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/utils/RedirectUtils.java 287 ●●●●● patch | view | raw | blame | history
pom.xml
@@ -22,7 +22,7 @@
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-protocol-cas</artifactId>
    <version>26.7.0</version>
    <version>26.7.2</version>
    <name>Keycloak CAS Protocol</name>
    <description />
@@ -39,7 +39,7 @@
        <maven.compiler.release>${java.version}</maven.compiler.release>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.outputTimestamp>1783596721</project.build.outputTimestamp>
        <project.build.outputTimestamp>1787130879</project.build.outputTimestamp>
    </properties>
    <dependencies>
@@ -115,7 +115,7 @@
        <dependency>
            <groupId>org.xmlunit</groupId>
            <artifactId>xmlunit-core</artifactId>
            <version>2.12.0</version>
            <version>2.13.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java
@@ -12,8 +12,8 @@
import org.keycloak.protocol.cas.mappers.CASAttributeMapper;
import org.keycloak.protocol.cas.representations.CASErrorCode;
import org.keycloak.protocol.cas.utils.CASValidationException;
import org.keycloak.protocol.cas.utils.RedirectUtils;
import org.keycloak.protocol.oidc.utils.OAuth2Code;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.util.DefaultClientSessionContext;
src/main/java/org/keycloak/protocol/cas/endpoints/AuthorizationEndpoint.java
@@ -12,7 +12,7 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.cas.CASLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.cas.utils.RedirectUtils;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.CacheControlUtil;
src/main/java/org/keycloak/protocol/cas/endpoints/LogoutEndpoint.java
@@ -10,7 +10,7 @@
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.cas.CASLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.cas.utils.RedirectUtils;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
src/main/java/org/keycloak/protocol/cas/utils/RedirectUtils.java
New file
@@ -0,0 +1,287 @@
/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.keycloak.protocol.cas.utils;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.UriUtils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakUriInfo;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.cas.CASLoginProtocol;
import org.keycloak.services.Urls;
import org.keycloak.services.util.ResolveRelative;
import org.jboss.logging.Logger;
/**
 * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
 */
public class RedirectUtils {
    public static final Set<String> LOOPBACK_INTERFACES = new HashSet<>(Arrays.asList("localhost", "127.0.0.1", "[::1]"));
    private static final Set<String> FORBIDDEN_OIDC_PARAMS = Set.of(
            CASLoginProtocol.SAMLART_RESPONSE_PARAM.toLowerCase(Locale.ROOT),
            CASLoginProtocol.TICKET_RESPONSE_PARAM.toLowerCase(Locale.ROOT)
    );
    private static final Logger logger = Logger.getLogger(RedirectUtils.class);
    public static String verifyRedirectUri(KeycloakSession session, String redirectUri, ClientModel client) {
        return verifyRedirectUri(session, redirectUri, client, true);
    }
    public static String verifyRedirectUri(KeycloakSession session, String redirectUri, ClientModel client, boolean requireRedirectUri) {
        if (client != null)
            return verifyRedirectUri(session, client.getRootUrl(), redirectUri, client.getRedirectUris(), requireRedirectUri);
        return null;
    }
    public static Set<String> resolveValidRedirects(KeycloakSession session, String rootUrl, Set<String> validRedirects) {
        // If the valid redirect URI is relative (no scheme, host, port) then use the request's scheme, host, and port
        // the set is ordered by length to get the longest match first
        Set<String> resolveValidRedirects = new TreeSet<>((String s1, String s2) -> s1.length() == s2.length()? s1.compareTo(s2) : s1.length() < s2.length()? 1 : -1);
        for (String validRedirect : validRedirects) {
            if (validRedirect.startsWith("/")) {
                validRedirect = relativeToAbsoluteURI(session, rootUrl, validRedirect);
                logger.debugv("replacing relative valid redirect with: {0}", validRedirect);
            }
            resolveValidRedirects.add(validRedirect);
        }
        return resolveValidRedirects;
    }
    public static String verifyRedirectUri(KeycloakSession session, String rootUrl, String redirectUri, Set<String> validRedirects, boolean requireRedirectUri) {
        KeycloakUriInfo uriInfo = session.getContext().getUri();
        RealmModel realm = session.getContext().getRealm();
        if (redirectUri == null) {
            if (!requireRedirectUri) {
                redirectUri = getSingleValidRedirectUri(validRedirects);
            }
            if (redirectUri == null) {
                logger.debug("No Redirect URI parameter specified");
                return null;
            }
        } else if (validRedirects.isEmpty()) {
            logger.debug("No Redirect URIs supplied");
            redirectUri = null;
        } else {
            URI originalRedirect = toUri(redirectUri);
            if (originalRedirect == null) {
                // invalid URI passed as redirectUri
                return null;
            }
            // Check for HTTP Parameter Pollution - forbidden OIDC response parameters in redirect URI
            if (containsForbiddenOidcParameters(originalRedirect)){
                return null;
            }
            // check if the passed URI allows wildcards
            boolean allowWildcards = areWildcardsAllowed(originalRedirect);
            String r = redirectUri;
            Set<String> resolveValidRedirects = resolveValidRedirects(session, rootUrl, validRedirects);
            String valid = matchesRedirects(resolveValidRedirects, r, allowWildcards);
            if (valid == null && "http".equals(originalRedirect.getScheme()) && LOOPBACK_INTERFACES.contains(originalRedirect.getHost())) {
                String redirectWithDefaultPort = KeycloakUriBuilder.fromUri(originalRedirect).port(80).buildAsString();
                valid = matchesRedirects(resolveValidRedirects, redirectWithDefaultPort, allowWildcards);
            }
            if (valid != null && !originalRedirect.isAbsolute()) {
                // return absolute if the original URI is relative
                if (!redirectUri.startsWith("/")) {
                    redirectUri = "/" + redirectUri;
                }
                redirectUri = relativeToAbsoluteURI(session, rootUrl, redirectUri);
            }
            String scheme = originalRedirect.getScheme();
            if (valid != null && scheme != null) {
                // check the scheme is valid, it should be http(s) or explicitly allowed by the validation
                if (!valid.startsWith(scheme + ":") && !"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
                    logger.debugf("Invalid URI because scheme is not allowed: %s", redirectUri);
                    valid = null;
                }
            }
            redirectUri = valid != null ? redirectUri : null;
        }
        if (Constants.INSTALLED_APP_URN.equals(redirectUri)) {
            return Urls.realmInstalledAppUrnCallback(uriInfo.getBaseUri(), realm.getName()).toString();
        } else {
            return redirectUri;
        }
    }
    private static boolean containsForbiddenOidcParameters(URI originalRedirect) {
        String query = originalRedirect.getRawQuery();
        if (query != null && !query.isEmpty()) {
            MultivaluedHashMap<String, String> params =UriUtils.decodeQueryString(query);
            for (String paramName : params.keySet()) {
                if (FORBIDDEN_OIDC_PARAMS.contains(paramName.toLowerCase(Locale.ROOT))) {
                    logger.warnf("Redirect URI rejected: contains forbidden OIDC parameter '%s' in query string: scheme=%s, host=%s, path=%s",
                            paramName,
                            originalRedirect.getScheme(),
                            originalRedirect.getHost(),
                            originalRedirect.getPath());
                    return true;
                }
            }
        }
        return false;
    }
    private static URI toUri(String redirectUri) {
        URI uri = null;
        if (redirectUri != null) {
            try {
                uri = URI.create(redirectUri);
            } catch (IllegalArgumentException cause) {
                logger.debugf(cause, "Invalid redirect uri %s", redirectUri);
            } catch (Exception cause) {
                logger.debugf(cause, "Unexpected error when parsing redirect uri %s", redirectUri);
            }
        }
        return uri;
    }
    // any access to parent folder /../ is unsafe with or without encoding
    //   <sep>             = / | %2F | %5C | \
    //   <dots>            = "..", including %2E and %252E (double-encoded) variants
    //   <terminator>      = / | %2F | %5C | \ | ; | %3B | %09 | %0A | %0D | %00 | end-of-input
    private final static Pattern UNSAFE_PATH_PATTERN = Pattern.compile(
            "(/|%2[fF]|%5[cC]|\\\\)(%2[eE]|%252[eE]|\\.){2}(/|%2[fF]|%5[cC]|\\\\|;|%3[bB]|%09|%0[aAdD]|%00|$)");
    private static boolean areWildcardsAllowed(URI redirectUri) {
        // wildcars are only allowed if no user-info and no unparsed authority and no unsafe pattern in path
        return redirectUri.getRawUserInfo() == null
                && !(redirectUri.getRawAuthority() != null && redirectUri.getRawUserInfo() == null && redirectUri.getHost() == null && redirectUri.getPort() == -1)
                && (redirectUri.getRawPath() == null || !UNSAFE_PATH_PATTERN.matcher(redirectUri.getRawPath()).find());
    }
    private static String relativeToAbsoluteURI(KeycloakSession session, String rootUrl, String relative) {
        if (rootUrl != null) {
            rootUrl = ResolveRelative.resolveRootUrl(session, rootUrl);
        }
        if (rootUrl == null || rootUrl.isEmpty()) {
            rootUrl = UriUtils.getOrigin(session.getContext().getUri().getBaseUri());
        }
        StringBuilder sb = new StringBuilder();
        sb.append(rootUrl);
        sb.append(relative);
        return sb.toString();
    }
    // return the String that matched the redirect or null if not matched
    private static String matchesRedirects(Set<String> validRedirects, String redirect, boolean allowWildcards) {
        logger.tracef("matchesRedirects: redirect URL to check: %s, allow wildcards: %b, Configured valid redirect URLs: %s", redirect, allowWildcards, validRedirects);
        for (String validRedirect : validRedirects) {
            if ("*".equals(validRedirect)) {
                // the valid redirect * is a full wildcard for http(s) even if the redirect URI does not allow wildcards
                return validRedirect;
            } else {
                String validRedirectWildcard = allowWildcards ? checkValidRedirectWildcard(validRedirect) : null;
                if (validRedirectWildcard != null) {
                    // strip off the query or fragment components - we don't check them when wildcards are effective
                    int idx = redirect.indexOf('?');
                    if (idx == -1) {
                        idx = redirect.indexOf('#');
                    }
                    String r = idx == -1 ? redirect : redirect.substring(0, idx);
                    // strip off *
                    int length = validRedirectWildcard.length() - 1;
                    validRedirectWildcard = validRedirectWildcard.substring(0, length);
                    if (r.startsWith(validRedirectWildcard)) {
                        return validRedirectWildcard;
                    }
                    // strip off trailing '/'
                    if (length - 1 > 0 && validRedirectWildcard.charAt(length - 1) == '/') {
                        length--;
                    }
                    validRedirectWildcard = validRedirectWildcard.substring(0, length);
                    if (validRedirectWildcard.equals(r)) {
                        return validRedirectWildcard;
                    }
                } else if (validRedirect.equals(redirect)) {
                    return validRedirect;
                }
            }
        }
        return null;
    }
    private static String checkValidRedirectWildcard(String validRedirect) {
        if (!validRedirect.endsWith("*") || validRedirect.contains("?") || validRedirect.contains("#")) {
            return null; // no wildcard as before
        }
        KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(validRedirect, false);
        if (uriBuilder.getPath() != null) {
            return validRedirect; // wildcard valid on path
        }
        if (uriBuilder.getAuthority() != null) {
            if (uriBuilder.getAuthority().equals("*") || uriBuilder.getAuthority().endsWith(":*")) {
                return validRedirect; // on authority just full wildcard or on port
            } else {
                // treat the wildcard after the authority
                validRedirect = validRedirect.substring(0, validRedirect.length() - 1);
                validRedirect = validRedirect + "/*";
                return validRedirect;
            }
        }
        if (uriBuilder.getSsp() != null) {
            return validRedirect; // wildcard valid on SSP
        }
        return null;
    }
    private static String getSingleValidRedirectUri(Collection<String> validRedirects) {
        if (validRedirects.size() != 1) return null;
        String validRedirect = validRedirects.iterator().next();
        return validateRedirectUriWildcard(validRedirect);
    }
    public static String validateRedirectUriWildcard(String redirectUri) {
        if (redirectUri == null)
            return null;
        int idx = redirectUri.indexOf("/*");
        if (idx > -1) {
            redirectUri = redirectUri.substring(0, idx);
        }
        return redirectUri;
    }
}