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");
+ });
+ }
}