keycloak-aplcache

Details

diff --git a/forms/common-themes/src/main/resources/theme/login/base/code.ftl b/forms/common-themes/src/main/resources/theme/login/base/code.ftl
new file mode 100755
index 0000000..43fdbe5
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/login/base/code.ftl
@@ -0,0 +1,19 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+    <#if section = "title">
+        <#if code.success>
+            Success code=${code.code}
+        <#else>
+            Error error=${code.error}
+        </#if>
+    <#elseif section = "form">
+        <div id="kc-code">
+            <#if code.success>
+                <p>Please copy this code and paste it into your application:</p>
+                <textarea id="code">${code.code}</textarea>
+            <#else>
+                <p>${code.error}</p>
+            </#if>
+        </div>
+    </#if>
+</@layout.registrationLayout>
diff --git a/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css b/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css
index 5de3439..6851d6c 100644
--- a/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css
+++ b/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css
@@ -121,6 +121,12 @@ ol#kc-totp-settings li:first-of-type {
     width: 50%;
 }
 
+/* Code */
+#kc-code textarea {
+    width: 100%;
+    height: 8em;
+}
+
 /* Social */
 
 #kc-social-providers ul {
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java b/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java
index e112755..2eb3704 100755
--- a/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java
+++ b/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java
@@ -27,6 +27,8 @@ public interface LoginForms {
 
     public Response createOAuthGrant();
 
+    public Response createCode();
+
     public LoginForms setAccessCode(String accessCodeId, String accessCode);
 
     public LoginForms setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested);
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java
index f0d1300..2b0cd23 100644
--- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java
+++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java
@@ -5,6 +5,6 @@ package org.keycloak.login;
  */
 public enum LoginFormsPages {
 
-    LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_USERNAME_REMINDER, REGISTER, ERROR, LOGIN_UPDATE_PROFILE;
+    LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, ERROR, LOGIN_UPDATE_PROFILE, CODE;
 
 }
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java
index b139aa5..69e17fc 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java
@@ -8,6 +8,7 @@ import org.keycloak.freemarker.Theme;
 import org.keycloak.freemarker.ThemeLoader;
 import org.keycloak.login.LoginForms;
 import org.keycloak.login.LoginFormsPages;
+import org.keycloak.login.freemarker.model.CodeBean;
 import org.keycloak.login.freemarker.model.LoginBean;
 import org.keycloak.login.freemarker.model.MessageBean;
 import org.keycloak.login.freemarker.model.OAuthGrantBean;
@@ -178,6 +179,9 @@ public class FreeMarkerLoginForms implements LoginForms {
             case OAUTH_GRANT:
                 attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested));
                 break;
+            case CODE:
+                attributes.put("code", new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null));
+                break;
         }
 
         try {
@@ -197,10 +201,6 @@ public class FreeMarkerLoginForms implements LoginForms {
         return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD);
     }
 
-    public Response createUsernameReminder() {
-        return createResponse(LoginFormsPages.LOGIN_USERNAME_REMINDER);
-    }
-
     public Response createLoginTotp() {
         return createResponse(LoginFormsPages.LOGIN_TOTP);
     }
@@ -218,6 +218,11 @@ public class FreeMarkerLoginForms implements LoginForms {
         return createResponse(LoginFormsPages.OAUTH_GRANT);
     }
 
