├── 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 |
--------------------------------------------------------------------------------