From 7bea4af6c990022bd070f3ca6bb33216f2f3a756 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 14 May 2014 13:37:11 +0100 Subject: [PATCH 1/2] Redirect to login page if logged out and submitting forms in acct mngmt --- .../services/resources/AccountService.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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(); From 643daadf606cc25242fd2a9c072463c435668286 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 14 May 2014 17:46:19 +0100 Subject: [PATCH 2/2] Updated session status iframe for JavaScript adapter to be created automatically, and to invoke onAuthLogout callback if user is logged out --- .../en/en-US/modules/javascript-adapter.xml | 150 +++++++++- .../src/main/webapp/customers/view.html | 28 +- examples/js-console/example-realm.json | 7 +- .../js-console/src/main/webapp/index.html | 6 +- .../js-console/src/main/webapp/keycloak.json | 2 +- .../theme/admin/base/resources/js/app.js | 18 +- integration/js/src/main/resources/keycloak.js | 274 ++++++++++-------- 7 files changed, 315 insertions(+), 170 deletions(-) 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 {