+    @Override
+    public Response createCode() {
+        return createResponse(LoginFormsPages.CODE);
+    }
+
     public FreeMarkerLoginForms setError(String message) {
         this.message = message;
         this.messageType = MessageType.ERROR;
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/CodeBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/CodeBean.java
new file mode 100644
index 0000000..8851f9e
--- /dev/null
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/CodeBean.java
@@ -0,0 +1,27 @@
+package org.keycloak.login.freemarker.model;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class CodeBean {
+
+    private final String code;
+    private final String error;
+
+    public CodeBean(String code, String error) {
+        this.code = code;
+        this.error = error;
+    }
+
+    public boolean isSuccess() {
+        return code != null && error == null;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getError() {
+        return error;
+    }
+}
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java
index a57e8a3..02e20ec 100644
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java
@@ -29,6 +29,8 @@ public class Templates {
                 return "error.ftl";
             case LOGIN_UPDATE_PROFILE:
                 return "login-update-profile.ftl";
+            case CODE:
+                return "code.ftl";
             default:
                 throw new IllegalArgumentException();
         }
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 5d4bf8c..dd49966 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
@@ -5,7 +5,7 @@ var Keycloak = function (options) {
         return new Keycloak(options);
     }
 
-    var instance = this;
+    var kc = this;
 
     if (!options.url) {
         var scripts = document.getElementsByTagName('script');
@@ -33,7 +33,7 @@ var Keycloak = function (options) {
         throw 'clientSecret missing';
     }
 
-    this.init = function (successCallback, errorCallback) {
+    kc.init = function (successCallback, errorCallback) {
         if (window.oauth.callback) {
             delete sessionStorage.oauthToken;
             processCallback(successCallback, errorCallback);
@@ -44,50 +44,50 @@ var Keycloak = function (options) {
         } else if (options.onload) {
             switch (options.onload) {
                 case 'login-required' :
-                    window.location = createLoginUrl(true);
+                    window.location = kc.createLoginUrl(true);
                     break;
                 case 'check-sso' :
-                    window.location = createLoginUrl(false);
+                    window.location = kc.createLoginUrl(false);
                     break;
             }
         }
     }
 
-    this.login = function () {
-        window.location.href = createLoginUrl(true);
+    kc.login = function () {
+        window.location.href = kc.createLoginUrl(true);
     }
 
-    this.logout = function () {
+    kc.logout = function () {
         setToken(undefined);
-        window.location.href = createLogoutUrl();
+        window.location.href = kc.createLogoutUrl();
     }
 
-    this.hasRealmRole = function (role) {
-        var access = this.realmAccess;
+    kc.hasRealmRole = function (role) {
+        var access = kc.realmAccess;
         return access && access.roles.indexOf(role) >= 0 || false;
     }
 
-    this.hasResourceRole = function (role, resource) {
-        if (!this.resourceAccess) {
+    kc.hasResourceRole = function (role, resource) {
+        if (!kc.resourceAccess) {
             return false;
         }
 
-        var access = this.resourceAccess[resource || options.clientId];
+        var access = kc.resourceAccess[resource || options.clientId];
         return access && access.roles.indexOf(role) >= 0 || false;
     }
 
-    this.loadUserProfile = function (success, error) {
-        var url = getRealmUrl() + '/account';
+    kc.loadUserProfile = function (success, error) {
+        var url = kc.getRealmUrl() + '/account';
         var req = new XMLHttpRequest();
         req.open('GET', url, true);
         req.setRequestHeader('Accept', 'application/json');
-        req.setRequestHeader('Authorization', 'bearer ' + this.token);
+        req.setRequestHeader('Authorization', 'bearer ' + kc.token);
 
         req.onreadystatechange = function () {
             if (req.readyState == 4) {
                 if (req.status == 200) {
-                    instance.profile = JSON.parse(req.responseText);
-                    success && success(instance.profile)
+                    kc.profile = JSON.parse(req.responseText);
+                    success && success(kc.profile)
                 } else {
                     var response = { status: req.status, statusText: req.status };
                     if (req.responseText) {
@@ -108,22 +108,22 @@ var Keycloak = function (options) {
      * @param successCallback
      * @param errorCallback
      */
-    this.onValidAccessToken = function(successCallback, errorCallback) {
-        if (!this.tokenParsed) {
+    kc.onValidAccessToken = function(successCallback, errorCallback) {
+        if (!kc.tokenParsed) {
             console.log('no token');
             errorCallback();
             return;
         }
         var currTime = new Date().getTime() / 1000;
-        if (currTime > this.tokenParsed['exp']) {
-            if (!this.refreshToken) {
+        if (currTime > kc.tokenParsed['exp']) {
+            if (!kc.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 params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
+            var url = kc.getRealmUrl() + '/tokens/refresh';
 
             var req = new XMLHttpRequest();
             req.open('POST', url, true, options.clientId, options.clientSecret);
@@ -134,8 +134,8 @@ var Keycloak = function (options) {
                     if (req.status == 200) {
                         console.log('Refresh Success');
                         var tokenResponse = JSON.parse(req.responseText);
-                        this.refreshToken = tokenResponse['refresh_token'];
-                        setToken(tokenResponse['access_token'], successCallback);
+                        kc.refreshToken = tokenResponse['refresh_token'];
+                        kc.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 });
@@ -150,7 +150,7 @@ var Keycloak = function (options) {
 
     }
 
-    function getRealmUrl() {
+    kc.getRealmUrl = function() {
         return options.url + '/auth/rest/realms/' + encodeURIComponent(options.realm);
     }
 
@@ -161,7 +161,7 @@ var Keycloak = function (options) {
 
         if (code) {
             var params = 'code=' + code;
-            var url = getRealmUrl() + '/tokens/access/codes';
+            var url = kc.getRealmUrl() + '/tokens/access/codes';
 
             var req = new XMLHttpRequest();
             req.open('POST', url, true, options.clientId, options.clientSecret);
@@ -171,8 +171,8 @@ var Keycloak = function (options) {
                 if (req.readyState == 4) {
                     if (req.status == 200) {
                         var tokenResponse = JSON.parse(req.responseText);
-                        instance.refreshToken = tokenResponse['refresh_token'];
-                        setToken(tokenResponse['access_token'], successCallback);
+                        kc.refreshToken = tokenResponse['refresh_token'];
+                        kc.setToken(tokenResponse['access_token'], successCallback);
                     } else {
                         errorCallback && errorCallback({ authenticated: false, status: req.status, statusText: req.statusText });
                     }
@@ -189,33 +189,33 @@ var Keycloak = function (options) {
         }
     }
 
-    function setToken(token, successCallback) {
+    kc.setToken = function(token, successCallback) {
         if (token) {
             sessionStorage.oauthToken = token;
             window.oauth.token = token;
-            instance.token = token;
+            kc.token = token;
 
-            instance.tokenParsed = JSON.parse(atob(token.split('.')[1]));
-            instance.authenticated = true;
-            instance.username = instance.tokenParsed.sub;
-            instance.realmAccess = instance.tokenParsed.realm_access;
-            instance.resourceAccess = instance.tokenParsed.resource_access;
+            kc.tokenParsed = JSON.parse(atob(token.split('.')[1]));
+            kc.authenticated = true;
+            kc.username = kc.tokenParsed.sub;
+            kc.realmAccess = kc.tokenParsed.realm_access;
+            kc.resourceAccess = kc.tokenParsed.resource_access;
 
             setTimeout(function() {
-                successCallback && successCallback({ authenticated: instance.authenticated, username: instance.username });
+                successCallback && successCallback({ authenticated: kc.authenticated, username: kc.username });
             }, 0);
         } else {
             delete sessionStorage.oauthToken;
             delete window.oauth.token;
-            delete instance.token;
+            delete kc.token;
         }
     }
 
-    function createLoginUrl(prompt) {
+    kc.createLoginUrl = function(prompt) {
         var state = createUUID();
 
         sessionStorage.oauthState = state;
-        var url = getRealmUrl()
+        var url = kc.getRealmUrl()
             + '/tokens/login'
             + '?client_id=' + encodeURIComponent(options.clientId)
             + '&redirect_uri=' + getEncodedRedirectUri()
@@ -229,17 +229,22 @@ var Keycloak = function (options) {
         return url;
     }
 
-    function createLogoutUrl() {
-        var url = getRealmUrl()
+    kc.createLogoutUrl = function() {
+        var url = kc.getRealmUrl()
             + '/tokens/logout'
             + '?redirect_uri=' + getEncodedRedirectUri();
         return url;
     }
 
     function getEncodedRedirectUri() {
-        var url = (location.protocol + '//' + location.hostname + (location.port && (':' + location.port)) + location.pathname);
-        if (location.hash) {
-            url += '?redirect_fragment=' + encodeURIComponent(location.hash.substring(1));
+        var url;
+        if (options.redirectUri) {
+            url = options.redirectUri;
+        } else {
+            url = (location.protocol + '//' + location.hostname + (location.port && (':' + location.port)) + location.pathname);
+            if (location.hash) {
+                url += '?redirect_fragment=' + encodeURIComponent(location.hash.substring(1));
+            }
         }
         return encodeURI(url);
     }
diff --git a/model/api/src/main/java/org/keycloak/models/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java
index d243bd1..0630397 100755
--- a/model/api/src/main/java/org/keycloak/models/Constants.java
+++ b/model/api/src/main/java/org/keycloak/models/Constants.java
@@ -11,4 +11,6 @@ public interface Constants {
     String INTERNAL_ROLE = "KEYCLOAK_";
 
     String ACCOUNT_MANAGEMENT_APP = "account";
+
+    String INSTALLED_APP_URN = "urn:ietf:wg:oauth:2.0:oob";
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
index 38d00e2..eeb6674 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
@@ -38,6 +38,7 @@ import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.TokenManager;
 import org.keycloak.services.resources.TokenService;
 
+import javax.ws.rs.Path;
 import javax.ws.rs.core.Cookie;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
@@ -79,24 +80,32 @@ public class OAuthFlows {
 
     public Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect, boolean rememberMe) {
         String code = accessCode.getCode();
-        UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", code);
-        log.debug("redirectAccessCode: state: {0}", state);
-        if (state != null)
-            redirectUri.queryParam("state", state);
-        Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
-        Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
-        rememberMe = rememberMe || remember != null;
-        location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), uriInfo, rememberMe));
-        return location.build();
+
+        if (Constants.INSTALLED_APP_URN.equals(redirect)) {
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), code).createCode();
+        } else {
+            UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", code);
+            log.debug("redirectAccessCode: state: {0}", state);
+            if (state != null)
+                redirectUri.queryParam("state", state);
+            Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
+            Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
+            rememberMe = rememberMe || remember != null;
+            location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), uriInfo, rememberMe));
+            return location.build();
+        }
     }
 
     public Response redirectError(ClientModel client, String error, String state, String redirect) {
-        UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("error", error);
-        if (state != null) {
-            redirectUri.queryParam("state", state);
+        if (Constants.INSTALLED_APP_URN.equals(redirect)) {
+            return Flows.forms(realm, request, uriInfo).setError(error).createCode();
+        } else {
+            UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("error", error);
+            if (state != null) {
+                redirectUri.queryParam("state", state);
+            }
+            return Response.status(302).location(redirectUri.build()).build();
         }
-
-        return Response.status(302).location(redirectUri.build()).build();
     }
 
     public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
index 620ecad..4e1f043 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
@@ -26,8 +26,8 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.Constants;
 import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.OAuthClient;
 import org.keycloak.testsuite.OAuthClient.AuthorizationCodeResponse;
@@ -36,6 +36,7 @@ import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.rule.KeycloakRule;
 import org.keycloak.testsuite.rule.WebResource;
 import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 
 import java.io.IOException;
@@ -73,6 +74,21 @@ public class AuthorizationCodeTest {
         Assert.assertNotNull(response.getCode());
         Assert.assertEquals("mystate", response.getState());
         Assert.assertNull(response.getError());
+
+        oauth.verifyCode(response.getCode());
+    }
+
+    @Test
+    public void authorizationRequestInstalledApp() throws IOException {
+        oauth.redirectUri(Constants.INSTALLED_APP_URN);
+
+        oauth.doLogin("test-user@localhost", "password");
+
+        String title = driver.getTitle();
+        Assert.assertTrue(title.startsWith("Success code="));
+
+        String code = driver.findElement(By.id("code")).getText();
+        oauth.verifyCode(code);
     }
 
     @Test
@@ -94,6 +110,8 @@ public class AuthorizationCodeTest {
 
         Assert.assertTrue(response.isRedirected());
         Assert.assertNotNull(response.getCode());
+
+        oauth.verifyCode(response.getCode());
     }
 
     @Test
@@ -104,6 +122,8 @@ public class AuthorizationCodeTest {
         Assert.assertNotNull(response.getCode());
         Assert.assertNull(response.getState());
         Assert.assertNull(response.getError());
+
+        oauth.verifyCode(response.getCode());
     }
 
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
index 8d9b742..18fb97d 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
@@ -38,6 +38,8 @@ import org.json.JSONObject;
 import org.junit.Assert;
 import org.keycloak.RSATokenVerifier;
 import org.keycloak.VerificationException;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.crypto.RSAProvider;
 import org.keycloak.representations.AccessScope;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.idm.UserRepresentation;
@@ -156,6 +158,12 @@ public class OAuthClient {
         }
     }
 
+    public void verifyCode(String code) {
+        if (!RSAProvider.verify(new JWSInput(code), realmPublicKey)) {
+            throw new RuntimeException("Failed to verify code");
+        }
+    }
+
     public String getClientId() {
         return clientId;
     }