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 @@ + + + Keycloak Auth + Keycloak Cordova Example + Keycloak Team + + + + + + + + + diff --git a/examples/cordova/www/index.html b/examples/cordova/www/index.html new file mode 100644 index 0000000000..43bd527db2 --- /dev/null +++ b/examples/cordova/www/index.html @@ -0,0 +1,77 @@ + + + + Authentication Example + + + + + + + + + + + diff --git a/examples/demo-template/customer-app-cli/pom.xml b/examples/demo-template/customer-app-cli/pom.xml index 2abf6709fc..363bc3ea93 100755 --- a/examples/demo-template/customer-app-cli/pom.xml +++ b/examples/demo-template/customer-app-cli/pom.xml @@ -14,14 +14,6 @@ Customer Portal CLI - - - jboss - jboss repo - http://repository.jboss.org/nexus/content/groups/public/ - - - org.keycloak @@ -32,6 +24,14 @@ + + org.jboss.as.plugins + jboss-as-maven-plugin + 7.4.Final + + true + + org.apache.maven.plugins maven-deploy-plugin 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 845a1ffe1a..445876f001 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 @@ -5,7 +5,8 @@ -

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",