From 87aaaf0b0697f720adad6cc2d1d472576e536c01 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 3 Mar 2014 09:28:38 +0000 Subject: [PATCH] Started support for installed applications --- .../main/resources/theme/login/base/code.ftl | 19 ++++ .../login/base/login-username-reminder.ftl | 33 ------- .../login/patternfly/resources/css/login.css | 6 ++ .../java/org/keycloak/login/LoginForms.java | 2 + .../org/keycloak/login/LoginFormsPages.java | 2 +- .../freemarker/FreeMarkerLoginForms.java | 13 ++- .../keycloak/login/freemarker/Templates.java | 2 + .../login/freemarker/model/CodeBean.java | 27 ++++++ .../META-INF/resources/js/keycloak.js | 97 ++++++++++--------- .../java/org/keycloak/models/Constants.java | 2 + .../services/resources/flows/OAuthFlows.java | 37 ++++--- .../org/keycloak/testsuite/OAuthClient.java | 8 ++ .../oauth/AuthorizationCodeTest.java | 22 ++++- 13 files changed, 171 insertions(+), 99 deletions(-) create mode 100755 forms/common-themes/src/main/resources/theme/login/base/code.ftl delete mode 100755 forms/common-themes/src/main/resources/theme/login/base/login-username-reminder.ftl create mode 100644 forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/CodeBean.java 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 0000000000..43fdbe593c --- /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} + + <#elseif section = "form"> +
+ <#if code.success> +

Please copy this code and paste it into your application:

+ + <#else> +

${code.error}

+ +
+ + diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-username-reminder.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-username-reminder.ftl deleted file mode 100755 index 90741fda12..0000000000 --- a/forms/common-themes/src/main/resources/theme/login/base/login-username-reminder.ftl +++ /dev/null @@ -1,33 +0,0 @@ -<#import "template.ftl" as layout> -<@layout.registrationLayout displayInfo=true; section> - <#if section = "title"> - ${rb.emailUsernameForgotHeader} - <#elseif section = "header"> - ${rb.emailUsernameForgotHeader} - <#elseif section = "form"> -
-
-
- -
-
- -
-
- -
- - -
- -
-
-
- <#elseif section = "info" > - ${rb.emailUsernameInstruction} - - \ No newline at end of file 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 5de3439431..6851d6c56a 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 e112755da7..2eb370418e 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 realmRolesRequested, MultivaluedMap 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 f0d1300c89..2b0cd23597 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 b139aa5785..69e17fcfd7 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/Templates.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java index a57e8a316f..02e20ec3b7 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/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 0000000000..8851f9eade --- /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 Stian Thorgersen + */ +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/integration/js/src/main/resources/META-INF/resources/js/keycloak.js b/integration/js/src/main/resources/META-INF/resources/js/keycloak.js index 5d4bf8c7b8..dd49966b8d 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 d243bd1b42..0630397295 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 38d00e25a4..eeb6674d66 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/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java index 8d9b742320..18fb97d30e 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; } 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 620ecadfbc..4e1f043304 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()); } }