├── server ├── project │ ├── plugins.sbt │ └── build.properties ├── run.sh ├── src │ └── main │ │ ├── resources │ │ ├── application.conf │ │ ├── keycloak.json │ │ └── logback.xml │ │ └── scala │ │ └── com │ │ └── bandrzejczak │ │ └── sso │ │ ├── oauth2 │ │ ├── TokenVerifier.scala │ │ ├── KeycloakTokenVerifier.scala │ │ └── OAuth2Authorization.scala │ │ ├── Jsonp.scala │ │ └── Main.scala └── build.sbt ├── .gitignore ├── client ├── run.sh ├── index.html ├── app.js └── keycloak.js └── README.md /server/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn -------------------------------------------------------------------------------- /server/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.8 -------------------------------------------------------------------------------- /server/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sbt run 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | keycloak/ 2 | .idea 3 | *.iml 4 | target 5 | log 6 | 7 | -------------------------------------------------------------------------------- /client/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python -m SimpleHTTPServer 8000 4 | -------------------------------------------------------------------------------- /server/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loggers = ["akka.event.slf4j.Slf4jLogger"] 3 | loglevel = "DEBUG" 4 | } -------------------------------------------------------------------------------- /server/src/main/scala/com/bandrzejczak/sso/oauth2/TokenVerifier.scala: -------------------------------------------------------------------------------- 1 | package com.bandrzejczak.sso.oauth2 2 | 3 | import scala.concurrent.Future 4 | 5 | trait TokenVerifier { 6 | def verifyToken(token: String): Future[String] 7 | } 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keycloak-angular-akka-http 2 | 3 | A source code for two-post series on my blog: 4 | 5 | - [Part 1](http://bandrzejczak.com/blog/2015/11/22/single-sign-on-with-keycloak-in-a-sigle-page-application-part-1-slash-2-angular-dot-js/) 6 | - [Part 2](http://bandrzejczak.com/blog/2015/12/06/sso-for-your-single-page-application-part-2-slash-2-akka-http/) 7 | -------------------------------------------------------------------------------- /server/src/main/resources/keycloak.json: -------------------------------------------------------------------------------- 1 | { 2 | "realm": "master", 3 | "realm-public-key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiRBZR10PVBsmDiFekOi0Q2AanME3Atyf2/kbIzW5R8YNf5i0xg+r18pOwmxNm9+DjyF2NCCpeWbrG6a0PoyeDq7UvNiRg0BVvfPXqvfNVWk7Tf6H/Vm1ENzWJuUMdOeGnJW6Ov9sBe3QRBI0Go1eoMOpx306E7hM3jHkx3Rc+Q8AURL2mhGZ3ZhD22V53Su2XEAGIQuFCncXw5KJMGzI2r/YtK9LwOSKxapJhYc05imatr5y7VEPijWSEtRQ5+IdkGy1FJcPxy9pPad1vPQlIsjMNQLFMlsW2zA0lz+E7keE/GMaPw2su89X7dsWkq4YRs63Q5L6+9EO+Em0PmC46wIDAQAB", 4 | "bearer-only": true, 5 | "auth-server-url": "http://localhost:8080/auth", 6 | "ssl-required": "external", 7 | "resource": "backend" 8 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Keycloak Test 6 | 7 | 8 |
9 | You've been authorized using token {{authData.token}}
10 | Your preferred username is {{authData.username}}. 11 |
12 | Logout 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /server/src/main/scala/com/bandrzejczak/sso/oauth2/KeycloakTokenVerifier.scala: -------------------------------------------------------------------------------- 1 | package com.bandrzejczak.sso.oauth2 2 | 3 | import org.keycloak.RSATokenVerifier 4 | import org.keycloak.adapters.KeycloakDeployment 5 | 6 | import scala.concurrent.forkjoin.ForkJoinPool 7 | import scala.concurrent.{ExecutionContext, Future} 8 | 9 | class KeycloakTokenVerifier(keycloakDeployment: KeycloakDeployment) extends TokenVerifier { 10 | implicit val executionContext = ExecutionContext.fromExecutor(new ForkJoinPool(2)) 11 | 12 | def verifyToken(token: String): Future[String] = { 13 | Future { 14 | RSATokenVerifier.verifyToken( 15 | token, 16 | keycloakDeployment.getRealmKey, 17 | keycloakDeployment.getRealmInfoUrl 18 | ).getPreferredUsername 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/src/main/scala/com/bandrzejczak/sso/Jsonp.scala: -------------------------------------------------------------------------------- 1 | package com.bandrzejczak.sso 2 | 3 | import akka.http.scaladsl.model.MediaTypes._ 4 | import akka.http.scaladsl.model.{ContentType, HttpEntity} 5 | import akka.http.scaladsl.server.Directives._ 6 | import akka.util.ByteString 7 | 8 | trait Jsonp { 9 | def jsonpWithParameter(parameterName: String) = 10 | parameter(parameterName.?).flatMap { 11 | case Some(wrapper) => 12 | mapResponseEntity { 13 | case HttpEntity.Strict(ct @ ContentType(`application/json`, _), data) => 14 | HttpEntity.Strict( 15 | ct.withMediaType(`application/javascript`), 16 | ByteString(wrapper + "(") ++ data ++ ByteString(")") 17 | ) 18 | case entity => entity 19 | } 20 | case None => pass 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | System.out 6 | 7 | %X{akkaTimestamp} %-5level[%thread] %logger{0} - %msg%n 8 | 9 | 10 | 11 | 12 | log/application.log 13 | false 14 | 15 | %date{yyyy-MM-dd} %X{akkaTimestamp} %-5level[%thread] %logger{1} - %msg%n 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /server/build.sbt: -------------------------------------------------------------------------------- 1 | name := "keycloak-akka-http" 2 | 3 | version := "1.0" 4 | 5 | scalaVersion := "2.11.7" 6 | 7 | val akkaV = "2.4.1" 8 | val akkaStreamV = "2.0-M2" 9 | 10 | val akka = Seq ( 11 | "com.typesafe.akka" %% "akka-actor" % akkaV, 12 | "com.typesafe.akka" %% "akka-http-experimental" % akkaStreamV, 13 | "com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaStreamV 14 | ) 15 | 16 | val logging = Seq ( 17 | "org.slf4j" % "slf4j-api" % "1.7.12", 18 | "ch.qos.logback" % "logback-classic" % "1.1.3", 19 | "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0", 20 | "com.typesafe.akka" %% "akka-slf4j" % akkaV 21 | ) 22 | 23 | val keycloak = Seq ( 24 | "org.keycloak" % "keycloak-adapter-core" % "1.6.1.Final", 25 | // we include all necessary transitive dependencies, 26 | // because they're marked provided in keycloak pom.xml 27 | "org.keycloak" % "keycloak-core" % "1.6.1.Final", 28 | "org.jboss.logging" % "jboss-logging" % "3.3.0.Final", 29 | "org.apache.httpcomponents" % "httpclient" % "4.5.1" 30 | ) 31 | 32 | libraryDependencies ++= akka ++ logging ++ keycloak 33 | -------------------------------------------------------------------------------- /server/src/main/scala/com/bandrzejczak/sso/Main.scala: -------------------------------------------------------------------------------- 1 | package com.bandrzejczak.sso 2 | 3 | import akka.actor.ActorSystem 4 | import akka.event.Logging 5 | import akka.http.scaladsl.Http 6 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 7 | import akka.http.scaladsl.server.Directives._ 8 | import akka.stream.ActorMaterializer 9 | import com.bandrzejczak.sso.oauth2.{KeycloakTokenVerifier, OAuth2Authorization, OAuth2Token} 10 | import org.keycloak.adapters.KeycloakDeploymentBuilder 11 | import spray.json.{DefaultJsonProtocol, RootJsonFormat} 12 | 13 | object Main extends App with Jsonp with JsonProtocol { 14 | implicit val system = ActorSystem("sso-system") 15 | implicit val actorMaterializer = ActorMaterializer() 16 | val oauth2 = new OAuth2Authorization( 17 | Logging(system, getClass), 18 | new KeycloakTokenVerifier( 19 | KeycloakDeploymentBuilder.build( 20 | getClass.getResourceAsStream("/keycloak.json") 21 | ) 22 | ) 23 | ) 24 | import oauth2._ 25 | 26 | val routes = authorized { token => 27 | path("test") { 28 | get { 29 | jsonpWithParameter("callback") { 30 | complete(token) 31 | } 32 | } 33 | } 34 | } 35 | 36 | Http().bindAndHandle(routes, "localhost", 9000) 37 | } 38 | 39 | trait JsonProtocol extends DefaultJsonProtocol with SprayJsonSupport { 40 | implicit def OAuth2TokenFormat: RootJsonFormat[OAuth2Token] = jsonFormat2(OAuth2Token) 41 | } 42 | -------------------------------------------------------------------------------- /server/src/main/scala/com/bandrzejczak/sso/oauth2/OAuth2Authorization.scala: -------------------------------------------------------------------------------- 1 | package com.bandrzejczak.sso.oauth2 2 | 3 | import akka.event.LoggingAdapter 4 | import akka.http.scaladsl.model.headers.{Authorization, OAuth2BearerToken} 5 | import akka.http.scaladsl.server.Directives._ 6 | import akka.http.scaladsl.server._ 7 | 8 | class OAuth2Authorization(logger: LoggingAdapter, tokenVerifier: TokenVerifier) { 9 | 10 | def authorized: Directive1[OAuth2Token] = { 11 | bearerToken.flatMap { 12 | case Some(token) => 13 | onComplete(tokenVerifier.verifyToken(token)).flatMap { 14 | _.map(username => provide(OAuth2Token(token, username))) 15 | .recover { 16 | case ex => 17 | logger.error(ex, "Couldn't log in using provided authorization token") 18 | reject(AuthorizationFailedRejection).toDirective[Tuple1[OAuth2Token]] 19 | } 20 | .get 21 | } 22 | case None => 23 | reject(AuthorizationFailedRejection) 24 | } 25 | } 26 | 27 | private def bearerToken: Directive1[Option[String]] = 28 | for { 29 | authBearerHeader <- optionalHeaderValueByType(classOf[Authorization]).map(extractBearerToken) 30 | xAuthCookie <- optionalCookie("X-Authorization-Token").map(_.map(_.value)) 31 | } yield authBearerHeader.orElse(xAuthCookie) 32 | 33 | private def extractBearerToken(authHeader: Option[Authorization]): Option[String] = 34 | authHeader.collect { 35 | case Authorization(OAuth2BearerToken(token)) => token 36 | } 37 | 38 | } 39 | 40 | case class OAuth2Token(token: String, username: String) -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function () { 4 | 5 | angular.module('KeycloakApp', ['ngCookies']); 6 | 7 | function initializeKeycloak() { 8 | var keycloakConfig = { 9 | "url": "http://localhost:8080/auth", 10 | "realm": "master", 11 | "clientId": "frontend", 12 | "credentials": { 13 | "secret": "1a1b98b6-66c5-4384-a4b4-7361717e773e" 14 | } 15 | }; 16 | var keycloak = Keycloak(keycloakConfig); 17 | keycloak.init({ 18 | onLoad: 'login-required' 19 | }).success(function () { 20 | keycloak.loadUserInfo().success(function (userInfo) { 21 | bootstrapAngular(keycloak, userInfo); 22 | }); 23 | }); 24 | } 25 | 26 | function bootstrapAngular(keycloak, userInfo) { 27 | angular.module('KeycloakApp') 28 | .run(function ($rootScope, $http, $interval, $cookies) { 29 | var updateTokenInterval = $interval(function () { 30 | // refresh token if it's valid for less then 15 minutes 31 | keycloak.updateToken(15) 32 | .success(function (refreshed) { 33 | if (refreshed) { 34 | $cookies.put('X-Authorization-Token', keycloak.token); 35 | } 36 | }); 37 | }, 10000); 38 | 39 | $cookies.put('X-Authorization-Token', keycloak.token); 40 | 41 | $rootScope.userLogout = function () { 42 | $cookies.remove('X-Authorization-Token'); 43 | $interval.cancel(updateTokenInterval); 44 | keycloak.logout(); 45 | }; 46 | 47 | $rootScope.authData = {}; 48 | 49 | $http.jsonp("http://localhost:9000/test?callback=JSON_CALLBACK") 50 | .success(function (response) { 51 | $rootScope.authData.token = response.token; 52 | $rootScope.authData.username = response.username; 53 | }); 54 | }); 55 | 56 | angular.bootstrap(document, ['KeycloakApp']); 57 | } 58 | 59 | initializeKeycloak(); 60 | 61 | }()); 62 | -------------------------------------------------------------------------------- /client/keycloak.js: -------------------------------------------------------------------------------- 1 | (function( window, undefined ) { 2 | 3 | var Keycloak = function (config) { 4 | if (!(this instanceof Keycloak)) { 5 | return new Keycloak(config); 6 | } 7 | 8 | var kc = this; 9 | var adapter; 10 | var refreshQueue = []; 11 | 12 | var loginIframe = { 13 | enable: true, 14 | callbackMap: [], 15 | interval: 5 16 | }; 17 | 18 | kc.init = function (initOptions) { 19 | kc.authenticated = false; 20 | 21 | if (window.Cordova) { 22 | adapter = loadAdapter('cordova'); 23 | } else { 24 | adapter = loadAdapter(); 25 | } 26 | 27 | if (initOptions) { 28 | if (typeof initOptions.checkLoginIframe !== 'undefined') { 29 | loginIframe.enable = initOptions.checkLoginIframe; 30 | } 31 | 32 | if (initOptions.checkLoginIframeInterval) { 33 | loginIframe.interval = initOptions.checkLoginIframeInterval; 34 | } 35 | 36 | if (initOptions.onLoad === 'login-required') { 37 | kc.loginRequired = true; 38 | } 39 | } 40 | 41 | var promise = createPromise(); 42 | 43 | var initPromise = createPromise(); 44 | initPromise.promise.success(function() { 45 | kc.onReady && kc.onReady(kc.authenticated); 46 | promise.setSuccess(kc.authenticated); 47 | }).error(function() { 48 | promise.setError(); 49 | }); 50 | 51 | var configPromise = loadConfig(config); 52 | 53 | function onLoad() { 54 | var doLogin = function(prompt) { 55 | if (!prompt) { 56 | options.prompt = 'none'; 57 | } 58 | kc.login(options).success(function () { 59 | initPromise.setSuccess(); 60 | }).error(function () { 61 | initPromise.setError(); 62 | }); 63 | } 64 | 65 | var options = {}; 66 | switch (initOptions.onLoad) { 67 | case 'check-sso': 68 | if (loginIframe.enable) { 69 | setupCheckLoginIframe().success(function() { 70 | checkLoginIframe().success(function () { 71 | doLogin(false); 72 | }).error(function () { 73 | initPromise.setSuccess(); 74 | }); 75 | }); 76 | } else { 77 | doLogin(false); 78 | } 79 | break; 80 | case 'login-required': 81 | doLogin(true); 82 | break; 83 | default: 84 | throw 'Invalid value for onLoad'; 85 | } 86 | } 87 | 88 | function processInit() { 89 | var callback = parseCallback(window.location.href); 90 | 91 | if (callback) { 92 | setupCheckLoginIframe(); 93 | window.history.replaceState({}, null, callback.newUrl); 94 | processCallback(callback, initPromise); 95 | return; 96 | } else if (initOptions) { 97 | if (initOptions.token || initOptions.refreshToken) { 98 | setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken); 99 | 100 | if (loginIframe.enable) { 101 | setupCheckLoginIframe().success(function() { 102 | checkLoginIframe().success(function () { 103 | initPromise.setSuccess(); 104 | }).error(function () { 105 | if (initOptions.onLoad) { 106 | onLoad(); 107 | } 108 | }); 109 | }); 110 | } else { 111 | initPromise.setSuccess(); 112 | } 113 | } else if (initOptions.onLoad) { 114 | onLoad(); 115 | } 116 | } else { 117 | initPromise.setSuccess(); 118 | } 119 | } 120 | 121 | configPromise.success(processInit); 122 | configPromise.error(function() { 123 | promise.setError(); 124 | }); 125 | 126 | return promise.promise; 127 | } 128 | 129 | kc.login = function (options) { 130 | return adapter.login(options); 131 | } 132 | 133 | kc.createLoginUrl = function(options) { 134 | var state = createUUID(); 135 | 136 | var redirectUri = adapter.redirectUri(options); 137 | if (options && options.prompt) { 138 | redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'prompt=' + options.prompt; 139 | } 140 | 141 | sessionStorage.oauthState = JSON.stringify({ state: state, redirectUri: encodeURIComponent(redirectUri) }); 142 | 143 | var action = 'auth'; 144 | if (options && options.action == 'register') { 145 | action = 'registrations'; 146 | } 147 | 148 | var url = getRealmUrl() 149 | + '/protocol/openid-connect/' + action 150 | + '?client_id=' + encodeURIComponent(kc.clientId) 151 | + '&redirect_uri=' + encodeURIComponent(redirectUri) 152 | + '&state=' + encodeURIComponent(state) 153 | + '&response_type=code'; 154 | 155 | if (options && options.prompt) { 156 | url += '&prompt=' + options.prompt; 157 | } 158 | 159 | if (options && options.loginHint) { 160 | url += '&login_hint=' + options.loginHint; 161 | } 162 | 163 | if (options && options.idpHint) { 164 | url += '&kc_idp_hint=' + options.idpHint; 165 | } 166 | 167 | return url; 168 | } 169 | 170 | kc.logout = function(options) { 171 | return adapter.logout(options); 172 | } 173 | 174 | kc.createLogoutUrl = function(options) { 175 | var url = getRealmUrl() 176 | + '/protocol/openid-connect/logout' 177 | + '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options)); 178 | 179 | return url; 180 | } 181 | 182 | kc.createAccountUrl = function(options) { 183 | var url = getRealmUrl() 184 | + '/account' 185 | + '?referrer=' + encodeURIComponent(kc.clientId) 186 | + '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options)); 187 | 188 | return url; 189 | } 190 | 191 | kc.accountManagement = function() { 192 | return adapter.accountManagement(); 193 | } 194 | 195 | kc.hasRealmRole = function (role) { 196 | var access = kc.realmAccess; 197 | return !!access && access.roles.indexOf(role) >= 0; 198 | } 199 | 200 | kc.hasResourceRole = function(role, resource) { 201 | if (!kc.resourceAccess) { 202 | return false; 203 | } 204 | 205 | var access = kc.resourceAccess[resource || kc.clientId]; 206 | return !!access && access.roles.indexOf(role) >= 0; 207 | } 208 | 209 | kc.loadUserProfile = function() { 210 | var url = getRealmUrl() + '/account'; 211 | var req = new XMLHttpRequest(); 212 | req.open('GET', url, true); 213 | req.setRequestHeader('Accept', 'application/json'); 214 | req.setRequestHeader('Authorization', 'bearer ' + kc.token); 215 | 216 | var promise = createPromise(); 217 | 218 | req.onreadystatechange = function () { 219 | if (req.readyState == 4) { 220 | if (req.status == 200) { 221 | kc.profile = JSON.parse(req.responseText); 222 | promise.setSuccess(kc.profile); 223 | } else { 224 | promise.setError(); 225 | } 226 | } 227 | } 228 | 229 | req.send(); 230 | 231 | return promise.promise; 232 | } 233 | 234 | kc.loadUserInfo = function() { 235 | var url = getRealmUrl() + '/protocol/openid-connect/userinfo'; 236 | var req = new XMLHttpRequest(); 237 | req.open('GET', url, true); 238 | req.setRequestHeader('Accept', 'application/json'); 239 | req.setRequestHeader('Authorization', 'bearer ' + kc.token); 240 | 241 | var promise = createPromise(); 242 | 243 | req.onreadystatechange = function () { 244 | if (req.readyState == 4) { 245 | if (req.status == 200) { 246 | kc.userInfo = JSON.parse(req.responseText); 247 | promise.setSuccess(kc.userInfo); 248 | } else { 249 | promise.setError(); 250 | } 251 | } 252 | } 253 | 254 | req.send(); 255 | 256 | return promise.promise; 257 | } 258 | 259 | kc.isTokenExpired = function(minValidity) { 260 | if (!kc.tokenParsed || !kc.refreshToken) { 261 | throw 'Not authenticated'; 262 | } 263 | 264 | var expiresIn = kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew; 265 | if (minValidity) { 266 | expiresIn -= minValidity; 267 | } 268 | 269 | return expiresIn < 0; 270 | } 271 | 272 | kc.updateToken = function(minValidity) { 273 | var promise = createPromise(); 274 | 275 | if (!kc.tokenParsed || !kc.refreshToken) { 276 | promise.setError(); 277 | return promise.promise; 278 | } 279 | 280 | minValidity = minValidity || 5; 281 | 282 | var exec = function() { 283 | if (!kc.isTokenExpired(minValidity)) { 284 | promise.setSuccess(false); 285 | } else { 286 | var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken; 287 | var url = getRealmUrl() + '/protocol/openid-connect/token'; 288 | 289 | refreshQueue.push(promise); 290 | 291 | if (refreshQueue.length == 1) { 292 | var req = new XMLHttpRequest(); 293 | req.open('POST', url, true); 294 | req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 295 | 296 | if (kc.clientId && kc.clientSecret) { 297 | req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret)); 298 | } else { 299 | params += '&client_id=' + encodeURIComponent(kc.clientId); 300 | } 301 | 302 | var timeLocal = new Date().getTime(); 303 | 304 | req.onreadystatechange = function () { 305 | if (req.readyState == 4) { 306 | if (req.status == 200) { 307 | timeLocal = (timeLocal + new Date().getTime()) / 2; 308 | 309 | var tokenResponse = JSON.parse(req.responseText); 310 | setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token']); 311 | 312 | kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat; 313 | 314 | kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess(); 315 | for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) { 316 | p.setSuccess(true); 317 | } 318 | } else { 319 | kc.onAuthRefreshError && kc.onAuthRefreshError(); 320 | for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) { 321 | p.setError(true); 322 | } 323 | } 324 | } 325 | }; 326 | 327 | req.send(params); 328 | } 329 | } 330 | } 331 | 332 | if (loginIframe.enable) { 333 | var iframePromise = checkLoginIframe(); 334 | iframePromise.success(function() { 335 | exec(); 336 | }).error(function() { 337 | promise.setError(); 338 | }); 339 | } else { 340 | exec(); 341 | } 342 | 343 | return promise.promise; 344 | } 345 | 346 | kc.clearToken = function() { 347 | if (kc.token) { 348 | setToken(null, null, null); 349 | kc.onAuthLogout && kc.onAuthLogout(); 350 | if (kc.loginRequired) { 351 | kc.login(); 352 | } 353 | } 354 | } 355 | 356 | function getRealmUrl() { 357 | if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') { 358 | return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm); 359 | } else { 360 | return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm); 361 | } 362 | } 363 | 364 | function getOrigin() { 365 | if (!window.location.origin) { 366 | return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: ''); 367 | } else { 368 | return window.location.origin; 369 | } 370 | } 371 | 372 | function processCallback(oauth, promise) { 373 | var code = oauth.code; 374 | var error = oauth.error; 375 | var prompt = oauth.prompt; 376 | 377 | if (code) { 378 | var params = 'code=' + code + '&grant_type=authorization_code'; 379 | var url = getRealmUrl() + '/protocol/openid-connect/token'; 380 | 381 | var req = new XMLHttpRequest(); 382 | req.open('POST', url, true); 383 | req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 384 | 385 | if (kc.clientId && kc.clientSecret) { 386 | req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret)); 387 | } else { 388 | params += '&client_id=' + encodeURIComponent(kc.clientId); 389 | } 390 | 391 | params += '&redirect_uri=' + oauth.redirectUri; 392 | 393 | req.withCredentials = true; 394 | 395 | var timeLocal = new Date().getTime(); 396 | 397 | req.onreadystatechange = function() { 398 | if (req.readyState == 4) { 399 | if (req.status == 200) { 400 | timeLocal = (timeLocal + new Date().getTime()) / 2; 401 | 402 | var tokenResponse = JSON.parse(req.responseText); 403 | setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token']); 404 | 405 | kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat; 406 | 407 | kc.onAuthSuccess && kc.onAuthSuccess(); 408 | promise && promise.setSuccess(); 409 | } else { 410 | kc.onAuthError && kc.onAuthError(); 411 | promise && promise.setError(); 412 | } 413 | } 414 | }; 415 | 416 | req.send(params); 417 | } else if (error) { 418 | if (prompt != 'none') { 419 | kc.onAuthError && kc.onAuthError(); 420 | promise && promise.setError(); 421 | } else { 422 | promise && promise.setSuccess(); 423 | } 424 | } 425 | } 426 | 427 | function loadConfig(url) { 428 | var promise = createPromise(); 429 | var configUrl; 430 | 431 | if (!config) { 432 | configUrl = 'keycloak.json'; 433 | } else if (typeof config === 'string') { 434 | configUrl = config; 435 | } 436 | 437 | if (configUrl) { 438 | var req = new XMLHttpRequest(); 439 | req.open('GET', configUrl, true); 440 | req.setRequestHeader('Accept', 'application/json'); 441 | 442 | req.onreadystatechange = function () { 443 | if (req.readyState == 4) { 444 | if (req.status == 200) { 445 | var config = JSON.parse(req.responseText); 446 | 447 | kc.authServerUrl = config['auth-server-url']; 448 | kc.realm = config['realm']; 449 | kc.clientId = config['resource']; 450 | kc.clientSecret = (config['credentials'] || {})['secret']; 451 | 452 | promise.setSuccess(); 453 | } else { 454 | promise.setError(); 455 | } 456 | } 457 | }; 458 | 459 | req.send(); 460 | } else { 461 | if (!config['url']) { 462 | var scripts = document.getElementsByTagName('script'); 463 | for (var i = 0; i < scripts.length; i++) { 464 | if (scripts[i].src.match(/.*keycloak\.js/)) { 465 | config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js')); 466 | break; 467 | } 468 | } 469 | } 470 | 471 | if (!config.realm) { 472 | throw 'realm missing'; 473 | } 474 | 475 | if (!config.clientId) { 476 | throw 'clientId missing'; 477 | } 478 | 479 | kc.authServerUrl = config.url; 480 | kc.realm = config.realm; 481 | kc.clientId = config.clientId; 482 | kc.clientSecret = (config.credentials || {}).secret; 483 | 484 | promise.setSuccess(); 485 | } 486 | 487 | return promise.promise; 488 | } 489 | 490 | function setToken(token, refreshToken, idToken) { 491 | if (token) { 492 | kc.token = token; 493 | kc.tokenParsed = decodeToken(token); 494 | var sessionId = kc.realm + '/' + kc.tokenParsed.sub; 495 | if (kc.tokenParsed.session_state) { 496 | sessionId = sessionId + '/' + kc.tokenParsed.session_state; 497 | } 498 | kc.sessionId = sessionId; 499 | kc.authenticated = true; 500 | kc.subject = kc.tokenParsed.sub; 501 | kc.realmAccess = kc.tokenParsed.realm_access; 502 | kc.resourceAccess = kc.tokenParsed.resource_access; 503 | } else { 504 | delete kc.token; 505 | delete kc.tokenParsed; 506 | delete kc.subject; 507 | delete kc.realmAccess; 508 | delete kc.resourceAccess; 509 | 510 | kc.authenticated = false; 511 | } 512 | 513 | if (refreshToken) { 514 | kc.refreshToken = refreshToken; 515 | kc.refreshTokenParsed = decodeToken(refreshToken); 516 | } else { 517 | delete kc.refreshToken; 518 | delete kc.refreshTokenParsed; 519 | } 520 | 521 | if (idToken) { 522 | kc.idToken = idToken; 523 | kc.idTokenParsed = decodeToken(idToken); 524 | } else { 525 | delete kc.idToken; 526 | delete kc.idTokenParsed; 527 | } 528 | } 529 | 530 | function decodeToken(str) { 531 | str = str.split('.')[1]; 532 | 533 | str = str.replace('/-/g', '+'); 534 | str = str.replace('/_/g', '/'); 535 | switch (str.length % 4) 536 | { 537 | case 0: 538 | break; 539 | case 2: 540 | str += '=='; 541 | break; 542 | case 3: 543 | str += '='; 544 | break; 545 | default: 546 | throw 'Invalid token'; 547 | } 548 | 549 | str = (str + '===').slice(0, str.length + (str.length % 4)); 550 | str = str.replace(/-/g, '+').replace(/_/g, '/'); 551 | 552 | str = decodeURIComponent(escape(atob(str))); 553 | 554 | str = JSON.parse(str); 555 | return str; 556 | } 557 | 558 | function createUUID() { 559 | var s = []; 560 | var hexDigits = '0123456789abcdef'; 561 | for (var i = 0; i < 36; i++) { 562 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); 563 | } 564 | s[14] = '4'; 565 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); 566 | s[8] = s[13] = s[18] = s[23] = '-'; 567 | var uuid = s.join(''); 568 | return uuid; 569 | } 570 | 571 | kc.callback_id = 0; 572 | 573 | function createCallbackId() { 574 | var id = ''; 575 | return id; 576 | 577 | } 578 | 579 | function parseCallback(url) { 580 | if (url.indexOf('?') != -1) { 581 | var oauth = {}; 582 | 583 | oauth.newUrl = url.split('?')[0]; 584 | var paramString = url.split('?')[1]; 585 | var fragIndex = paramString.indexOf('#'); 586 | if (fragIndex != -1) { 587 | paramString = paramString.substring(0, fragIndex); 588 | } 589 | var params = paramString.split('&'); 590 | for (var i = 0; i < params.length; i++) { 591 | var p = params[i].split('='); 592 | switch (decodeURIComponent(p[0])) { 593 | case 'code': 594 | oauth.code = p[1]; 595 | break; 596 | case 'error': 597 | oauth.error = p[1]; 598 | break; 599 | case 'state': 600 | oauth.state = decodeURIComponent(p[1]); 601 | break; 602 | case 'redirect_fragment': 603 | oauth.fragment = decodeURIComponent(p[1]); 604 | break; 605 | case 'prompt': 606 | oauth.prompt = p[1]; 607 | break; 608 | default: 609 | oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + p[0] + '=' + p[1]; 610 | break; 611 | } 612 | } 613 | 614 | var sessionState = sessionStorage.oauthState && JSON.parse(sessionStorage.oauthState); 615 | 616 | if (sessionState && (oauth.code || oauth.error) && oauth.state && oauth.state == sessionState.state) { 617 | delete sessionStorage.oauthState; 618 | 619 | oauth.redirectUri = sessionState.redirectUri; 620 | 621 | if (oauth.fragment) { 622 | oauth.newUrl += '#' + oauth.fragment; 623 | } 624 | 625 | return oauth; 626 | } 627 | } 628 | } 629 | 630 | function createPromise() { 631 | var p = { 632 | setSuccess: function(result) { 633 | p.success = true; 634 | p.result = result; 635 | if (p.successCallback) { 636 | p.successCallback(result); 637 | } 638 | }, 639 | 640 | setError: function(result) { 641 | p.error = true; 642 | p.result = result; 643 | if (p.errorCallback) { 644 | p.errorCallback(result); 645 | } 646 | }, 647 | 648 | promise: { 649 | success: function(callback) { 650 | if (p.success) { 651 | callback(p.result); 652 | } else if (!p.error) { 653 | p.successCallback = callback; 654 | } 655 | return p.promise; 656 | }, 657 | error: function(callback) { 658 | if (p.error) { 659 | callback(p.result); 660 | } else if (!p.success) { 661 | p.errorCallback = callback; 662 | } 663 | return p.promise; 664 | } 665 | } 666 | } 667 | return p; 668 | } 669 | 670 | function setupCheckLoginIframe() { 671 | var promise = createPromise(); 672 | 673 | if (!loginIframe.enable) { 674 | promise.setSuccess(); 675 | return promise.promise; 676 | } 677 | 678 | if (loginIframe.iframe) { 679 | promise.setSuccess(); 680 | return promise.promise; 681 | } 682 | 683 | var iframe = document.createElement('iframe'); 684 | loginIframe.iframe = iframe; 685 | 686 | iframe.onload = function() { 687 | var realmUrl = getRealmUrl(); 688 | if (realmUrl.charAt(0) === '/') { 689 | loginIframe.iframeOrigin = getOrigin(); 690 | } else { 691 | loginIframe.iframeOrigin = realmUrl.substring(0, realmUrl.indexOf('/', 8)); 692 | } 693 | promise.setSuccess(); 694 | 695 | setTimeout(check, loginIframe.interval * 1000); 696 | } 697 | 698 | var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html?client_id=' + encodeURIComponent(kc.clientId) + '&origin=' + getOrigin(); 699 | iframe.setAttribute('src', src ); 700 | iframe.style.display = 'none'; 701 | document.body.appendChild(iframe); 702 | 703 | var messageCallback = function(event) { 704 | if (event.origin !== loginIframe.iframeOrigin) { 705 | return; 706 | } 707 | var data = JSON.parse(event.data); 708 | var promise = loginIframe.callbackMap[data.callbackId]; 709 | delete loginIframe.callbackMap[data.callbackId]; 710 | 711 | if ((!kc.sessionId || kc.sessionId == data.session) && data.loggedIn) { 712 | promise.setSuccess(); 713 | } else { 714 | kc.clearToken(); 715 | promise.setError(); 716 | } 717 | }; 718 | window.addEventListener('message', messageCallback, false); 719 | 720 | var check = function() { 721 | checkLoginIframe(); 722 | if (kc.token) { 723 | setTimeout(check, loginIframe.interval * 1000); 724 | } 725 | }; 726 | 727 | return promise.promise; 728 | } 729 | 730 | function checkLoginIframe() { 731 | var promise = createPromise(); 732 | 733 | if (loginIframe.iframe && loginIframe.iframeOrigin) { 734 | var msg = {}; 735 | msg.callbackId = createCallbackId(); 736 | loginIframe.callbackMap[msg.callbackId] = promise; 737 | var origin = loginIframe.iframeOrigin; 738 | loginIframe.iframe.contentWindow.postMessage(JSON.stringify(msg), origin); 739 | } else { 740 | promise.setSuccess(); 741 | } 742 | 743 | return promise.promise; 744 | } 745 | 746 | function loadAdapter(type) { 747 | if (!type || type == 'default') { 748 | return { 749 | login: function(options) { 750 | window.location.href = kc.createLoginUrl(options); 751 | return createPromise().promise; 752 | }, 753 | 754 | logout: function(options) { 755 | window.location.href = kc.createLogoutUrl(options); 756 | return createPromise().promise; 757 | }, 758 | 759 | accountManagement : function() { 760 | window.location.href = kc.createAccountUrl(); 761 | return createPromise().promise; 762 | }, 763 | 764 | redirectUri: function(options) { 765 | if (options && options.redirectUri) { 766 | return options.redirectUri; 767 | } else if (kc.redirectUri) { 768 | return kc.redirectUri; 769 | } else { 770 | var redirectUri = location.href; 771 | if (location.hash) { 772 | redirectUri = redirectUri.substring(0, location.href.indexOf('#')); 773 | redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1)); 774 | } 775 | return redirectUri; 776 | } 777 | } 778 | }; 779 | } 780 | 781 | if (type == 'cordova') { 782 | loginIframe.enable = false; 783 | 784 | return { 785 | login: function(options) { 786 | var promise = createPromise(); 787 | 788 | var o = 'location=no'; 789 | if (options && options.prompt == 'none') { 790 | o += ',hidden=yes'; 791 | } 792 | 793 | var loginUrl = kc.createLoginUrl(options); 794 | var ref = window.open(loginUrl, '_blank', o); 795 | 796 | var callback; 797 | var error; 798 | 799 | ref.addEventListener('loadstart', function(event) { 800 | if (event.url.indexOf('http://localhost') == 0) { 801 | callback = parseCallback(event.url); 802 | ref.close(); 803 | } 804 | }); 805 | 806 | ref.addEventListener('loaderror', function(event) { 807 | if (event.url.indexOf('http://localhost') != 0) { 808 | error = true; 809 | ref.close(); 810 | } 811 | }); 812 | 813 | ref.addEventListener('exit', function(event) { 814 | if (error || !callback) { 815 | promise.setError(); 816 | } else { 817 | processCallback(callback, promise); 818 | } 819 | }); 820 | 821 | return promise.promise; 822 | }, 823 | 824 | logout: function(options) { 825 | var promise = createPromise(); 826 | 827 | var logoutUrl = kc.createLogoutUrl(options); 828 | var ref = window.open(logoutUrl, '_blank', 'location=no,hidden=yes'); 829 | 830 | var error; 831 | 832 | ref.addEventListener('loadstart', function(event) { 833 | if (event.url.indexOf('http://localhost') == 0) { 834 | ref.close(); 835 | } 836 | }); 837 | 838 | ref.addEventListener('loaderror', function(event) { 839 | if (event.url.indexOf('http://localhost') != 0) { 840 | error = true; 841 | ref.close(); 842 | } 843 | }); 844 | 845 | ref.addEventListener('exit', function(event) { 846 | if (error) { 847 | promise.setError(); 848 | } else { 849 | kc.clearToken(); 850 | promise.setSuccess(); 851 | } 852 | }); 853 | 854 | return promise.promise; 855 | }, 856 | 857 | accountManagement : function() { 858 | var accountUrl = kc.createAccountUrl(); 859 | var ref = window.open(accountUrl, '_blank', 'location=no'); 860 | ref.addEventListener('loadstart', function(event) { 861 | if (event.url.indexOf('http://localhost') == 0) { 862 | ref.close(); 863 | } 864 | }); 865 | }, 866 | 867 | redirectUri: function(options) { 868 | return 'http://localhost'; 869 | } 870 | } 871 | } 872 | 873 | throw 'invalid adapter type: ' + type; 874 | } 875 | } 876 | 877 | if ( typeof module === "object" && module && typeof module.exports === "object" ) { 878 | module.exports = Keycloak; 879 | } else { 880 | window.Keycloak = Keycloak; 881 | 882 | if ( typeof define === "function" && define.amd ) { 883 | define( "keycloak", [], function () { return Keycloak; } ); 884 | } 885 | } 886 | })( window ); 887 | --------------------------------------------------------------------------------