diff --git a/docbook/reference/en/en-US/modules/javascript-adapter.xml b/docbook/reference/en/en-US/modules/javascript-adapter.xml
index 8e0a5c70d1..fa01819af4 100755
--- a/docbook/reference/en/en-US/modules/javascript-adapter.xml
+++ b/docbook/reference/en/en-US/modules/javascript-adapter.xml
@@ -50,7 +50,7 @@ var keycloak = Keycloak({
required will redirect to the login form on the server, while check-sso will redirect to the auth server to check
if the user is already logged in to the realm. For example:
@@ -125,6 +125,17 @@ keycloak.updateToken(30).success(function() {
]]>
+
+ Session status iframe
+
+
+ By default the JavaScript adapter creates a non-visible iframe that is used to detect if a single-sign out has occured.
+ This does not require any network traffic, instead the status is retrieved from a special status cookie. This feature can be disabled
+ by setting checkLoginIframe: false in the options passed to the init
+ method.
+
+
+
JavaScript Adapter reference
@@ -156,20 +167,127 @@ new Keycloak({ url: 'http://localhost/auth', realm: 'myrealm', clientId: 'myApp'
Methods
-
- init - called to initialize the adapter. Returns promise to set functions to be invoked on success or error
- login(options) - redirects to login form on (options is an optional object with redirectUri and/or prompt fields)
- createLoginUrl(options) - returns the url to login form on (options is an optional object with redirectUri and/or prompt fields)
- logout(options) - redirects to logout (options is an optional object with redirectUri)
- createLogoutUrl(options) - returns the url to logout (options is an optional object with redirectUri)
- accountManagement - redirects to account management
- createAccountUrl - returns the url to account management
- hasRealmRole(role) - returns true if the token has the given realm role
- hasResourceRole(role, resource) - returns true if the token has the given role for the resource (resource is optional, if not specified clientId is used)
- loadUserProfile() - loads the users profile. Returns promise to set functions to be invoked on success or error
- isTokenExpired(minValidity) - returns true if the token has less than minValidity seconds left before it expires (minValidity is optional, if not specified 0 is used)
- updateToken(minValidity) - refreshes the token if the token expires within minValidity seconds (minValidity is optional, if not specified 0 is used). Returns promise to set functions to be invoked on success or error
-
+
+ init(options)
+
+ Called to initialize the adapter.
+ Options is an Object, where:
+
+ onLoad - specifies an action to do on load, can be either 'login-required' or 'check-sso'
+ token - set an initial value for the token
+ refreshToken - set an initial value for the refresh token
+ checkLoginIframe - set to enable/disable monitoring login state (default is true)
+ checkLoginIframeInterval - set the interval to check login state (default is 5 seconds)
+
+
+ Returns promise to set functions to be invoked on success or error.
+
+
+
+ login(options)
+
+ Redirects to login form on (options is an optional object with redirectUri and/or prompt fields)
+ Options is an Object, where:
+
+ redirectUri - specifies the uri to redirect to after login
+ prompt - can be set to 'none' to check if the user is logged in already (if not logged in a login form is not displayed)
+
+
+
+
+ createLoginUrl(options)
+
+ Returns the url to login form on (options is an optional object with redirectUri and/or prompt fields)
+ Options is an Object, where:
+
+ redirectUri - specifies the uri to redirect to after login
+ prompt - can be set to 'none' to check if the user is logged in already (if not logged in a login form is not displayed)
+
+
+
+
+
+ logout(options)
+
+ Redirects to logout
+ Options is an Object, where:
+
+ redirectUri - specifies the uri to redirect to after logout
+
+
+
+
+
+ createLogoutUrl(options)
+
+ Returns logout out
+ Options is an Object, where:
+
+ redirectUri - specifies the uri to redirect to after logout
+
+
+
+
+
+ accountManagement()
+
+ Redirects to account management
+
+
+
+ createAccountUrl()
+
+ Returns the url to account management
+
+
+
+ hasRealmRole(role)
+
+ Returns true if the token has the given realm role
+
+
+
+ hasResourceRole(role, resource)
+
+ Returns true if the token has the given role for the resource (resource is optional, if not specified clientId is used)
+
+
+
+ loadUserProfile()
+
+ Loads the users profile
+
+ Returns promise to set functions to be invoked on success or error.
+
+
+
+ isTokenExpired(minValidity)
+
+ Returns true if the token has less than minValidity seconds left before it expires (minValidity is optional, if not specified 0 is used)
+
+
+
+ updateToken(minValidity)
+
+ If the token expires within minValidity seconds (minValidity is optional, if not specified 0 is used) the token is refreshed.
+ If the session status iframe is enabled, the session status is also checked.
+
+
+ Returns promise to set functions that can be invoked if the token is still valid, or if the token is no longer valid. For example:
+
+
+
+
@@ -187,7 +305,7 @@ keycloak.onAuthSuccess = function() { alert('authenticated'); }
onAuthError - called if there was an error during authentication
onAuthRefreshSuccess - called when the token is refreshed
onAuthRefreshError - called if there was an error while trying to refresh the token
- onAuthLogout - called when the user is logged out (only relevant to Cordova)
+ onAuthLogout - called if the user is logged out (will only be called if the session status iframe is enabled, or in Cordova mode)
diff --git a/examples/demo-template/customer-app-js/src/main/webapp/customers/view.html b/examples/demo-template/customer-app-js/src/main/webapp/customers/view.html
index ae7dcc58e8..9c0dcb65a0 100755
--- a/examples/demo-template/customer-app-js/src/main/webapp/customers/view.html
+++ b/examples/demo-template/customer-app-js/src/main/webapp/customers/view.html
@@ -73,32 +73,14 @@ User made this request.
};
var reloadData = function () {
- keycloak.checkLoginIframe(
- function() {
- keycloak.updateToken(10).success(loadData).error(loadFailure);
- },
- function() {
+ keycloak.updateToken(10)
+ .success(loadData)
+ .error(function() {
document.getElementById('customers').innerHTML = 'Failed to load data. User is logged out.';
- //window.location.reload();
- }
- );
+ });
}
- //
- // NOTE!!!: keycloak.setupCheckLoginIframe and checkLoginIframe are an optional way
- // of discovering if you are logged out. This approach may not always be feasible, but it is a lightweight
- // approach that requires no network call to determine if the user is logged in. The downside is that it
- // requires a callback.
- //
- // Alternatively, on a single log out, the access token will eventually time out, then refresh token will fail
- // as the session will be logged out on the auth server.
- //
- //
-
- keycloak.init('login-required').success(function() {
- // Use an iframe to detect if the user has done an SSO logout in a different window.
- keycloak.setupCheckLoginIframe(document, window);
- });
+ keycloak.init({ onLoad: 'login-required' }).success(reloadData);
diff --git a/examples/js-console/example-realm.json b/examples/js-console/example-realm.json
index 537d120392..7cb5485af6 100755
--- a/examples/js-console/example-realm.json
+++ b/examples/js-console/example-realm.json
@@ -48,9 +48,12 @@
"name": "js-console",
"enabled": true,
"publicClient": true,
- "baseUrl": "http://localhost:8080/js-console",
+ "baseUrl": "http://localhost/js-console",
"redirectUris": [
- "http://localhost:8080/js-console/*"
+ "http://localhost/js-console/*"
+ ],
+ "webOrigins": [
+ "http://localhost"
]
}
],
diff --git a/examples/js-console/src/main/webapp/index.html b/examples/js-console/src/main/webapp/index.html
index 3b9e4a00ea..499c928637 100644
--- a/examples/js-console/src/main/webapp/index.html
+++ b/examples/js-console/src/main/webapp/index.html
@@ -1,6 +1,6 @@
-
+
@@ -86,6 +86,10 @@
event('Auth Refresh Error');
};
+ keycloak.onAuthLogout = function () {
+ event('Auth Logout');
+ };
+
keycloak.init().success(function(authenticated) {
output('Init Success (' + (authenticated ? 'Authenticated' : 'Not Authenticated') + ')');
}).error(function() {
diff --git a/examples/js-console/src/main/webapp/keycloak.json b/examples/js-console/src/main/webapp/keycloak.json
index f166f87106..66434b9bf0 100644
--- a/examples/js-console/src/main/webapp/keycloak.json
+++ b/examples/js-console/src/main/webapp/keycloak.json
@@ -1,7 +1,7 @@
{
"realm" : "example",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
- "auth-server-url" : "http://localhost:8080/auth",
+ "auth-server-url" : "http://localhost:8081/auth",
"ssl-not-required" : true,
"resource" : "js-console",
"public-client" : true
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
index 33c204803f..74d3a5e899 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
@@ -9,9 +9,6 @@ var logoutUrl = consoleBaseUrl + "/logout";
var auth = {};
var logout = function(){
console.log('*** LOGOUT');
- auth.loggedIn = false;
- auth.authz = null;
- auth.user = null;
window.location = logoutUrl;
};
@@ -28,7 +25,11 @@ angular.element(document).ready(function ($http) {
var keycloakAuth = new Keycloak(configUrl);
auth.loggedIn = false;
- keycloakAuth.init('login-required').success(function () {
+ keycloakAuth.onAuthLogout = function() {
+ location.reload();
+ }
+
+ keycloakAuth.init({ onLoad: 'login-required' }).success(function () {
auth.loggedIn = true;
auth.authz = keycloakAuth;
module.factory('Auth', function() {
@@ -36,9 +37,8 @@ angular.element(document).ready(function ($http) {
});
angular.bootstrap(document, ["keycloak"]);
}).error(function () {
- window.location.reload();
- });
-
+ window.location.reload();
+ });
});
module.factory('authInterceptor', function($q, Auth) {
@@ -52,8 +52,8 @@ module.factory('authInterceptor', function($q, Auth) {
deferred.resolve(config);
}).error(function() {
- deferred.reject('Failed to refresh token');
- });
+ location.reload();
+ });
}
return deferred.promise;
}
diff --git a/integration/js/src/main/resources/keycloak.js b/integration/js/src/main/resources/keycloak.js
index 6c73ad5bd3..046c9c1b95 100755
--- a/integration/js/src/main/resources/keycloak.js
+++ b/integration/js/src/main/resources/keycloak.js
@@ -6,11 +6,13 @@ var Keycloak = function (config) {
var kc = this;
var adapter;
- kc.callbackMap = new Object();
+ var loginIframe = {
+ enable: true,
+ callbackMap: [],
+ interval: 5
+ };
-
-
- kc.init = function (init) {
+ kc.init = function (initOptions) {
kc.authenticated = false;
if (window.Cordova) {
@@ -19,6 +21,16 @@ var Keycloak = function (config) {
adapter = loadAdapter();
}
+ if (initOptions) {
+ if (typeof initOptions.checkLoginIframe !== 'undefined') {
+ loginIframe.enable = initOptions.checkLoginIframe;
+ }
+
+ if (initOptions.checkLoginIframeInterval) {
+ loginIframe.interval = initOptions.checkLoginIframeInterval;
+ }
+ }
+
var promise = createPromise();
var initPromise = createPromise();
@@ -33,37 +45,31 @@ var Keycloak = function (config) {
function processInit() {
var callback = parseCallback(window.location.href);
+
if (callback) {
window.history.replaceState({}, null, location.protocol + '//' + location.host + location.pathname + (callback.fragment ? '#' + callback.fragment : ''));
processCallback(callback, initPromise);
return;
- } else if (init) {
- if (init.code || init.error) {
- processCallback(init, initPromise);
- return;
- } else if (init.token || init.refreshToken) {
- setToken(init.token, init.refreshToken);
- initPromise.setSuccess();
- } else if (init == 'login-required') {
- var p = kc.login();
- if (p) {
- p.success(function() {
- initPromise.setSuccess();
- }).error(function() {
- initPromise.setError();
- });
- };
- } else if (init == 'check-sso') {
- var p = kc.login({ prompt: 'none' });
- if (p) {
- p.success(function() {
- initPromise.setSuccess();
- }).error(function() {
- initPromise.setSuccess();
- });
- };
- } else {
- throw 'invalid init: ' + init;
+ } else if (initOptions) {
+ if (initOptions.token || initOptions.refreshToken) {
+ setToken(initOptions.token, initOptions.refreshToken);
+ } else if (initOptions.onLoad) {
+ if (initOptions.onLoad == 'check-sso' || initOptions.onLoad == 'login-required') {
+ var options = {};
+ if (initOptions.onLoad == 'check-sso') {
+ options.prompt = 'none';
+ }
+ var p = kc.login(options);
+ if (p) {
+ p.success(function() {
+ initPromise.setSuccess();
+ }).error(function() {
+ initPromise.setError();
+ });
+ };
+ } else {
+ throw 'Invalid value for onLoad';
+ }
}
} else {
initPromise.setSuccess();
@@ -82,59 +88,6 @@ var Keycloak = function (config) {
return adapter.login(options);
}
- kc.setupCheckLoginIframe = function(doc, win) {
- kc.iframe = doc.createElement('iframe');
- var src = getRealmUrl() + "/login-status-iframe.html?client_id=" + encodeURIComponent(kc.clientId);
- console.log('iframe src='+ src);
- kc.iframe.setAttribute('src', src );
- kc.iframe.style.display = "none";
- doc.body.appendChild(kc.iframe);
- if (!kc.iframe.contentWindow.location.origin) {
- kc.iframe.contentWindow.location.origin = kc.iframe.contentWindow.location.protocol + "//" + kc.iframe.contentWindow.location.host;
- }
-
- var messageCallback = function(event) {
- if (event.origin !== kc.iframe.contentWindow.location.origin) {
- return;
- }
- var data = event.data;
- var success = kc.callbackMap[data.successId];
- var failure = kc.callbackMap[data.failureId];
- delete kc.callbackMap[data.successId];
- delete kc.callbackMap[data.failureId];
- if (kc.sessionId != data.session) {
- console.log("session doesn't match received session: " + event.data.session);
- console.log("forcing loggedIn to be false");
- failure();
- }
- if (data.loggedIn) {
- success();
- } else {
- failure();
- }
- };
- win.addEventListener("message", messageCallback, false);
- }
-
- kc.checkLoginIframe = function(success, failure) {
-
- var msg = {};
- if (!success) {
- throw "You must define a success method";
- }
- if (!failure) {
- throw "You must define a failure method";
- }
- msg.successId = createCallbackId();
- msg.failureId = createCallbackId();
- kc.callbackMap[msg.successId] = success;
- kc.callbackMap[msg.failureId] = failure;
- var origin = kc.iframe.contentWindow.location.origin;
- console.log('*** origin: ' + origin);
- var iframe = kc.iframe;
- iframe.contentWindow.postMessage(msg, origin);
- }
-
kc.createLoginUrl = function(options) {
var state = createUUID();
@@ -241,47 +194,64 @@ var Keycloak = function (config) {
}
kc.updateToken = function(minValidity) {
- if (!kc.tokenParsed || !kc.refreshToken) {
- throw 'Not authenticated';
- }
-
var promise = createPromise();
- if (minValidity) {
+ if (!kc.tokenParsed || !kc.refreshToken) {
+ promise.setError();
+ return promise.promise;
+ }
+
+ minValidity = minValidity || 5;
+
+ var exec = function() {
if (!kc.isTokenExpired(minValidity)) {
promise.setSuccess(false);
- return promise.promise;
- }
- }
+ } else {
+ var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
+ var url = getRealmUrl() + '/tokens/refresh';
- var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
- var url = getRealmUrl() + '/tokens/refresh';
+ var req = new XMLHttpRequest();
+ req.open('POST', url, true);
+ req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
- var req = new XMLHttpRequest();
- req.open('POST', url, true);
- req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
-
- if (kc.clientId && kc.clientSecret) {
- req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret));
- } else {
- params += '&client_id=' + encodeURIComponent(kc.clientId);
- }
-
- req.onreadystatechange = function() {
- if (req.readyState == 4) {
- if (req.status == 200) {
- var tokenResponse = JSON.parse(req.responseText);
- setToken(tokenResponse['access_token'], tokenResponse['refresh_token']);
- kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess();
- promise.setSuccess(true);
+ if (kc.clientId && kc.clientSecret) {
+ req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret));
} else {
- kc.onAuthRefreshError && kc.onAuthRefreshError();
- promise.setError();
+ params += '&client_id=' + encodeURIComponent(kc.clientId);
}
- }
- };
- req.send(params);
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ if (req.status == 200) {
+ var tokenResponse = JSON.parse(req.responseText);
+ setToken(tokenResponse['access_token'], tokenResponse['refresh_token']);
+ kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess();
+ promise.setSuccess(true);
+ } else {
+ kc.onAuthRefreshError && kc.onAuthRefreshError();
+ promise.setError();
+ }
+ }
+ };
+
+ req.send(params);
+ }
+ }
+
+ if (loginIframe.enable) {
+ if (loginIframe.enable) {
+ var iframePromise = checkLoginIframe();
+ iframePromise.success(function() {
+ exec();
+ }).error(function() {
+ promise.setError();
+ })
+ } else {
+ promise.setSuccess(false);
+ }
+ } else {
+ exec();
+ }
return promise.promise;
}
@@ -398,11 +368,17 @@ var Keycloak = function (config) {
}
function clearToken() {
- setToken(null, null);
- kc.onAuthLogout && kc.onAuthLogout();
+ if (kc.token) {
+ setToken(null, null);
+ kc.onAuthLogout && kc.onAuthLogout();
+ }
}
function setToken(token, refreshToken) {
+ if (token || refreshToken) {
+ setupCheckLoginIframe();
+ }
+
if (token) {
kc.token = token;
kc.tokenParsed = JSON.parse(decodeURIComponent(escape(window.atob( token.split('.')[1] ))));
@@ -539,6 +515,68 @@ var Keycloak = function (config) {
return p;
}
+ function setupCheckLoginIframe() {
+ if (!loginIframe.enable || loginIframe.iframe) {
+ return;
+ }
+
+ var iframe = document.createElement('iframe');
+
+ iframe.onload = function() {
+ var realmUrl = getRealmUrl();
+ loginIframe.iframeOrigin = realmUrl.substring(0, realmUrl.indexOf('/', 8));
+ loginIframe.iframe = iframe;
+ }
+
+ var src = getRealmUrl() + '/login-status-iframe.html?client_id=' + encodeURIComponent(kc.clientId);
+ console.log('iframe src='+ src);
+ iframe.setAttribute('src', src );
+ iframe.style.display = 'none';
+ document.body.appendChild(iframe);
+
+ var messageCallback = function(event) {
+ if (event.origin !== loginIframe.iframeOrigin) {
+ return;
+ }
+ var data = event.data;
+ var promise = loginIframe.callbackMap[data.callbackId];
+ delete loginIframe.callbackMap[data.callbackId];
+ if (kc.sessionId == data.session && data.loggedIn) {
+ promise.setSuccess();
+ } else {
+ clearToken();
+ promise.setError();
+ }
+ };
+ window.addEventListener('message', messageCallback, false);
+
+ var check = function() {
+ checkLoginIframe();
+ if (kc.token) {
+ setTimeout(check, loginIframe.interval * 1000);
+ }
+ };
+
+ setTimeout(check, loginIframe.interval * 1000);
+ }
+
+ function checkLoginIframe() {
+ var promise = createPromise();
+
+ if (loginIframe.iframe || loginIframe.iframeOrigin) {
+ var msg = {};
+ msg.callbackId = createCallbackId();
+ loginIframe.callbackMap[msg.callbackId] = promise;
+ var origin = loginIframe.iframeOrigin;
+ console.log('*** origin: ' + origin);
+ loginIframe.iframe.contentWindow.postMessage(msg, origin);
+ } else {
+ promise.setSuccess();
+ }
+
+ return promise.promise;
+ }
+
function loadAdapter(type) {
if (!type || type == 'default') {
return {
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 35c70f67e2..980ce585a5 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -278,6 +278,10 @@ public class AccountService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processAccountUpdate(final MultivaluedMap formData) {
+ if (auth == null) {
+ return login(null);
+ }
+
require(AccountRoles.MANAGE_ACCOUNT);
UserModel user = auth.getUser();
@@ -309,6 +313,10 @@ public class AccountService {
@Path("totp-remove")
@GET
public Response processTotpRemove() {
+ if (auth == null) {
+ return login("totp");
+ }
+
require(AccountRoles.MANAGE_ACCOUNT);
UserModel user = auth.getUser();
@@ -323,6 +331,10 @@ public class AccountService {
@Path("sessions-logout")
@GET
public Response processSessionsLogout() {
+ if (auth == null) {
+ return login("sessions");
+ }
+
require(AccountRoles.MANAGE_ACCOUNT);
UserModel user = auth.getUser();
@@ -335,6 +347,10 @@ public class AccountService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processTotpUpdate(final MultivaluedMap formData) {
+ if (auth == null) {
+ return login("totp");
+ }
+
require(AccountRoles.MANAGE_ACCOUNT);
UserModel user = auth.getUser();
@@ -364,6 +380,10 @@ public class AccountService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processPasswordUpdate(final MultivaluedMap formData) {
+ if (auth == null) {
+ return login("password");
+ }
+
require(AccountRoles.MANAGE_ACCOUNT);
UserModel user = auth.getUser();
@@ -403,6 +423,10 @@ public class AccountService {
@GET
public Response processSocialUpdate(@QueryParam("action") String action,
@QueryParam("provider_id") String providerId) {
+ if (auth == null) {
+ return login("social");
+ }
+
require(AccountRoles.MANAGE_ACCOUNT);
UserModel user = auth.getUser();