Updates to keycloak.js, including adding support for Cordova

Conflicts:
	examples/cordova/example-realm.json
	examples/js-console/src/main/webapp/index.html
	integration/js/src/main/resources/META-INF/resources/js/keycloak.js
This commit is contained in:
Stian Thorgersen 2014-03-15 12:53:52 +00:00
parent 2d5e6520db
commit b5bdeae2a2
12 changed files with 484 additions and 125 deletions

4
examples/cordova/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
platforms
plugins
www/keycloak.js
www/keycloak.json

View file

@ -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'.

View file

@ -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"]
}
]
}
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:gap="http://phonegap.com/ns/1.0" id="org.keycloak.examples.cordova" version="1.0.0">
<name>Keycloak Auth</name>
<description>Keycloak Cordova Example</description>
<author href="http://www.keycloak.org">Keycloak Team</author>
<feature name="http://api.phonegap.com/1.0/device" />
<preference name="permissions" value="none"/>
<gap:plugin name="org.apache.cordova.inappbrowser" />
<access origin="http://*"/>
</widget>

View file

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html>
<head>
<title>Authentication Example</title>
<script type="text/javascript" charset="utf-8" src="cordova.js"></script>
<script type="text/javascript" charset="utf-8" src="keycloak.js"></script>
<script type="text/javascript" charset="utf-8">
var keycloak = new Keycloak();
keycloak.onReady = updateState;
keycloak.onAuthSuccess = updateState;
keycloak.onAuthRefreshSuccess = updateState;
keycloak.onAuthLogout = updateState;
function updateState() {
console.debug('Updating state');
if (keycloak.authenticated) {
document.getElementById('authenticated').style.display = 'block';
document.getElementById('not-authenticated').style.display = 'none';
document.getElementById('subject').innerText = keycloak.subject;
document.getElementById('username').innerText = keycloak.idToken.preferred_username;
document.getElementById('tokenExpires').innerText = new Date(keycloak.tokenParsed.exp * 1000).toLocaleString();
document.getElementById('tokenRefreshExpires').innerText = new Date(keycloak.refreshTokenParsed.exp * 1000).toLocaleString();
} else {
document.getElementById('authenticated').style.display = 'none';
document.getElementById('not-authenticated').style.display = 'block';
}
}
document.addEventListener("deviceready", function() {
console.debug('Device ready');
keycloak.init('check-sso');
}, false);
</script>
</head>
<body>
<div id="authenticated" style="display: none;">
<div>
<button onclick="keycloak.logout()">Log out</button>
<button onclick="keycloak.updateToken()">Refresh token</button>
</div>
<div>
<table>
<tr>
<td>Subject</td>
<td id="subject"></td>
</tr>
<tr>
<td>Username</td>
<td id="username"></td>
</tr>
<tr>
<td>Token expires</td>
<td id="tokenExpires"></td>
</tr>
<tr>
<td>Refresh token expires</td>
<td id="tokenRefreshExpires"></td>
</tr>
</table>
</div>
</div>
<div id="not-authenticated" style="display: none;">
<div>
<button onclick="keycloak.login()">Log in</button>
</div>
<div>
<p>Not authenticated</p>
</div>
</div>
</body>
</html>

View file

@ -14,14 +14,6 @@
<name>Customer Portal CLI</name>
<description/>
<repositories>
<repository>
<id>jboss</id>
<name>jboss repo</name>
<url>http://repository.jboss.org/nexus/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
@ -32,6 +24,14 @@
<build>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<version>7.4.Final</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>

View file

@ -5,7 +5,8 @@
</head>
<body bgcolor="#E3F6CE">
<p>Goto: <a href="#" onclick="keycloak.logout()">logout</a></p>
<p>Goto: <a href="http://localhost:8080/product-portal">products</a> | <a href="#" onclick="keycloak.logout()">logout</a> | <a href="#" onclick="keycloak.accountManagement()">manage acct</a></p>
User <b id="subject"></b> made this request.
<p><b>User details (from <span id="profileType"></span>)</b></p>
<p>Username: <span id="username"></span></p>
@ -18,16 +19,11 @@ User <b id="subject"></b> made this request.
<div id="customers"></div>
<script>
var keycloak = Keycloak({
clientId: 'customer-portal-js',
realm: 'demo',
onload: 'login-required'
});
var keycloak = Keycloak('../keycloak.json');
var loadData = function () {
document.getElementById('subject').innerText = keycloak.subject;
console.debug(keycloak.idToken);
if (keycloak.idToken) {
document.getElementById('profileType').innerText = 'IDToken';
document.getElementById('username').innerText = keycloak.idToken.preferred_username;
@ -74,14 +70,14 @@ User <b id="subject"></b> made this request.
var loadFailure = function () {
document.getElementById('customers').innerHTML = '<b>Failed to load data. Check console log</b>';
};
var reloadData = function () {
keycloak.onValidAccessToken(loadData, loadFailure);
keycloak.updateToken(10).success(loadData).error(loadFailure);
}
keycloak.init(loadData, loadFailure);
keycloak.onAuthSuccess = loadData;
keycloak.init('login-required');
</script>

View file

@ -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
}

View file

@ -51,6 +51,10 @@
"client": "customer-portal",
"roles": ["user"]
},
{
"client": "customer-portal-js",
"roles": ["user"]
},
{
"client": "product-portal",
"roles": ["user"]

View file

@ -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'.

View file

@ -1,6 +1,6 @@
<html>
<head>
<script src="http://localhost:8080/auth/js/keycloak.js"></script>
<script src="/auth/js/keycloak.js"></script>
</head>
<body>
@ -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 {

View file

@ -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,14 +256,24 @@ var Keycloak = function (config) {
} else if (error) {
if (prompt != 'none') {
kc.onAuthError && kc.onAuthError();
promise.setError();
promise && promise.setError();
}
}
}
function loadConfig(url, configPromise) {
function loadConfig(url) {
var promise = createPromise();
var configUrl;
if (!config) {
configUrl = 'keycloak.json';
} else if (typeof config === 'string') {
configUrl = config;
}
if (configUrl) {
var req = new XMLHttpRequest();
req.open('GET', url, true);
req.open('GET', configUrl, true);
req.setRequestHeader('Accept', 'application/json');
req.onreadystatechange = function () {
@ -262,14 +285,46 @@ var Keycloak = function (config) {
kc.realm = config['realm'];
kc.clientId = config['resource'];
configPromise.setSuccess();
promise.setSuccess();
} else {
configPromise.setError();
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;
}
}
}
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",