From 1f02f87a6e2930ce9c947db9ad5e47eebb46ca9d Mon Sep 17 00:00:00 2001 From: stianst Date: Wed, 25 Mar 2020 08:11:12 +0100 Subject: [PATCH] KEYCLOAK-13565 Add support for kc_action to keycloak.js Co-authored-by mhajas --- .../oidc/js/src/main/resources/keycloak.d.ts | 7 +++- .../oidc/js/src/main/resources/keycloak.js | 14 +++++-- .../js-console/src/main/webapp/index.html | 12 ++++++ .../javascript/JavascriptTestExecutor.java | 1 + .../javascript/AbstractJavascriptTest.java | 5 ++- .../javascript/JavascriptAdapterTest.java | 37 +++++++++++++++++++ 6 files changed, 70 insertions(+), 6 deletions(-) diff --git a/adapters/oidc/js/src/main/resources/keycloak.d.ts b/adapters/oidc/js/src/main/resources/keycloak.d.ts index b7495f649e..b666eb8120 100644 --- a/adapters/oidc/js/src/main/resources/keycloak.d.ts +++ b/adapters/oidc/js/src/main/resources/keycloak.d.ts @@ -173,7 +173,7 @@ declare namespace Keycloak { * If value is `'register'` then user is redirected to registration page, * otherwise to login page. */ - action?: 'register'; + action?: string; /** * Used just if user is already authenticated. Specifies maximum time since @@ -433,6 +433,11 @@ declare namespace Keycloak { */ onTokenExpired?(): void; + /** + * Called when a AIA has been requested by the application. + */ + onActionUpdate?(status: 'success'|'cancelled'|'error'): void; + /** * Called to initialize the adapter. * @param initOptions Initialization options. diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js index 0f179cb176..0a85ed2de0 100755 --- a/adapters/oidc/js/src/main/resources/keycloak.js +++ b/adapters/oidc/js/src/main/resources/keycloak.js @@ -476,6 +476,10 @@ url += '&kc_idp_hint=' + encodeURIComponent(options.idpHint); } + if (options && options.action && options.action != 'register') { + url += '&kc_action=' + encodeURIComponent(options.action); + } + if (options && options.locale) { url += '&ui_locales=' + encodeURIComponent(options.locale); } @@ -740,6 +744,10 @@ var timeLocal = new Date().getTime(); + if (oauth['kc_action_status']) { + kc.onActionUpdate && kc.onActionUpdate(oauth['kc_action_status']); + } + if (error) { if (prompt != 'none') { var errorData = { error: error, error_description: oauth.error_description }; @@ -1085,13 +1093,13 @@ var supportedParams; switch (kc.flow) { case 'standard': - supportedParams = ['code', 'state', 'session_state']; + supportedParams = ['code', 'state', 'session_state', 'kc_action_status']; break; case 'implicit': - supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in']; + supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status']; break; case 'hybrid': - supportedParams = ['access_token', 'id_token', 'code', 'state', 'session_state']; + supportedParams = ['access_token', 'id_token', 'code', 'state', 'session_state', 'kc_action_status']; break; } diff --git a/examples/js-console/src/main/webapp/index.html b/examples/js-console/src/main/webapp/index.html index 3cb65e01d4..d35b85fcb0 100644 --- a/examples/js-console/src/main/webapp/index.html +++ b/examples/js-console/src/main/webapp/index.html @@ -23,6 +23,7 @@
+ @@ -155,6 +156,17 @@ event('Access token expired.'); }; + keycloak.onActionUpdate = function (status) { + switch (status) { + case 'success': + event('Action completed successfully'); break; + case 'cancelled': + event('Action cancelled by user'); break; + case 'error': + event('Action failed'); break; + } + }; + // Flow can be changed to 'implicit' or 'hybrid', but then client must enable implicit flow in admin console too var initOptions = { responseMode: 'fragment', diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java index 2dff60cf38..e64739d472 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/javascript/JavascriptTestExecutor.java @@ -114,6 +114,7 @@ public class JavascriptTestExecutor { jsExecutor.executeScript("window.keycloak.onAuthRefreshError = function () {event('Auth Refresh Error')}"); jsExecutor.executeScript("window.keycloak.onAuthLogout = function () {event('Auth Logout')}"); jsExecutor.executeScript("window.keycloak.onTokenExpired = function () {event('Access token expired.')}"); + jsExecutor.executeScript("window.keycloak.onActionUpdate = function (status) {event('AIA status: ' + status)}"); configured = true; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/AbstractJavascriptTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/AbstractJavascriptTest.java index 6ae2ab7de2..7778a33b5a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/AbstractJavascriptTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/AbstractJavascriptTest.java @@ -55,6 +55,7 @@ public abstract class AbstractJavascriptTest extends AbstractAuthTest { public static final String JAVASCRIPT_ENCODED_SPACE_URL = "/auth/realms/Example%20realm/testing/javascript"; public static final String JAVASCRIPT_SPACE_URL = "/auth/realms/Example realm/testing/javascript"; public static int TOKEN_LIFESPAN_LEEWAY = 3; // seconds + public static final String USER_PASSWORD = "password"; protected JavascriptExecutor jsExecutor; @@ -80,8 +81,8 @@ public abstract class AbstractJavascriptTest extends AbstractAuthTest { public static final UserRepresentation unauthorizedUser; static { - testUser = UserBuilder.create().username("test-user@localhost").password("password").build(); - unauthorizedUser = UserBuilder.create().username("unauthorized").password("password").build(); + testUser = UserBuilder.create().username("test-user@localhost").password(USER_PASSWORD).build(); + unauthorizedUser = UserBuilder.create().username("unauthorized").password(USER_PASSWORD).build(); } @BeforeClass diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java index 4d5ebfe38c..16c3f0e62c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/javascript/JavascriptAdapterTest.java @@ -20,6 +20,7 @@ import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.auth.page.account.Applications; import org.keycloak.testsuite.auth.page.login.OAuthGrant; +import org.keycloak.testsuite.auth.page.login.UpdatePassword; import org.keycloak.testsuite.util.JavascriptBrowser; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; @@ -77,6 +78,10 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest { @JavascriptBrowser private OAuthGrant oAuthGrantPage; + @Page + @JavascriptBrowser + private UpdatePassword updatePasswordPage; + @Override protected RealmRepresentation updateRealm(RealmBuilder builder) { return builder.accessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY).build(); @@ -660,4 +665,36 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest { .init(defaultArguments(), this::assertSuccessfullyLoggedIn) .executeAsyncScript(refreshWithDeprecatedHandles, assertOutputContains("Success handle")); } + + @Test + public void testAIAFromJavascriptAdapterSuccess() { + testExecutor.init(defaultArguments(), this::assertInitNotAuth) + .login(JSObjectBuilder.create() + .add("action", "UPDATE_PASSWORD") + .build(), this::assertOnLoginPage) + .loginForm(testUser); + + updatePasswordPage.updatePasswords(USER_PASSWORD, USER_PASSWORD); + + testExecutor.init(defaultArguments(), (driver1, output, events1) -> { + assertSuccessfullyLoggedIn(driver1, output, events1); + waitUntilElement(events1).text().contains("AIA status: success"); + }); + } + + @Test + public void testAIAFromJavascriptAdapterCancelled() { + testExecutor.init(defaultArguments(), this::assertInitNotAuth) + .login(JSObjectBuilder.create() + .add("action", "UPDATE_PASSWORD") + .build(), this::assertOnLoginPage) + .loginForm(testUser); + + updatePasswordPage.cancel(); + + testExecutor.init(defaultArguments(), (driver1, output, events1) -> { + assertSuccessfullyLoggedIn(driver1, output, events1); + waitUntilElement(events1).text().contains("AIA status: cancelled"); + }); + } }