diff --git a/examples/cordova/.gitignore b/examples/cordova/.gitignore
new file mode 100644
index 0000000000..758f6ed74b
--- /dev/null
+++ b/examples/cordova/.gitignore
@@ -0,0 +1,4 @@
+platforms
+plugins
+www/keycloak.js
+www/keycloak.json
diff --git a/examples/cordova/README.md b/examples/cordova/README.md
new file mode 100644
index 0000000000..9173783f75
--- /dev/null
+++ b/examples/cordova/README.md
@@ -0,0 +1,31 @@
+Basic Cordova Example
+=====================
+
+Before running this example you need to have Cordova installed with a phone or emulator available.
+
+Start and configure Keycloak
+----------------------------
+
+Start Keycloak bound to an IP address available to the phone or emulator. For example:
+
+ bin/standalone.sh -b 192.168.0.10
+
+Open the Keycloak admin console, click on Add Realm, click on 'Choose a JSON file', selct example-realm.json and click Upload.
+
+Navigate to applications, click on 'Cordova', select 'Installation' and in the 'Format option' drop-down select 'keycloak.json'. Download this file to the www folder.
+
+Download '/js/keycloak.js' from the server to the www folder as well. For example:
+
+ wget http://192.168.0.10:8080/auth/js/keycloak.js
+
+
+Install to Android phone or emulator
+------------------------------------
+
+ mkdir platforms plugins
+ cordova plugin add org.apache.cordova.inappbrowser
+ cordova platform add android
+ cordova run android
+
+
+Once the application is opened you can login with username: 'user', and password: 'password'.
diff --git a/examples/cordova/example-realm.json b/examples/cordova/example-realm.json
new file mode 100755
index 0000000000..37e899e2a2
--- /dev/null
+++ b/examples/cordova/example-realm.json
@@ -0,0 +1,72 @@
+{
+ "realm": "example",
+ "enabled": true,
+ "sslNotRequired": true,
+ "registrationAllowed": true,
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [ "password" ],
+ "users" : [
+ {
+ "username" : "user",
+ "enabled": true,
+ "email" : "sample-user@example",
+ "firstName": "Sample",
+ "lastName": "User",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ]
+ }
+ ],
+ "roles" : {
+ "realm" : [
+ {
+ "name": "user",
+ "description": "User privileges"
+ },
+ {
+ "name": "admin",
+ "description": "Administrator privileges"
+ }
+ ]
+ },
+ "roleMappings": [
+ {
+ "username": "user",
+ "roles": ["user"]
+ }
+ ],
+ "scopeMappings": [
+ {
+ "client": "cordova",
+ "roles": ["user"]
+ }
+ ],
+ "applications": [
+ {
+ "name": "cordova",
+ "enabled": true,
+ "publicClient": true,
+ "redirectUris": [
+ "http://localhost"
+ ]
+ }
+ ],
+ "applicationRoleMappings": {
+ "account": [
+ {
+ "username": "user",
+ "roles": ["view-profile", "manage-account"]
+ }
+ ]
+ },
+ "applicationScopeMappings": {
+ "account": [
+ {
+ "client": "cordova",
+ "roles": ["view-profile"]
+ }
+ ]
+ }
+}
diff --git a/examples/cordova/www/config.xml b/examples/cordova/www/config.xml
new file mode 100644
index 0000000000..c568b5a258
--- /dev/null
+++ b/examples/cordova/www/config.xml
@@ -0,0 +1,14 @@
+
+
Goto: logout
+Goto: products | logout | manage acct
+ User made this request.User details (from )
Username:
@@ -18,16 +19,11 @@ User made this request. diff --git a/examples/demo-template/customer-app-js/src/main/webapp/keycloak.json b/examples/demo-template/customer-app-js/src/main/webapp/keycloak.json new file mode 100644 index 0000000000..d73332e0eb --- /dev/null +++ b/examples/demo-template/customer-app-js/src/main/webapp/keycloak.json @@ -0,0 +1,8 @@ +{ + "realm" : "demo", + "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url" : "http://localhost:8080/auth", + "ssl-not-required" : true, + "resource" : "customer-portal-js", + "public-client" : true +} \ No newline at end of file diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json index a82e9e866f..81af756756 100755 --- a/examples/demo-template/testrealm.json +++ b/examples/demo-template/testrealm.json @@ -51,6 +51,10 @@ "client": "customer-portal", "roles": ["user"] }, + { + "client": "customer-portal-js", + "roles": ["user"] + }, { "client": "product-portal", "roles": ["user"] diff --git a/examples/js-console/README.md b/examples/js-console/README.md new file mode 100644 index 0000000000..749c0f3612 --- /dev/null +++ b/examples/js-console/README.md @@ -0,0 +1,17 @@ +Basic JavaScript Example +======================== + +Start and configure Keycloak +---------------------------- + +Start Keycloak: + + bin/standalone.sh + +Open the Keycloak admin console, click on Add Realm, click on 'Choose a JSON file', selct example-realm.json and click Upload. + +Deploy the JS Console to Keycloak by running: + + mvn install jboss-as:deploy + +Open the console at http://localhost:8080/js-console and login with username: 'user', and password: 'password'. diff --git a/examples/js-console/src/main/webapp/index.html b/examples/js-console/src/main/webapp/index.html index 58513adc65..3b9e4a00ea 100644 --- a/examples/js-console/src/main/webapp/index.html +++ b/examples/js-console/src/main/webapp/index.html @@ -1,6 +1,6 @@ - + @@ -36,7 +36,7 @@ } function refreshToken(minValidity) { - keycloak.refreshAccessToken(minValidity).success(function(refreshed) { + keycloak.updateToken(minValidity).success(function(refreshed) { if (refreshed) { output(keycloak.tokenParsed); } else { diff --git a/integration/js/src/main/resources/META-INF/resources/js/keycloak.js b/integration/js/src/main/resources/META-INF/resources/js/keycloak.js index 6860b78f51..821d0e196b 100755 --- a/integration/js/src/main/resources/META-INF/resources/js/keycloak.js +++ b/integration/js/src/main/resources/META-INF/resources/js/keycloak.js @@ -4,107 +4,117 @@ var Keycloak = function (config) { } var kc = this; - kc.authenticated = false; - - var configPromise = createPromise(); - configPromise.name = 'config'; - - if (!config) { - loadConfig('keycloak.json', configPromise); - } else if (typeof config === 'string') { - loadConfig(config, configPromise); - } else { - if (!config['url']) { - var scripts = document.getElementsByTagName('script'); - for (var i = 0; i < scripts.length; i++) { - if (scripts[i].src.match(/.*keycloak\.js/)) { - config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js')); - break; - } - } - } - - if (!config.realm) { - throw 'realm missing'; - } - - if (!config.clientId) { - throw 'clientId missing'; - } - - kc.authServerUrl = config.url; - kc.realm = config.realm; - kc.clientId = config.clientId; - - configPromise.setSuccess(); - } + var adapter; kc.init = function (init) { + kc.authenticated = false; + + if (window.Cordova) { + adapter = loadAdapter('cordova'); + } else { + adapter = loadAdapter(); + } + var promise = createPromise(); - var callback = parseCallback(window.location.href); + + var initPromise = createPromise(); + initPromise.promise.success(function() { + kc.onReady && kc.onReady(kc.authenticated); + promise.setSuccess(kc.authenticated); + }).error(promise.error); + + var configPromise = loadConfig(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, promise); + processCallback(callback, initPromise); return; } else if (init) { if (init.code || init.error) { - processCallback(init, promise); + processCallback(init, initPromise); return; } else if (init.token || init.refreshToken) { setToken(init.token, init.refreshToken); + initPromise.setSuccess(); } else if (init == 'login-required') { - kc.login(); - return; + var p = kc.login(); + if (p) { + p.success(function() { + initPromise.setSuccess(); + }).error(function() { + initPromise.setError(); + }); + }; } else if (init == 'check-sso') { - window.location = kc.createLoginUrl() + '&prompt=none'; - return; + var p = kc.login({ prompt: 'none' }); + if (p) { + p.success(function() { + initPromise.setSuccess(); + }).error(function() { + initPromise.setSuccess(); + }); + }; + } else { + throw 'invalid init: ' + init; } + } else { + initPromise.setSuccess(); } - - promise.setSuccess(false); } - configPromise.promise.success(processInit); + configPromise.success(processInit); return promise.promise; } - kc.login = function (redirectUri) { - window.location.href = kc.createLoginUrl(redirectUri); + kc.login = function (options) { + return adapter.login(options); } - kc.createLoginUrl = function(redirectUri) { + kc.createLoginUrl = function(options) { var state = createUUID(); sessionStorage.oauthState = state; var url = getRealmUrl() + '/tokens/login' + '?client_id=' + encodeURIComponent(kc.clientId) - + '&redirect_uri=' + getEncodedRedirectUri(redirectUri) + + '&redirect_uri=' + encodeURIComponent(adapter.redirectUri(options)) + '&state=' + encodeURIComponent(state) + '&response_type=code'; + if (options && options.prompt) { + url += '&prompt=' + options.prompt; + } + return url; } - kc.logout = function(redirectUri) { - setToken(null, null); - window.location.href = kc.createLogoutUrl(redirectUri); + kc.logout = function(options) { + return adapter.logout(options); } - kc.clearToken = function() { - setToken(null, null); - } - - kc.createLogoutUrl = function(redirectUri) { + kc.createLogoutUrl = function(options) { var url = getRealmUrl() + '/tokens/logout' - + '?redirect_uri=' + getEncodedRedirectUri(redirectUri); + + '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options)); + return url; } + kc.createAccountUrl = function() { + var url = getRealmUrl() + + '/account' + + '?referrer=' + kc.clientId; + + return url; + } + + kc.accountManagement = function() { + return adapter.accountManagement(); + } + kc.hasRealmRole = function (role) { var access = kc.realmAccess; return access && access.roles.indexOf(role) >= 0 || false; @@ -144,7 +154,20 @@ var Keycloak = function (config) { return promise.promise; } - kc.refreshAccessToken = function(minValidity) { + kc.isTokenExpired = function(minValidity) { + if (!kc.tokenParsed || !kc.refreshToken) { + throw 'Not authenticated'; + } + + var expiresIn = kc.tokenParsed['exp'] - (new Date().getTime() / 1000); + if (minValidity) { + expiresIn -= minValidity; + } + + return expiresIn < 0; + } + + kc.updateToken = function(minValidity) { if (!kc.tokenParsed || !kc.refreshToken) { throw 'Not authenticated'; } @@ -152,8 +175,7 @@ var Keycloak = function (config) { var promise = createPromise(); if (minValidity) { - var expiresIn = kc.tokenParsed['exp'] - (new Date().getTime() / 1000); - if (expiresIn > minValidity) { + if (!kc.isTokenExpired(minValidity)) { promise.setSuccess(false); return promise.promise; } @@ -191,15 +213,6 @@ var Keycloak = function (config) { return promise.promise; } - kc.processCallback = function(url) { - var callback = parseCallback(url); - if (callback) { - var promise = createPromise(); - processCallback(callback, promise); - return promise; - } - } - function getRealmUrl() { return kc.authServerUrl + '/rest/realms/' + encodeURIComponent(kc.realm); } @@ -231,10 +244,10 @@ var Keycloak = function (config) { var tokenResponse = JSON.parse(req.responseText); setToken(tokenResponse['access_token'], tokenResponse['refresh_token']); kc.onAuthSuccess && kc.onAuthSuccess(); - promise.setSuccess(true); + promise && promise.setSuccess(); } else { kc.onAuthError && kc.onAuthError(); - promise.setError(); + promise && promise.setError(); } } }; @@ -243,33 +256,75 @@ var Keycloak = function (config) { } else if (error) { if (prompt != 'none') { kc.onAuthError && kc.onAuthError(); - promise.setError(); + promise && promise.setError(); } } } - function loadConfig(url, configPromise) { - var req = new XMLHttpRequest(); - req.open('GET', url, true); - req.setRequestHeader('Accept', 'application/json'); + function loadConfig(url) { + var promise = createPromise(); + var configUrl; - req.onreadystatechange = function () { - if (req.readyState == 4) { - if (req.status == 200) { - var config = JSON.parse(req.responseText); + if (!config) { + configUrl = 'keycloak.json'; + } else if (typeof config === 'string') { + configUrl = config; + } - kc.authServerUrl = config['auth-server-url']; - kc.realm = config['realm']; - kc.clientId = config['resource']; + if (configUrl) { + var req = new XMLHttpRequest(); + req.open('GET', configUrl, true); + req.setRequestHeader('Accept', 'application/json'); - configPromise.setSuccess(); - } else { - configPromise.setError(); + req.onreadystatechange = function () { + if (req.readyState == 4) { + if (req.status == 200) { + var config = JSON.parse(req.responseText); + + kc.authServerUrl = config['auth-server-url']; + kc.realm = config['realm']; + kc.clientId = config['resource']; + + promise.setSuccess(); + } else { + promise.setError(); + } + } + }; + + req.send(); + } else { + if (!config['url']) { + var scripts = document.getElementsByTagName('script'); + for (var i = 0; i < scripts.length; i++) { + if (scripts[i].src.match(/.*keycloak\.js/)) { + config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js')); + break; + } } } - }; - req.send(); + if (!config.realm) { + throw 'realm missing'; + } + + if (!config.clientId) { + throw 'clientId missing'; + } + + kc.authServerUrl = config.url; + kc.realm = config.realm; + kc.clientId = config.clientId; + + promise.setSuccess(); + } + + return promise.promise; + } + + function clearToken() { + setToken(null, null); + kc.onAuthLogout && kc.onAuthLogout(); } function setToken(token, refreshToken) { @@ -310,21 +365,6 @@ var Keycloak = function (config) { } } - function getEncodedRedirectUri(redirectUri) { - var url; - if (redirectUri) { - url = redirectUri; - } else if (kc.redirectUri) { - url = kc.redirectUri; - } else { - url = (location.protocol + '//' + location.hostname + (location.port && (':' + location.port)) + location.pathname); - if (location.hash) { - url += '?redirect_fragment=' + encodeURIComponent(location.hash.substring(1)); - } - } - return encodeURI(url); - } - function createUUID() { var s = []; var hexDigits = '0123456789abcdef'; @@ -339,7 +379,6 @@ var Keycloak = function (config) { } function parseCallback(url) { - if (url.indexOf('?') != -1) { var oauth = {}; @@ -365,7 +404,7 @@ var Keycloak = function (config) { } } - if (oauth.state && oauth.state == sessionStorage.oauthState) { + if ((oauth.code || oauth.error) && oauth.state && oauth.state == sessionStorage.oauthState) { delete sessionStorage.oauthState; return oauth; } @@ -412,6 +451,103 @@ var Keycloak = function (config) { return p; } + function loadAdapter(type) { + if (!type || type == 'default') { + return { + login: function(options) { + window.location.href = kc.createLoginUrl(options); + }, + + logout: function(options) { + window.location.href = kc.createLogoutUrl(options); + }, + + accountManagement : function() { + window.location.href = kc.createAccountUrl(); + }, + + redirectUri: function(options) { + if (options && options.redirectUri) { + return options.redirectUri; + } else if (kc.redirectUri) { + return kc.redirectUri; + } else { + var url = (location.protocol + '//' + location.hostname + (location.port && (':' + location.port)) + location.pathname); + if (location.hash) { + url += '?redirect_fragment=' + encodeURIComponent(location.hash.substring(1)); + } + return url; + } + } + }; + } + + if (type == 'cordova') { + console.debug('Enabling Cordova support'); + + return { + login: function(options) { + var promise = createPromise(); + + var o = 'location=no'; + if (options && options.prompt == 'none') { + o += ',hidden=yes'; + } + + var loginUrl = kc.createLoginUrl(options); + var ref = window.open(loginUrl, '_blank', o); + ref.addEventListener('loadstart', function(event) { + if (event.url.indexOf('http://localhost') == 0) { + var callback = parseCallback(event.url); + ref.close(); + processCallback(callback); + + if (callback.code) { + promise.setSuccess(); + } else { + promise.setError(); + } + } + }); + + return promise.promise; + }, + + logout: function(options) { + var promise = createPromise(); + + var logoutUrl = kc.createLogoutUrl(options); + var ref = window.open(logoutUrl, '_blank', 'location=no,hidden=yes'); + ref.addEventListener('loadstart', function(event) { + if (event.url.indexOf('http://localhost') == 0) { + ref.close(); + clearToken(); + promise.setSuccess(); + } + }); + + return promise.promise; + }, + + accountManagement : function() { + var accountUrl = kc.createAccountUrl(); + var ref = window.open(accountUrl, '_blank', 'location=no'); + ref.addEventListener('loadstart', function(event) { + if (event.url.indexOf('http://localhost') == 0) { + ref.close(); + } + }); + }, + + redirectUri: function(options) { + return 'http://localhost'; + } + } + } + + throw 'invalid adapter type: ' + type; + } + var idTokenProperties = [ "name", "given_name",