From 5b5fea347afd1134120eae51f75d602a10668ea3 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 5 Aug 2014 19:36:19 -0400 Subject: [PATCH] improved oauth login/grant --- .../theme/login/base/login-oauth-grant.ftl | 8 ++--- .../main/resources/theme/login/base/login.ftl | 12 ++++++-- .../login/base/messages/messages.properties | 9 ++++-- .../FreeMarkerLoginFormsProvider.java | 5 ++++ .../login/freemarker/model/ClientBean.java | 29 +++++++++++++++++++ .../freemarker/model/OAuthGrantBean.java | 2 -- .../services/resources/AccountService.java | 2 +- .../resources/RequiredActionsService.java | 20 ++++++------- .../services/resources/SocialResource.java | 14 ++++----- .../services/resources/TokenService.java | 22 +++++++------- .../services/resources/flows/Flows.java | 5 ++-- .../services/resources/flows/OAuthFlows.java | 6 ++-- 12 files changed, 90 insertions(+), 44 deletions(-) create mode 100755 forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl index a44b2c7dc5..e8cfbf3524 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl @@ -4,10 +4,10 @@ <#if section = "title"> ${rb.oauthGrantTitle} <#elseif section = "header"> - ${rb.oauthGrantTitleHtml} + Temporary access for ${(realm.name)!''} requested by ${(client.clientId)!''}. <#elseif section = "form">
-

${oauth.client} ${rb.oauthGrantRequest}

+

${rb.oauthGrantRequest}

