From 34fe0a751c428f36dffff4b2f054dfd1b6b50632 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Thu, 10 Oct 2013 17:55:03 +0100 Subject: [PATCH] Added cors support to TokenService.accessCodeToToken --- .../idm/ApplicationRepresentation.java | 9 + examples/js-google/index.html | 24 ++ examples/js-google/keycloak.js | 139 +++++++++++ examples/js-google/keycloak.js.orig | 222 ++++++++++++++++++ examples/js-google/kinvey.html | 13 + examples/js-google/testrealm.json | 60 +++++ examples/js/keycloak.js | 186 +++++++-------- examples/js/testrealm.json | 1 + .../java/org/keycloak/models/UserModel.java | 8 + .../models/picketlink/UserAdapter.java | 21 ++ .../services/managers/ApplicationManager.java | 15 ++ .../org/keycloak/services/resources/Cors.java | 43 ++++ .../services/resources/TokenService.java | 3 +- 13 files changed, 639 insertions(+), 105 deletions(-) create mode 100644 examples/js-google/index.html create mode 100644 examples/js-google/keycloak.js create mode 100644 examples/js-google/keycloak.js.orig create mode 100644 examples/js-google/kinvey.html create mode 100755 examples/js-google/testrealm.json create mode 100644 services/src/main/java/org/keycloak/services/resources/Cors.java diff --git a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java index 7b7fa9d48a..878092e886 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java @@ -21,6 +21,7 @@ public class ApplicationRepresentation { protected List roleMappings; protected List scopeMappings; protected List redirectUris; + protected List webOrigins; public String getSelf() { return self; @@ -155,4 +156,12 @@ public class ApplicationRepresentation { public void setRedirectUris(List redirectUris) { this.redirectUris = redirectUris; } + + public List getWebOrigins() { + return webOrigins; + } + + public void setWebOrigins(List webOrigins) { + this.webOrigins = webOrigins; + } } diff --git a/examples/js-google/index.html b/examples/js-google/index.html new file mode 100644 index 0000000000..7a7212060d --- /dev/null +++ b/examples/js-google/index.html @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/examples/js-google/keycloak.js b/examples/js-google/keycloak.js new file mode 100644 index 0000000000..172557dbad --- /dev/null +++ b/examples/js-google/keycloak.js @@ -0,0 +1,139 @@ +window.keycloak = (function () { + var kc = {}; + var config = { + clientId: null, + clientSecret: null + }; + + kc.init = function (c) { + for (var prop in config) { + if (c[prop]) { + config[prop] = c[prop]; + } + + if (!config[prop]) { + throw new Error(prop + ' not defined'); + } + } + + loadToken(); + + if (kc.token) { + kc.user = kc.tokenInfo.user_id; + kc.authenticated = true; + } else { + kc.authenticated = false; + kc.user = null; + } + } + + kc.login = function () { + var clientId = encodeURIComponent(config.clientId); + var redirectUri = encodeURIComponent(window.location.href); + var state = encodeURIComponent(createUUID()); + var scope = encodeURIComponent('https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/plus.login'); + var url = 'https://accounts.google.com/o/oauth2/auth?response_type=token&client_id=' + clientId + '&redirect_uri=' + redirectUri + + '&state=' + state + '&scope=' + scope; + + sessionStorage.state = state; + + window.location.href = url; + } + + function parseToken(token) { + return JSON.parse(atob(token.split('.')[1])); + } + + kc.profile = function(header) { + var url = 'https://www.googleapis.com/oauth2/v1/userinfo' + + if (!header) { + url = url + '?access_token=' + kc.token; + } + + var http = new XMLHttpRequest(); + http.open('GET', url, false); + if (header) { + http.setRequestHeader('Authorization', 'Bearer ' + kc.token); + } + + http.send(); + if (http.status == 200) { + return JSON.parse(http.responseText); + } + } + + kc.contacts = function(header) { + var url = 'https://www.googleapis.com/plus/v1/people/me'; + + if (!header) { + url = url + '?access_token=' + kc.token; + } + + var http = new XMLHttpRequest(); + http.open('GET', url, false); + if (header) { + http.setRequestHeader('Authorization', 'Bearer ' + kc.token); + } + + http.send(); + if (http.status == 200) { + return http.responseText; + } + } + + return kc; + + function loadToken() { + var params = {} + var queryString = location.hash.substring(1) + var regex = /([^&=]+)=([^&]*)/g, m; + while (m = regex.exec(queryString)) { + params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); + } + + var token = params['access_token']; + var state = params['state']; + + if (token && state === sessionStorage.state) { + window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname); + + kc.token = token; + + var url = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + token; + + var http = new XMLHttpRequest(); + http.open('GET', url, false); + + http.send(); + if (http.status == 200) { + kc.tokenInfo = JSON.parse(http.responseText); + } + } + return undefined; + } + + function getQueryParam(name) { + console.debug(window.location.hash); + var params = window.location.hash.substring(1).split('&'); + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + if (decodeURIComponent(p[0]) == name) { + return p[1]; + } + } + } + + function createUUID() { + var s = []; + var hexDigits = '0123456789abcdef'; + for (var i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = '4'; + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = '-'; + var uuid = s.join(''); + return uuid; + } +})(); diff --git a/examples/js-google/keycloak.js.orig b/examples/js-google/keycloak.js.orig new file mode 100644 index 0000000000..439d2af5f6 --- /dev/null +++ b/examples/js-google/keycloak.js.orig @@ -0,0 +1,222 @@ +<<<<<<< Updated upstream +window.keycloak = (function() { + var kc = {}; + var config = null; + + kc.init = function(c) { + config = c; + + var token = getTokenFromCode(); + if (token) { + var t = parseToken(token); + kc.user = t.prn; + kc.authenticated = true; + } else { + kc.authenticated = false; + } + } + + kc.login = function() { + var clientId = encodeURIComponent(config.clientId); + var redirectUri = encodeURIComponent(window.location.href); + var state = encodeURIComponent(createUUID()); + var realm = encodeURIComponent(config.realm); + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri + + '&state=' + state; + window.location.href = url; + } + + return kc; + + function parseToken(token) { + return JSON.parse(atob(token.split('.')[1])); + } + + function getTokenFromCode() { + var code = getQueryParam('code'); + if (code) { + window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname); + + var clientId = encodeURIComponent(config.clientId); + var clientSecret = encodeURIComponent(config.clientSecret); + var realm = encodeURIComponent(config.realm); + + var params = 'code=' + code + '&client_id=' + config.clientId + '&password=' + config.clientSecret; + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes' + + var http = new XMLHttpRequest(); + http.open('POST', url, false); + http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + + http.send(params); + if (http.status == 200) { + return JSON.parse(http.responseText)['access_token']; + } + } + return undefined; + } + + function getQueryParam(name) { + var params = window.location.search.substring(1).split('&'); + for ( var i = 0; i < params.length; i++) { + var p = params[i].split('='); + if (decodeURIComponent(p[0]) == name) { + return p[1]; + } + } + } + + function createUUID() { + var s = []; + var hexDigits = '0123456789abcdef'; + for ( var i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = '4'; + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = '-'; + var uuid = s.join(''); + return uuid; + } +======= +window.keycloak = (function () { + var kc = {}; + var config = { + baseUrl : null, + clientId : null, + clientSecret: null, + realm: null + }; + + kc.init = function (c) { + for (var prop in config) { + if (c[prop]) { + config[prop] = c[prop]; + } + + if (!config[prop]) { + throw new Error(prop + 'not defined'); + } + } + + var token = getTokenFromCode(); + if (token) { + var t = parseToken(token); + kc.user = t.prn; + kc.authenticated = true; + } else { + kc.authenticated = false; + } + } + + kc.login = function () { + var clientId = encodeURIComponent(config.clientId); + var redirectUri = encodeURIComponent(window.location.href); + var realm = encodeURIComponent(config.realm); + var state = encodeURIComponent(createUUID()); + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri + + '&state=' + state; + + sessionStorage.state = state; + + window.location.href = url; + } + + return kc; + + function parseToken(token) { + var t = base64Decode(token.split('.')[1]); + return JSON.parse(t); + } + + function getTokenFromCode() { + var code = getQueryParam('code'); + var state = getQueryParam('state'); + + if (code) { + if (state && state === sessionStorage.state) { + window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname); + + var clientId = encodeURIComponent(config.clientId); + var clientSecret = encodeURIComponent(config.clientSecret); + var realm = encodeURIComponent(config.realm); + + var params = 'code=' + code + '&client_id=' + clientId + '&password=' + clientSecret; + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes' + + var http = new XMLHttpRequest(); + http.open('POST', url, false); + http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + + http.send(params); + if (http.status == 200) { + return JSON.parse(http.responseText)['access_token']; + } + } + } + return undefined; + } + + function getQueryParam(name) { + var params = window.location.search.substring(1).split('&'); + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + if (decodeURIComponent(p[0]) == name) { + return p[1]; + } + } + } + + function createUUID() { + var s = []; + var hexDigits = '0123456789abcdef'; + for (var i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = '4'; + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = '-'; + var uuid = s.join(''); + return uuid; + } + + function base64Decode(data) { + var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ''; + + do { + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; + + o1 = bits >> 16 & 0xff; + o2 = bits >> 8 & 0xff; + o3 = bits & 0xff; + + if (h3 == 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 == 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(''); + + return dec; + } +>>>>>>> Stashed changes +})(); \ No newline at end of file diff --git a/examples/js-google/kinvey.html b/examples/js-google/kinvey.html new file mode 100644 index 0000000000..9e1324c3d4 --- /dev/null +++ b/examples/js-google/kinvey.html @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/examples/js-google/testrealm.json b/examples/js-google/testrealm.json new file mode 100755 index 0000000000..2468f48035 --- /dev/null +++ b/examples/js-google/testrealm.json @@ -0,0 +1,60 @@ +{ + "id": "test", + "realm": "test", + "enabled": true, + "tokenLifespan": 300, + "accessCodeLifespan": 10, + "accessCodeLifespanUserAction": 600, + "sslNotRequired": true, + "cookieLoginAllowed": true, + "registrationAllowed": true, + "resetPasswordAllowed": 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" ], + "requiredApplicationCredentials": [ "password" ], + "requiredOAuthClientCredentials": [ "password" ], + "defaultRoles": [ "user" ], + "users" : [ + { + "username" : "test-user@localhost", + "enabled": true, + "email" : "test-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + } + ], + "roles": [ + { + "name": "user", + "description": "Have User privileges" + }, + { + "name": "admin", + "description": "Have Administrator privileges" + } + ], + "roleMappings": [ + { + "username": "test-user@localhost", + "roles": ["user"] + } + ], + "applications": [ + { + "name": "test-app", + "enabled": true, + "adminUrl": "http://localhost:8081/app/logout", + "useRealmMappings": true, + "webOrigins": [ "http://localhost", "http://localhost:8000", "http://localhost:8080" ], + "credentials": [ + { + "type": "password", + "value": "password" + } + ] + } + ] +} diff --git a/examples/js/keycloak.js b/examples/js/keycloak.js index dfb6ebb263..480996d343 100644 --- a/examples/js/keycloak.js +++ b/examples/js/keycloak.js @@ -1,120 +1,98 @@ -window.keycloak = (function() { - var kc = {}; - var config = null; +window.keycloak = (function () { + var kc = {}; + var config = { + baseUrl: null, + clientId: null, + clientSecret: null, + realm: null + }; - kc.init = function(c) { - config = c; + kc.init = function (c) { + for (var prop in config) { + if (c[prop]) { + config[prop] = c[prop]; + } - var token = getTokenFromCode(); - if (token) { - var t = parseToken(token); - kc.user = t.prn; - kc.authenticated = true; - } else { - kc.authenticated = false; - } - } + if (!config[prop]) { + throw new Error(prop + 'not defined'); + } + } - kc.login = function() { - var clientId = encodeURIComponent(config.clientId); - var redirectUri = encodeURIComponent(window.location.href); - var state = encodeURIComponent(createUUID()); - var realm = encodeURIComponent(config.realm); - var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri - + '&state=' + state; - window.location.href = url; - } + var token = getTokenFromCode(); + if (token) { + var t = parseToken(token); + kc.user = t.prn; + kc.authenticated = true; + kc.token = token; + } else { + kc.authenticated = false; + } + } - return kc; + kc.login = function () { + var clientId = encodeURIComponent(config.clientId); + var redirectUri = encodeURIComponent(window.location.href); + var state = encodeURIComponent(createUUID()); + var realm = encodeURIComponent(config.realm); + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri + + '&state=' + state; - function parseToken(token) { - var t = base64Decode(token.split('.')[1]); - return JSON.parse(t); - } + sessionStorage.state = state; - function getTokenFromCode() { - var code = getQueryParam('code'); - if (code) { - window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname); - - var clientId = encodeURIComponent(config.clientId); - var clientSecret = encodeURIComponent(config.clientSecret); - var realm = encodeURIComponent(config.realm); + window.location.href = url; + } - var params = 'code=' + code + '&client_id=' + config.clientId + '&password=' + config.clientSecret; - var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes' + return kc; - var http = new XMLHttpRequest(); - http.open('POST', url, false); - http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + function parseToken(token) { + return JSON.parse(atob(token.split('.')[1])); + } - http.send(params); - if (http.status == 200) { - return JSON.parse(http.responseText)['access_token']; - } - } - return undefined; - } + function getTokenFromCode() { + var code = getQueryParam('code'); + var state = getQueryParam('state'); + if (code && state === sessionStorage.state) { + window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname); - function getQueryParam(name) { - var params = window.location.search.substring(1).split('&'); - for ( var i = 0; i < params.length; i++) { - var p = params[i].split('='); - if (decodeURIComponent(p[0]) == name) { - return p[1]; - } - } - } + var clientId = encodeURIComponent(config.clientId); + var clientSecret = encodeURIComponent(config.clientSecret); + var realm = encodeURIComponent(config.realm); - function createUUID() { - var s = []; - var hexDigits = '0123456789abcdef'; - for ( var i = 0; i < 36; i++) { - s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); - } - s[14] = '4'; - s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); - s[8] = s[13] = s[18] = s[23] = '-'; - var uuid = s.join(''); - return uuid; - } + var params = 'code=' + code + '&client_id=' + clientId + '&password=' + clientSecret; + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes' - function base64Decode (data) { - var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, - ac = 0, - dec = "", - tmp_arr = []; + var http = new XMLHttpRequest(); + http.open('POST', url, false); + http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - if (!data) { - return data; - } + http.send(params); + if (http.status == 200) { + return JSON.parse(http.responseText)['access_token']; + } + } + return undefined; + } - data += ''; + function getQueryParam(name) { + var params = window.location.search.substring(1).split('&'); + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + if (decodeURIComponent(p[0]) == name) { + return p[1]; + } + } + } - do { - h1 = b64.indexOf(data.charAt(i++)); - h2 = b64.indexOf(data.charAt(i++)); - h3 = b64.indexOf(data.charAt(i++)); - h4 = b64.indexOf(data.charAt(i++)); - - bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; - - o1 = bits >> 16 & 0xff; - o2 = bits >> 8 & 0xff; - o3 = bits & 0xff; - - if (h3 == 64) { - tmp_arr[ac++] = String.fromCharCode(o1); - } else if (h4 == 64) { - tmp_arr[ac++] = String.fromCharCode(o1, o2); - } else { - tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); - } - } while (i < data.length); - - dec = tmp_arr.join(''); - - return dec; - } + function createUUID() { + var s = []; + var hexDigits = '0123456789abcdef'; + for (var i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = '4'; + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = '-'; + var uuid = s.join(''); + return uuid; + } })(); \ No newline at end of file diff --git a/examples/js/testrealm.json b/examples/js/testrealm.json index 824fe14153..2468f48035 100755 --- a/examples/js/testrealm.json +++ b/examples/js/testrealm.json @@ -48,6 +48,7 @@ "enabled": true, "adminUrl": "http://localhost:8081/app/logout", "useRealmMappings": true, + "webOrigins": [ "http://localhost", "http://localhost:8000", "http://localhost:8080" ], "credentials": [ { "type": "password", diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java index 8598ae765d..7e331857c0 100755 --- a/model/api/src/main/java/org/keycloak/models/UserModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserModel.java @@ -35,6 +35,14 @@ public interface UserModel { void removeRequiredAction(RequiredAction action); + Set getWebOrigins(); + + void setWebOrigins(Set webOrigins); + + void addWebOrigin(String webOrigin); + + void removeWebOrigin(String webOrigin); + Set getRedirectUris(); void setRedirectUris(Set redirectUris); diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/UserAdapter.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/UserAdapter.java index 98894ced7e..e44c92ac17 100755 --- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/UserAdapter.java +++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/UserAdapter.java @@ -22,6 +22,7 @@ public class UserAdapter implements UserModel { private static final String REQUIRED_ACTIONS_ATTR = "requiredActions"; private static final String REDIRECT_URIS = "redirectUris"; + private static final String WEB_ORIGINS = "webOrigins"; protected User user; protected IdentityManager idm; @@ -161,6 +162,26 @@ public class UserAdapter implements UserModel { removeFromAttributeSet(REDIRECT_URIS, redirectUri); } + @Override + public Set getWebOrigins() { + return getAttributeSet(WEB_ORIGINS); + } + + @Override + public void setWebOrigins(Set webOrigins) { + setAttributeSet(WEB_ORIGINS, webOrigins); + } + + @Override + public void addWebOrigin(String webOrigin) { + addToAttributeSet(WEB_ORIGINS, webOrigin); + } + + @Override + public void removeWebOrigin(String webOrigin) { + removeFromAttributeSet(WEB_ORIGINS, webOrigin); + } + @Override public boolean isTotp() { Attribute a = user.getAttribute(KEYCLOAK_TOTP_ATTR); diff --git a/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java b/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java index 93c29599d4..9b89cf4836 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java @@ -42,6 +42,11 @@ public class ApplicationManager { resourceUser.addRedirectUri(redirectUri); } } + if (resourceRep.getWebOrigins() != null) { + for (String webOrigin : resourceRep.getWebOrigins()) { + resourceUser.addWebOrigin(webOrigin); + } + } realm.grantRole(resourceUser, loginRole); @@ -97,6 +102,11 @@ public class ApplicationManager { if (redirectUris != null) { resource.getApplicationUser().setRedirectUris(new HashSet(redirectUris)); } + + List webOrigins = rep.getWebOrigins(); + if (webOrigins != null) { + resource.getApplicationUser().setWebOrigins(new HashSet(webOrigins)); + } } public ApplicationRepresentation toRepresentation(ApplicationModel applicationModel) { @@ -113,6 +123,11 @@ public class ApplicationManager { rep.setRedirectUris(new LinkedList(redirectUris)); } + Set webOrigins = applicationModel.getApplicationUser().getWebOrigins(); + if (webOrigins != null) { + rep.setWebOrigins(new LinkedList(webOrigins)); + } + return rep; } diff --git a/services/src/main/java/org/keycloak/services/resources/Cors.java b/services/src/main/java/org/keycloak/services/resources/Cors.java new file mode 100644 index 0000000000..8c28d6fa20 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/Cors.java @@ -0,0 +1,43 @@ +package org.keycloak.services.resources; + +import java.util.Set; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; + +import org.jboss.resteasy.spi.HttpRequest; + +/** + * @author Stian Thorgersen + */ +public class Cors { + + private HttpRequest request; + private ResponseBuilder response; + private Set allowedOrigins; + + public Cors(HttpRequest request, ResponseBuilder response) { + this.request = request; + this.response = response; + } + + public static Cors add(HttpRequest request, ResponseBuilder response) { + return new Cors(request, response); + } + + public Cors allowedOrigins(Set allowedOrigins) { + this.allowedOrigins = allowedOrigins; + return this; + } + + public Response build() { + String origin = request.getHttpHeaders().getHeaderString("Origin"); + if (origin == null || allowedOrigins == null || (!allowedOrigins.contains(origin))) { + return response.build(); + } + + response.header("Access-Control-Allow-Origin", origin); + return response.build(); + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index d72e80d9d4..7296d8e040 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -415,7 +415,8 @@ public class TokenService { } logger.info("accessRequest SUCCESS"); AccessTokenResponse res = accessTokenResponse(realm.getPrivateKey(), accessCode.getToken()); - return Response.ok(res).build(); + + return Cors.add(request, Response.ok(res)).allowedOrigins(client.getWebOrigins()).build(); } protected AccessTokenResponse accessTokenResponse(PrivateKey privateKey, SkeletonKeyToken token) {