keycloak-aplcache

javascript refresh token

2/24/2014 9:58:54 PM

Details

diff --git a/core/src/main/java/org/keycloak/AbstractOAuthClient.java b/core/src/main/java/org/keycloak/AbstractOAuthClient.java
index ccc5393..e3989e1 100755
--- a/core/src/main/java/org/keycloak/AbstractOAuthClient.java
+++ b/core/src/main/java/org/keycloak/AbstractOAuthClient.java
@@ -18,6 +18,7 @@ public class AbstractOAuthClient {
     protected KeyStore truststore;
     protected String authUrl;
     protected String codeUrl;
+    protected String refreshUrl;
     protected String scope;
     protected String stateCookieName = OAUTH_TOKEN_REQUEST_STATE;
     protected String stateCookiePath;
@@ -70,6 +71,14 @@ public class AbstractOAuthClient {
         this.codeUrl = codeUrl;
     }
 
+    public String getRefreshUrl() {
+        return refreshUrl;
+    }
+
+    public void setRefreshUrl(String refreshUrl) {
+        this.refreshUrl = refreshUrl;
+    }
+
     public String getScope() {
         return scope;
     }
diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml
index 23d2827..0537e1b 100755
--- a/docbook/reference/en/en-US/master.xml
+++ b/docbook/reference/en/en-US/master.xml
@@ -7,6 +7,7 @@
                 <!ENTITY OpenShift SYSTEM "modules/openshift.xml">
                 <!ENTITY AdapterConfig SYSTEM "modules/adapter-config.xml">
                 <!ENTITY JBossAdapter SYSTEM "modules/jboss-adapter.xml">
+                <!ENTITY JavascriptAdapter SYSTEM "modules/javascript-adapter.xml">
                 <!ENTITY SocialConfig SYSTEM "modules/social-config.xml">
                 <!ENTITY SocialFacebook SYSTEM "modules/social-facebook.xml">
                 <!ENTITY SocialGitHub SYSTEM "modules/social-github.xml">
@@ -63,6 +64,7 @@
         </para>
         &AdapterConfig;
         &JBossAdapter;
+        &JavascriptAdapter;
     </chapter>
 
     <chapter>
diff --git a/docbook/reference/en/en-US/modules/javascript-adapter.xml b/docbook/reference/en/en-US/modules/javascript-adapter.xml
new file mode 100755
index 0000000..45c41d1
--- /dev/null
+++ b/docbook/reference/en/en-US/modules/javascript-adapter.xml
@@ -0,0 +1,94 @@
+<section>
+    <title>Pure Client Javascript Adapter</title>
+    <para>
+        The Keycloak Server comes with a Javascript library you can use to secure pure HTML/Javascript applications.  It
+        works in the same way as other application adapters accept that your browser is driving the OAuth redirect protocol
+        rather than the server.
+    </para>
+    <para>
+        The
+        disadvantage of using this approach is that you end up having a non-confidential, public client.  This can be mitigated
+        by registering valid redirect URLs.  You are still vulnerable if somebody hijacks the IP/DNS name of your pure
+        HTML/Javascript application though.
+    </para>
+    <para>
+        To use this adapter, you first must load and initialize the keycloak javascript library into your application.
+<programlisting><![CDATA[
+<head>
+    <title>Customer View Page</title>
+    <script src="/auth/js/keycloak.js"></script>
+    <script>
+        var keycloak = Keycloak({
+            clientId: 'application-name',
+            clientSecret: '1234234-234234-234234-234234',
+            realm: 'demo',
+            onload: 'login-required'
+        });
+        keycloak.init();
+    </script>
+</head>
+]]></programlisting>
+    </para>
+    <para>
+        The above code will initialize the adapter and redirect you to your realm's login screen.  You must fill in the
+        appropriate <literal>clientId</literal>, <literal>clientSecret</literal>, and <literal>realm</literal> options
+        based on how you created your application in your realm through the admin console. The <literal>init()</literal>
+        method can also take a success and error callback function as parameters.
+    </para>
+    <para>
+        After you login, your application will be able to make REST calls using bearer token authentication.  Here's
+        an example pulled from the <literal>customer-portal-js</literal> example that comes with the distribution.
+<programlisting><![CDATA[
+<script>
+    var loadData = function () {
+        document.getElementById('username').innerText = keycloak.username;
+
+        var url = 'http://localhost:8080/database/customers';
+
+        var req = new XMLHttpRequest();
+        req.open('GET', url, true);
+        req.setRequestHeader('Accept', 'application/json');
+        req.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
+
+        req.onreadystatechange = function () {
+            if (req.readyState == 4) {
+                if (req.status == 200) {
+                    var users = JSON.parse(req.responseText);
+                    var html = '';
+                    for (var i = 0; i < users.length; i++) {
+                        html += '<p>' + users[i] + '</p>';
+                    }
+                    document.getElementById('customers').innerHTML = html;
+                    console.log('finished loading data');
+                }
+            }
+        }
+
+        req.send();
+    };
+
+    var loadFailure = function () {
+        document.getElementById('customers').innerHTML = '<b>Failed to load data.  Check console log</b>';
+
+    };
+
+    var reloadData = function () {
+        keycloak.onValidAccessToken(loadData, loadFailure);
+    }
+</script>
+
+<button onclick="reloadData()">Submit</button>
+]]></programlisting>
+
+    </para>
+    <para>
+        The <literal>loadData()</literal> method builds an HTTP request setting the <literal>Authorization</literal>
+        header to a bearer token.  The <literal>keycloak.token</literal> points to the access token the browser obtained
+        when it logged you in.  The <literal>loadFailure()</literal> method is invoked on a failure.  The <literal>reloadData()</literal>
+        function calls <literal>keycloak.onValidAccessToken()</literal> passing in the <literal>loadData()</literal> and
+        <literal>loadFailure()</literal> callbacks.  The <literal>keycloak.onValidAcessToken()</literal> method checks to
+        see if the access token hasn't expired.  If it hasn't, and your oauth login returned a refresh token, this method
+        will refresh the access token.  Finally, if successful, it will invoke the success callback, which in this case
+        is the <literal>loadData()</literal> method.
+    </para>
+</section>
\ No newline at end of file
diff --git a/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml b/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml
index 8830a7f..bfc96eb 100755
--- a/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml
+++ b/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml
@@ -7,6 +7,14 @@
                 SkeletonKeyToken, SkeletonKeyScope, SkeletonKeyPrincipal, and SkeletonKeySession have been renamed to:
                 AccessToken, AccessScope, KeycloakPrincipal, and KeycloakAuthenticatedSession respectively.
             </listitem>
+            <listitem>
+                ServleOAuthClient.getBearerToken() method signature has changed.  It now returns an AccessTokenResponse
+                so that you can obtain a refresh token too.
+            </listitem>
+            <listitem>
+                Adapters now check the access token expiration with every request.  If the token is expired, they will
+                attempt to invoke a refresh on the auth server using a saved refresh token.
+            </listitem>
         </itemizedlist>
     </sect1>
     <sect1>
diff --git a/examples/demo-template/customer-app-js/src/main/webapp/customers/view.html b/examples/demo-template/customer-app-js/src/main/webapp/customers/view.html
index 2722702..36ef79f 100755
--- a/examples/demo-template/customer-app-js/src/main/webapp/customers/view.html
+++ b/examples/demo-template/customer-app-js/src/main/webapp/customers/view.html
@@ -12,13 +12,13 @@ User <b id="username"></b> made this request.
 
 <script>
     var keycloak = Keycloak({
-        clientId: '<INSERT CLIENT ID>',
-        clientSecret: '<INSERT SECRET>',
-        realm: '<INSERT REALM NAME>',
+        clientId: 'customer-portal',
+        clientSecret: 'password',
+        realm: 'demo',
         onload: 'login-required'
     });
 
-    keycloak.init(function() {
+    var loadData = function () {
         document.getElementById('username').innerText = keycloak.username;
 
         var url = 'http://localhost:8080/database/customers';
@@ -37,15 +37,29 @@ User <b id="username"></b> made this request.
                         html += '<p>' + users[i] + '</p>';
                     }
                     document.getElementById('customers').innerHTML = html;
+                    console.log('finished loading data');
                 }
             }
         }
 
         req.send();