diff --git a/forms/common-themes/src/main/resources/theme/login/base/login.ftl b/forms/common-themes/src/main/resources/theme/login/base/login.ftl index 9bc157009e..6fd5808e29 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login.ftl @@ -1,9 +1,17 @@ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=social.displayInfo; section> <#if section = "title"> - ${rb.loginTitle} ${realm.name} + <#if client.application> + ${rb.loginTitle} ${realm.name} + <#elseif client.oauthClient> + ${realm.name} ${rb.loginOauthTitle} + <#elseif section = "header"> - ${rb.loginTitle} ${(realm.name)!''} + <#if client.application> + ${rb.loginTitle} ${(realm.name)!''} + <#elseif client.oauthClient> + Temporary access for ${(realm.name)!''} requested by ${(client.clientId)!''}. + <#elseif section = "form"> <#if realm.password>
diff --git a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties index 5bac22e986..e7a0eb9cc3 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties +++ b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties @@ -23,6 +23,8 @@ passwordNewConfirm=New Password confirmation cancel=Cancel accept=Accept submit=Submit +yes=Yes +no=No authenticatorCode=One-time-password clientCertificate=Client Certificate @@ -50,6 +52,8 @@ successTotpRemoved=Google authenticator removed. usernameExists=Username already exists loginTitle=Log in to +loginOauthTitle=Temporary access. +loginOauthTitleHtml=Temporary access requested. Login to grant access. loginForgot=Forgot loginTotpTitle=Google Authenticator Setup @@ -67,9 +71,10 @@ loginProfileError=Some required fields are empty or incorrect. loginProfileErrorSteps=Please correct the fields in red. oauthGrantTitle=OAuth Grant -oauthGrantTitleHtml=Keycloak Central Login +oauthGrantTitleHtml=Temporary access requested oauthGrantTerms=Keycloak Central Login and Google will use this information in accordance with their respective terms of service and privacy policies. -oauthGrantRequest=requests access to: +oauthGrantRequest=Do you grant these access privileges? +oauthGrantLoginRequest=Do you grant access? emailVerifyTitle=Email verification emailVerifyInstr=An email with instructions to verify your email address has been sent to you. diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index f0540addb6..fcabeb131d 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -10,6 +10,7 @@ import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.ThemeProvider; import org.keycloak.login.LoginFormsPages; import org.keycloak.login.LoginFormsProvider; +import org.keycloak.login.freemarker.model.ClientBean; import org.keycloak.login.freemarker.model.CodeBean; import org.keycloak.login.freemarker.model.LoginBean; import org.keycloak.login.freemarker.model.MessageBean; @@ -189,6 +190,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { attributes.put("url", new UrlBean(realm, theme, baseUri)); } + if (client != null) { + attributes.put("client", new ClientBean(client)); + } + attributes.put("login", new LoginBean(formData)); switch (page) { diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java new file mode 100755 index 0000000000..450776288c --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java @@ -0,0 +1,29 @@ +package org.keycloak.login.freemarker.model; + +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.OAuthClientModel; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientBean { + protected ClientModel client; + + public ClientBean(ClientModel client) { + this.client = client; + } + + public boolean isApplication() { + return client instanceof ApplicationModel; + } + + public boolean isOauthClient() { + return client instanceof OAuthClientModel; + } + + public String getClientId() { + return client.getClientId(); + } +} diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java index 664b25bfdc..a5551b064b 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java @@ -41,8 +41,6 @@ public class OAuthGrantBean { private String code; private ClientModel client; private List claimsRequested; - private String oAuthCode; - private String action; public OAuthGrantBean(String code, ClientModel client, List realmRolesRequested, MultivaluedMap resourceRolesRequested) { this.code = code; diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 17147fbd15..dca8ad1873 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -189,7 +189,7 @@ public class AccountService { try { require(AccountRoles.MANAGE_ACCOUNT); } catch (ForbiddenException e) { - return Flows.forms(session, realm, uriInfo).setError("No access").createErrorPage(); + return Flows.forms(session, realm, null, uriInfo).setError("No access").createErrorPage(); } String[] referrer = getReferrer(); diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java index 5d598e6c0b..4e33e75152 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -116,7 +116,7 @@ public class RequiredActionsService { String error = Validation.validateUpdateProfileForm(formData); if (error != null) { - return Flows.forms(session, realm, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE); + return Flows.forms(session, realm, null, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE); } user.setFirstName(formData.getFirst("firstName")); @@ -155,7 +155,7 @@ public class RequiredActionsService { String totp = formData.getFirst("totp"); String totpSecret = formData.getFirst("totpSecret"); - LoginFormsProvider loginForms = Flows.forms(session, realm, uriInfo).setUser(user); + LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo).setUser(user); if (Validation.isEmpty(totp)) { return loginForms.setError(Messages.MISSING_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP); } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { @@ -195,7 +195,7 @@ public class RequiredActionsService { String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); - LoginFormsProvider loginForms = Flows.forms(session, realm, uriInfo).setUser(user); + LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo).setUser(user); if (Validation.isEmpty(passwordNew)) { return loginForms.setError(Messages.MISSING_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD); } else if (!passwordNew.equals(passwordConfirm)) { @@ -251,7 +251,7 @@ public class RequiredActionsService { initAudit(accessCode); - return Flows.forms(session, realm, uriInfo).setAccessCode(accessCode.getCode()).setUser(accessCode.getUser()) + return Flows.forms(session, realm, null, uriInfo).setAccessCode(accessCode.getCode()).setUser(accessCode.getUser()) .createResponse(RequiredAction.VERIFY_EMAIL); } } @@ -265,9 +265,9 @@ public class RequiredActionsService { return unauthorized(); } - return Flows.forms(session, realm, uriInfo).setAccessCode(accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD); + return Flows.forms(session, realm, null, uriInfo).setAccessCode(accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD); } else { - return Flows.forms(session, realm, uriInfo).createPasswordReset(); + return Flows.forms(session, realm, null, uriInfo).createPasswordReset(); } } @@ -327,11 +327,11 @@ public class RequiredActionsService { audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getCodeId()).success(); } catch (EmailException e) { logger.error("Failed to send password reset email", e); - return Flows.forms(this.session, realm, uriInfo).setError("emailSendError").createErrorPage(); + return Flows.forms(this.session, realm, client, uriInfo).setError("emailSendError").createErrorPage(); } } - return Flows.forms(session, realm, uriInfo).setSuccess("emailSent").createPasswordReset(); + return Flows.forms(session, realm, client, uriInfo).setSuccess("emailSent").createPasswordReset(); } private AccessCode getAccessCodeEntry(RequiredAction requiredAction) { @@ -368,7 +368,7 @@ public class RequiredActionsService { Set requiredActions = user.getRequiredActions(); if (!requiredActions.isEmpty()) { accessCode.setRequiredAction(requiredActions.iterator().next()); - return Flows.forms(session, realm, uriInfo).setAccessCode(accessCode.getCode()).setUser(user) + return Flows.forms(session, realm, null, uriInfo).setAccessCode(accessCode.getCode()).setUser(user) .createResponse(requiredActions.iterator().next()); } else { logger.debugv("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri()); @@ -410,7 +410,7 @@ public class RequiredActionsService { } private Response unauthorized() { - return Flows.forms(session, realm, uriInfo).setError("Unauthorized request").createErrorPage(); + return Flows.forms(session, realm, null, uriInfo).setError("Unauthorized request").createErrorPage(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java index e9acd79288..84439e4cd1 100755 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -110,7 +110,7 @@ public class SocialResource { initialRequest = new JWSInput(encodedState).readJsonContent(State.class); } catch (Throwable t) { logger.warn("Invalid social callback", t); - return Flows.forms(session, null, uriInfo).setError("Unexpected callback").createErrorPage(); + return Flows.forms(session, null, null, uriInfo).setError("Unexpected callback").createErrorPage(); } SocialProvider provider = SocialLoader.load(initialRequest.getProvider()); @@ -174,7 +174,7 @@ public class SocialResource { queryParms.putSingle(OAuth2Constants.RESPONSE_TYPE, responseType); audit.error(Errors.REJECTED_BY_USER); - return Flows.forms(session, realm, uriInfo).setQueryParams(queryParms).setWarning("Access denied").createLogin(); + return Flows.forms(session, realm, client, uriInfo).setQueryParams(queryParms).setWarning("Access denied").createLogin(); } catch (SocialProviderException e) { logger.error("Failed to process social callback", e); return oauth.forwardToSecurityFailure("Failed to process social callback"); @@ -278,25 +278,25 @@ public class SocialResource { SocialProvider provider = SocialLoader.load(providerId); if (provider == null) { audit.error(Errors.SOCIAL_PROVIDER_NOT_FOUND); - return Flows.forms(session, realm, uriInfo).setError("Social provider not found").createErrorPage(); + return Flows.forms(session, realm, null, uriInfo).setError("Social provider not found").createErrorPage(); } ClientModel client = realm.findClient(clientId); if (client == null) { audit.error(Errors.CLIENT_NOT_FOUND); logger.warn("Unknown login requester: " + clientId); - return Flows.forms(session, realm, uriInfo).setError("Unknown login requester.").createErrorPage(); + return Flows.forms(session, realm, null, uriInfo).setError("Unknown login requester.").createErrorPage(); } if (!client.isEnabled()) { audit.error(Errors.CLIENT_DISABLED); logger.warn("Login requester not enabled."); - return Flows.forms(session, realm, uriInfo).setError("Login requester not enabled.").createErrorPage(); + return Flows.forms(session, realm, null, uriInfo).setError("Login requester not enabled.").createErrorPage(); } redirectUri = TokenService.verifyRedirectUri(uriInfo, redirectUri, realm, client); if (redirectUri == null) { audit.error(Errors.INVALID_REDIRECT_URI); - return Flows.forms(session, realm, uriInfo).setError("Invalid redirect_uri.").createErrorPage(); + return Flows.forms(session, realm, null, uriInfo).setError("Invalid redirect_uri.").createErrorPage(); } try { @@ -309,7 +309,7 @@ public class SocialResource { .redirectToSocialProvider(); } catch (Throwable t) { logger.error("Failed to redirect to social auth", t); - return Flows.forms(session, realm, uriInfo).setError("Failed to redirect to social auth").createErrorPage(); + return Flows.forms(session, realm, null, uriInfo).setError("Failed to redirect to social auth").createErrorPage(); } } 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 67d29bbb5f..a466c31160 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -538,18 +538,18 @@ public class TokenService { return oauth.processAccessCode(scopeParam, state, redirect, client, user, userSession, audit); case ACCOUNT_TEMPORARILY_DISABLED: audit.error(Errors.USER_TEMPORARILY_DISABLED); - return Flows.forms(this.session, realm, uriInfo).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).setFormData(formData).createLogin(); + return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).setFormData(formData).createLogin(); case ACCOUNT_DISABLED: audit.error(Errors.USER_DISABLED); - return Flows.forms(this.session, realm, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin(); + return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin(); case MISSING_TOTP: - return Flows.forms(this.session, realm, uriInfo).setFormData(formData).createLoginTotp(); + return Flows.forms(this.session, realm, client, uriInfo).setFormData(formData).createLoginTotp(); case INVALID_USER: audit.error(Errors.USER_NOT_FOUND); - return Flows.forms(this.session, realm, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); + return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); default: audit.error(Errors.INVALID_USER_CREDENTIALS); - return Flows.forms(this.session, realm, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); + return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); } } @@ -634,13 +634,13 @@ public class TokenService { if (error != null) { audit.error(Errors.INVALID_REGISTRATION); - return Flows.forms(session, realm, uriInfo).setError(error).setFormData(formData).createRegistration(); + return Flows.forms(session, realm, client, uriInfo).setError(error).setFormData(formData).createRegistration(); } // Validate that user with this username doesn't exist in realm or any authentication provider if (session.users().getUserByUsername(username, realm) != null) { audit.error(Errors.USERNAME_IN_USE); - return Flows.forms(session, realm, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration(); + return Flows.forms(session, realm, client, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration(); } UserModel user = session.users().addUser(realm, username); @@ -668,7 +668,7 @@ public class TokenService { // User already registered, but force him to update password if (!passwordUpdateSuccessful) { user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - return Flows.forms(session, realm, uriInfo).setError(passwordUpdateError).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); + return Flows.forms(session, realm, client, uriInfo).setError(passwordUpdateError).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); } } @@ -959,7 +959,7 @@ public class TokenService { return oauth.redirectError(client, "access_denied", state, redirect); } - LoginFormsProvider forms = Flows.forms(session, realm, uriInfo); + LoginFormsProvider forms = Flows.forms(session, realm, client, uriInfo); if (loginHint != null) { MultivaluedMap formData = new MultivaluedMapImpl(); @@ -1028,7 +1028,7 @@ public class TokenService { authManager.expireIdentityCookie(realm, uriInfo, clientConnection); - return Flows.forms(session, realm, uriInfo).createRegistration(); + return Flows.forms(session, realm, client, uriInfo).createRegistration(); } /** @@ -1150,7 +1150,7 @@ public class TokenService { @Path("oauth/oob") @GET public Response installedAppUrnCallback(final @QueryParam("code") String code, final @QueryParam("error") String error, final @QueryParam("error_description") String errorDescription) { - LoginFormsProvider forms = Flows.forms(session, realm, uriInfo); + LoginFormsProvider forms = Flows.forms(session, realm, null, uriInfo); if (code != null) { return forms.setAccessCode(code).createCode(); } else { diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java index 781edbcfdb..9765c56d10 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java @@ -24,6 +24,7 @@ package org.keycloak.services.resources.flows; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.AuthenticationManager; @@ -40,8 +41,8 @@ public class Flows { private Flows() { } - public static LoginFormsProvider forms(KeycloakSession session, RealmModel realm, UriInfo uriInfo) { - return session.getProvider(LoginFormsProvider.class).setRealm(realm).setUriInfo(uriInfo); + public static LoginFormsProvider forms(KeycloakSession session, RealmModel realm, ClientModel client, UriInfo uriInfo) { + return session.getProvider(LoginFormsProvider.class).setRealm(realm).setUriInfo(uriInfo).setClient(client); } public static OAuthFlows oauth(KeycloakSession session, RealmModel realm, HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection, AuthenticationManager authManager, 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 621bb1c99e..68bf394117 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 @@ -142,7 +142,7 @@ public class OAuthFlows { audit.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success(); } - return Flows.forms(this.session, realm, uriInfo).setAccessCode(accessCode.getCode()).setUser(user) + return Flows.forms(this.session, realm, client, uriInfo).setAccessCode(accessCode.getCode()).setUser(user) .createResponse(action); } @@ -159,7 +159,7 @@ public class OAuthFlows { } } - return Flows.forms(this.session, realm, uriInfo) + return Flows.forms(this.session, realm, client, uriInfo) .setAccessCode(accessCode.getCode()) .setAccessRequest(realmRoles, resourceRoles) .setClient(client) @@ -177,7 +177,7 @@ public class OAuthFlows { } public Response forwardToSecurityFailure(String message) { - return Flows.forms(session, realm, uriInfo).setError(message).createErrorPage(); + return Flows.forms(session, realm, null, uriInfo).setError(message).createErrorPage(); } private void isTotpConfigurationRequired(UserModel user) {