diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js index 1323ec74992..ae476508acb 100755 --- a/adapters/oidc/js/src/main/resources/keycloak.js +++ b/adapters/oidc/js/src/main/resources/keycloak.js @@ -165,8 +165,11 @@ var callback = parseCallback(window.location.href); if (callback) { + window.history.replaceState({}, null, callback.newUrl); + } + + if (callback && callback.valid) { return setupCheckLoginIframe().success(function() { - window.history.replaceState({}, null, callback.newUrl); processCallback(callback, initPromise); }).error(function (e) { initPromise.setError(); @@ -861,20 +864,97 @@ } function parseCallback(url) { - var oauth = new CallbackParser(url, kc.responseMode).parseUri(); + var oauth = parseCallbackUrl(url); + if (!oauth) { + return; + } + var oauthState = callbackStorage.get(oauth.state); - if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) { + if (oauthState) { + oauth.valid = true; oauth.redirectUri = oauthState.redirectUri; oauth.storedNonce = oauthState.nonce; oauth.prompt = oauthState.prompt; - - if (oauth.fragment) { - oauth.newUrl += '#' + oauth.fragment; - } - - return oauth; } + + return oauth; + } + + function parseCallbackUrl(url) { + var supportedParams; + switch (kc.flow) { + case 'standard': + supportedParams = ['code', 'state', 'session_state']; + break; + case 'implicit': + supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'not-before-policy']; + break; + case 'hybrid': + supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'not-before-policy']; + break; + } + + supportedParams.push('error'); + supportedParams.push('error_description'); + supportedParams.push('error_uri'); + + var queryIndex = url.indexOf('?'); + var fragmentIndex = url.indexOf('#'); + + var newUrl; + var parsed; + + if (kc.responseMode === 'query' && queryIndex !== -1) { + newUrl = url.substring(0, queryIndex); + parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams); + if (parsed.paramsString !== '') { + newUrl += '?' + parsed.paramsString; + } + if (fragmentIndex !== -1) { + newUrl += url.substring(fragmentIndex); + } + } else if (kc.responseMode === 'fragment' && fragmentIndex !== -1) { + newUrl = url.substring(0, fragmentIndex); + parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams); + if (parsed.paramsString !== '') { + newUrl += '#' + parsed.paramsString; + } + } + + if (parsed && parsed.oauthParams) { + if (kc.flow === 'standard' || kc.flow === 'hybrid') { + if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) { + parsed.oauthParams.newUrl = newUrl; + return parsed.oauthParams; + } + } else if (kc.flow === 'implicit') { + if ((parsed.oauthParams.access_token || parsed.oauthParams.error) && parsed.oauthParams.state) { + parsed.oauthParams.newUrl = newUrl; + return parsed.oauthParams; + } + } + } + } + + function parseCallbackParams(paramsString, supportedParams) { + var p = paramsString.split('&'); + var result = { + paramsString: '', + oauthParams: {} + } + for (var i = 0; i < p.length; i++) { + var t = p[i].split('='); + if (supportedParams.indexOf(t[0]) !== -1) { + result.oauthParams[t[0]] = t[1]; + } else { + if (result.paramsString !== '') { + result.paramsString += '&'; + } + result.paramsString += p[i]; + } + } + return result; } function createPromise() { @@ -1081,12 +1161,7 @@ } else if (kc.redirectUri) { return kc.redirectUri; } else { - var redirectUri = location.href; - if (location.hash && encodeHash) { - redirectUri = redirectUri.substring(0, location.href.indexOf('#')); - redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1)); - } - return redirectUri; + return location.href; } } }; @@ -1327,99 +1402,6 @@ return new CookieStorage(); } - - var CallbackParser = function(uriToParse, responseMode) { - if (!(this instanceof CallbackParser)) { - return new CallbackParser(uriToParse, responseMode); - } - var parser = this; - - var initialParse = function() { - var baseUri = null; - var queryString = null; - var fragmentString = null; - - var questionMarkIndex = uriToParse.indexOf("?"); - var fragmentIndex = uriToParse.indexOf("#", questionMarkIndex + 1); - if (questionMarkIndex == -1 && fragmentIndex == -1) { - baseUri = uriToParse; - } else if (questionMarkIndex != -1) { - baseUri = uriToParse.substring(0, questionMarkIndex); - queryString = uriToParse.substring(questionMarkIndex + 1); - if (fragmentIndex != -1) { - fragmentIndex = queryString.indexOf("#"); - fragmentString = queryString.substring(fragmentIndex + 1); - queryString = queryString.substring(0, fragmentIndex); - } - } else { - baseUri = uriToParse.substring(0, fragmentIndex); - fragmentString = uriToParse.substring(fragmentIndex + 1); - } - - return { baseUri: baseUri, queryString: queryString, fragmentString: fragmentString }; - } - - var parseParams = function(paramString) { - var result = {}; - var params = paramString.split('&'); - for (var i = 0; i < params.length; i++) { - var p = params[i].split('='); - var paramName = decodeURIComponent(p[0]); - var paramValue = decodeURIComponent(p[1]); - result[paramName] = paramValue; - } - return result; - } - - var handleQueryParam = function(paramName, paramValue, oauth) { - var supportedOAuthParams = [ 'code', 'state', 'error', 'error_description' ]; - - for (var i = 0 ; i< supportedOAuthParams.length ; i++) { - if (paramName === supportedOAuthParams[i]) { - oauth[paramName] = paramValue; - return true; - } - } - return false; - } - - - parser.parseUri = function() { - var parsedUri = initialParse(); - - var queryParams = {}; - if (parsedUri.queryString) { - queryParams = parseParams(parsedUri.queryString); - } - - var oauth = { newUrl: parsedUri.baseUri }; - for (var param in queryParams) { - switch (param) { - case 'redirect_fragment': - oauth.fragment = queryParams[param]; - break; - default: - if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) { - oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + encodeURIComponent(queryParams[param]); - } - break; - } - } - - if (responseMode === 'fragment') { - var fragmentParams = {}; - if (parsedUri.fragmentString) { - fragmentParams = parseParams(parsedUri.fragmentString); - } - for (var param in fragmentParams) { - oauth[param] = fragmentParams[param]; - } - } - - return oauth; - } - } - } if ( typeof module === "object" && module && typeof module.exports === "object" ) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java index 9027ed5367d..51219897f15 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java @@ -87,6 +87,11 @@ public abstract class OIDCRedirectUriBuilder { protected FragmentRedirectUriBuilder(KeycloakUriBuilder uriBuilder) { super(uriBuilder); + + String fragment = uriBuilder.getFragment(); + if (fragment != null) { + this.fragment = new StringBuilder(fragment); + } } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 732ba0fba5a..6ea17c683a4 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -910,6 +910,11 @@ public class OAuthClient { } catch (IllegalArgumentException iae) { fragment = false; } + + if ("fragment".equals(client.responseMode)) { + fragment = true; + } + init (client, fragment); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java index 6f4e394a55c..efa238c3415 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java @@ -131,6 +131,11 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest { .secret("password"); realm.client(installedApp7); + ClientBuilder installedApp8 = ClientBuilder.create().id("test-fragment").name("test-fragment") + .redirectUris("http://localhost/*") + .secret("password"); + realm.client(installedApp8); + testRealms.add(realm.build()); } @@ -227,6 +232,20 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest { Assert.assertTrue(url.getQuery().contains("code=")); } + @Test + public void testWithFragment() throws IOException { + oauth.clientId("test-fragment"); + oauth.responseMode("fragment"); + + oauth.redirectUri(APP_ROOT + "/auth#key=value"); + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + + Assert.assertNotNull(response.getCode()); + URL url = new URL(driver.getCurrentUrl()); + Assert.assertTrue(url.toString().startsWith(APP_ROOT)); + Assert.assertTrue(url.toString().contains("key=value")); + } + @Test public void testQueryComponents() throws IOException { // KEYCLOAK-3420