-    });
+    };
+
+    var loadFailure = function () {
+        document.getElementById('customers').innerHTML = '<b>Failed to load data.  Check console log</b>';
+
+    };
+
+
+
+    var reloadData = function () {
+        keycloak.onValidAccessToken(loadData, loadFailure);
+    }
+    keycloak.init(loadData);
 
 </script>
 
 <br><br>
+<button onclick="reloadData()">Reload data</button>
 </body>
 </html>
\ No newline at end of file
diff --git a/examples/demo-template/database-service/src/main/java/org/keycloak/example/oauth/CustomerService.java b/examples/demo-template/database-service/src/main/java/org/keycloak/example/oauth/CustomerService.java
index 535e3fe..b1cf249 100755
--- a/examples/demo-template/database-service/src/main/java/org/keycloak/example/oauth/CustomerService.java
+++ b/examples/demo-template/database-service/src/main/java/org/keycloak/example/oauth/CustomerService.java
@@ -1,5 +1,7 @@
 package org.keycloak.example.oauth;
 
+import org.jboss.resteasy.annotations.cache.NoCache;
+
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
@@ -14,6 +16,7 @@ import java.util.List;
 public class CustomerService {
     @GET
     @Produces("application/json")
+    @NoCache
     public List<String> getCustomers() {
         ArrayList<String> rtn = new ArrayList<String>();
         rtn.add("Bill Burke");
diff --git a/examples/demo-template/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java b/examples/demo-template/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java
index fb5f620..10fd5d7 100755
--- a/examples/demo-template/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java
+++ b/examples/demo-template/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java
@@ -1,5 +1,7 @@
 package org.keycloak.example.oauth;
 
+import org.jboss.resteasy.annotations.cache.NoCache;
+
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
@@ -14,6 +16,7 @@ import java.util.List;
 public class ProductService {
     @GET
     @Produces("application/json")
+    @NoCache
     public List<String> getProducts() {
         ArrayList<String> rtn = new ArrayList<String>();
         rtn.add("iphone");
diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml
index ae1bb62..8185fb4 100755
--- a/examples/demo-template/pom.xml
+++ b/examples/demo-template/pom.xml
@@ -36,6 +36,7 @@
     <modules>
         <!-- <module>server</module> -->
         <module>customer-app</module>
+        <module>customer-app-js</module>
         <module>product-app</module>
         <module>database-service</module>
         <module>third-party</module>
diff --git a/examples/demo-template/README.md b/examples/demo-template/README.md
index 2d36e7c..5d5fba7 100755
--- a/examples/demo-template/README.md
+++ b/examples/demo-template/README.md
@@ -10,6 +10,7 @@ The following examples requires Wildfly 8.0.0, JBoss EAP 6.x, or JBoss AS 7.1.1.
 There are multiple WAR projects.  These will all run on the same WildFly instance, but pretend each one is running on a different
 machine on the network or Internet.
 * **customer-app** A WAR application that does remote login using OAuth2 browser redirects with the auth server
+* **customer-app-js** A pure HTML/Javascript application that does remote login using OAuth2 browser redirects with the auth server
 * **product-app** A WAR application that does remote login using OAuth2 browser redirects with the auth server
 * **database-service** JAX-RS services authenticated by bearer tokens only. The customer and product app invoke on it to get data
 * **third-party** Simple WAR that obtain a bearer token using OAuth2 using browser redirects to the auth-server.
@@ -146,6 +147,9 @@ are still happening, but the auth-server knows you are already logged in so the 
 
 If you click on the logout link of either of the product or customer app, you'll be logged out of all the applications.
 
+Ff you click on [http://localhost:8080/customer-portal-js](http://localhost:8080/customer-portal-js) you can invoke
+on the pure HTML/Javascript application.
+
 Step 6: Traditional OAuth2 Example
 ----------------------------------
 The customer and product apps are logins.  The third-party app is the traditional OAuth2 usecase of a client wanting
diff --git a/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java b/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java
index be32a39..9ab51b2 100755
--- a/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java
+++ b/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java
@@ -59,7 +59,7 @@ public class ProductDatabaseClient {
         ServletOAuthClient oAuthClient = (ServletOAuthClient) request.getServletContext().getAttribute(ServletOAuthClient.class.getName());
         String token = null;
         try {
-            token = oAuthClient.getBearerToken(request);
+            token = oAuthClient.getBearerToken(request).getToken();
         } catch (IOException e) {
             throw new RuntimeException(e);
         } catch (TokenGrantRequest.HttpFailure failure) {
diff --git a/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/RefreshTokenFilter.java b/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/RefreshTokenFilter.java
index 7467959..a1a71c2 100755
--- a/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/RefreshTokenFilter.java
+++ b/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/RefreshTokenFilter.java
@@ -43,7 +43,7 @@ public class RefreshTokenFilter implements Filter {
 
         if (reqParams.containsKey("code")) {
             try {
-                String accessToken = oauthClient.getBearerToken(request);
+                String accessToken = oauthClient.getBearerToken(request).getToken();
                 userData.setAccessToken(accessToken);
             } catch (TokenGrantRequest.HttpFailure e) {
                 throw new ServletException(e);
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/config/OAuthClientConfigLoader.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/config/OAuthClientConfigLoader.java
index 3e53fee..23fb072 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/config/OAuthClientConfigLoader.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/config/OAuthClientConfigLoader.java
@@ -35,8 +35,10 @@ public abstract class OAuthClientConfigLoader extends RealmConfigurationLoader {
         KeycloakUriBuilder serverBuilder = KeycloakUriBuilder.fromUri(adapterConfig.getAuthServerUrl());
         String authUrl = serverBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_LOGIN_PATH).build(adapterConfig.getRealm()).toString();
         String tokenUrl = serverBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_ACCESS_CODE_PATH).build(adapterConfig.getRealm()).toString();
+        String refreshUrl = serverBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_REFRESH_PATH).build(adapterConfig.getRealm()).toString();
         oauthClient.setAuthUrl(authUrl);
         oauthClient.setCodeUrl(tokenUrl);
+        oauthClient.setRefreshUrl(refreshUrl);
         oauthClient.setTruststore(truststore);
         if (adapterConfig.getScope() != null) {
             String scope = encodeScope(adapterConfig.getScope());
diff --git a/integration/js/src/main/resources/META-INF/resources/js/keycloak.js b/integration/js/src/main/resources/META-INF/resources/js/keycloak.js
index 077e852..5d4bf8c 100755
--- a/integration/js/src/main/resources/META-INF/resources/js/keycloak.js
+++ b/integration/js/src/main/resources/META-INF/resources/js/keycloak.js
@@ -101,6 +101,55 @@ var Keycloak = function (options) {
         req.send();
     }
 
+    /**
+     * checks to make sure token is valid.  If it is, it calls successCallback with no parameters.
+     * If it isn't valid, it tries to refresh the access token.  On successful refresh, it calls successCallback.
+     *
+     * @param successCallback
+     * @param errorCallback
+     */
+    this.onValidAccessToken = function(successCallback, errorCallback) {
+        if (!this.tokenParsed) {
+            console.log('no token');
+            errorCallback();
+            return;
+        }
+        var currTime = new Date().getTime() / 1000;
+        if (currTime > this.tokenParsed['exp']) {
+            if (!this.refreshToken) {
+                console.log('no refresh token');
+                errorCallback();
+                return;
+            }
+            console.log('calling refresh');
+            var params = 'grant_type=refresh_token&' + 'refresh_token=' + this.refreshToken;
+            var url = getRealmUrl() + '/tokens/refresh';
+
+            var req = new XMLHttpRequest();
+            req.open('POST', url, true, options.clientId, options.clientSecret);
+            req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+
+            req.onreadystatechange = function () {
+                if (req.readyState == 4) {
+                    if (req.status == 200) {
+                        console.log('Refresh Success');
+                        var tokenResponse = JSON.parse(req.responseText);
+                        this.refreshToken = tokenResponse['refresh_token'];
+                        setToken(tokenResponse['access_token'], successCallback);
+                    } else {
+                        console.log('error on refresh HTTP invoke: ' + req.status);
+                        errorCallback && errorCallback({ authenticated: false, status: req.status, statusText: req.statusText });
+                    }
+                }
+            };
+            req.send(params);
+        } else {
+            console.log('Token is still valid');
+            successCallback();
+        }
+
+    }
+
     function getRealmUrl() {
         return options.url + '/auth/rest/realms/' + encodeURIComponent(options.realm);
     }
@@ -121,7 +170,9 @@ var Keycloak = function (options) {
             req.onreadystatechange = function () {
                 if (req.readyState == 4) {
                     if (req.status == 200) {
-                        setToken(JSON.parse(req.responseText)['access_token'], successCallback);
+                        var tokenResponse = JSON.parse(req.responseText);
+                        instance.refreshToken = tokenResponse['refresh_token'];
+                        setToken(tokenResponse['access_token'], successCallback);
                     } else {
                         errorCallback && errorCallback({ authenticated: false, status: req.status, statusText: req.statusText });
                     }
diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
index 5d17c0a..44c0b9c 100755
--- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
+++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
@@ -4,6 +4,7 @@ import org.apache.http.client.HttpClient;
 import org.keycloak.AbstractOAuthClient;
 import org.keycloak.adapters.HttpClientBuilder;
 import org.keycloak.adapters.TokenGrantRequest;
+import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.util.KeycloakUriBuilder;
 
 import javax.servlet.http.Cookie;
@@ -45,8 +46,8 @@ public class ServletOAuthClient extends AbstractOAuthClient {
         this.client = client;
     }
 
-    public String resolveBearerToken(String redirectUri, String code) throws IOException, TokenGrantRequest.HttpFailure {
-        return TokenGrantRequest.invokeAccessCodeToToken(client, code, codeUrl, redirectUri, clientId, credentials).getToken();
+    public AccessTokenResponse resolveBearerToken(String redirectUri, String code) throws IOException, TokenGrantRequest.HttpFailure {
+        return TokenGrantRequest.invokeAccessCodeToToken(client, code, codeUrl, redirectUri, clientId, credentials);
     }
 
     /**
@@ -133,7 +134,7 @@ public class ServletOAuthClient extends AbstractOAuthClient {
      * @throws IOException
      * @throws org.keycloak.adapters.TokenGrantRequest.HttpFailure
      */
-    public String getBearerToken(HttpServletRequest request) throws IOException, TokenGrantRequest.HttpFailure {
+    public AccessTokenResponse getBearerToken(HttpServletRequest request) throws IOException, TokenGrantRequest.HttpFailure {
         String error = request.getParameter("error");
         if (error != null) throw new IOException("OAuth error: " + error);
         String redirectUri = request.getRequestURL().append("?").append(request.getQueryString()).toString();
@@ -151,5 +152,9 @@ public class ServletOAuthClient extends AbstractOAuthClient {
         return resolveBearerToken(redirectUri, code);
     }
 
+    public AccessTokenResponse refreshToken(String refreshToken) throws IOException, TokenGrantRequest.HttpFailure {
+        return TokenGrantRequest.invokeRefresh(client, refreshToken, refreshUrl, clientId, credentials);
+    }
+
 
 }
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 24d8090..44ac6e2 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -169,6 +169,7 @@ public class TokenService {
     @Produces(MediaType.APPLICATION_JSON)
     public Response refreshAccessToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader,
                                      final MultivaluedMap<String, String> form) {
+        logger.info("--> refreshAccessToken");
         if (!checkSsl()) {
             throw new NotAcceptableException("HTTPS required");
         }