KEYCLOAK-25 Added flow with user registration with prefilled attributes from social

This commit is contained in:
mposolda 2013-08-20 12:42:19 +02:00
parent 932ed10c17
commit dd3c438e00
18 changed files with 329 additions and 101 deletions

View file

@ -6,7 +6,8 @@
"sslNotRequired": true,
"cookieLoginAllowed": true,
"registrationAllowed": true,
"automaticRegistrationAfterSocialLogin": true,
"social": true,
"automaticRegistrationAfterSocialLogin": false,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],

View file

@ -26,6 +26,7 @@ import java.util.Map;
import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;
@ -42,6 +43,8 @@ public class RegisterBean {
private HashMap<String, String> formData;
private boolean socialRegistration;
@PostConstruct
public void init() {
FacesContext ctx = FacesContext.getCurrentInstance();
@ -49,6 +52,9 @@ public class RegisterBean {
this.formData = new HashMap<String, String>();
Boolean socialRegistrationAttr = (Boolean)request.getAttribute(FormFlows.SOCIAL_REGISTRATION);
this.socialRegistration = socialRegistrationAttr != null && socialRegistrationAttr;
@SuppressWarnings("unchecked")
MultivaluedMap<String, String> formData = (MultivaluedMap<String, String>) request.getAttribute(FormFlows.DATA);
if (formData != null) {
@ -62,4 +68,8 @@ public class RegisterBean {
return formData;
}
public boolean isSocialRegistration() {
return socialRegistration;
}
}

View file

@ -46,6 +46,9 @@ public class SocialBean {
@ManagedProperty(value = "#{realm}")
private RealmBean realm;
@ManagedProperty(value = "#{register}")
private RegisterBean registerBean;
@ManagedProperty(value = "#{url}")
private UrlBean url;
@ -73,4 +76,32 @@ public class SocialBean {
return providers;
}
// Display panel with social providers just in case that social is enabled for realm, but we are not in the middle of registration with social
public boolean isDisplaySocialProviders() {
return realm.isSocial() && !registerBean.isSocialRegistration();
}
public RealmBean getRealm() {
return realm;
}
public void setRealm(RealmBean realm) {
this.realm = realm;
}
public UrlBean getUrl() {
return url;
}
public void setUrl(UrlBean url) {
this.url = url;
}
public RegisterBean getRegisterBean() {
return registerBean;
}
public void setRegisterBean(RegisterBean registerBean) {
this.registerBean = registerBean;
}
}

View file

@ -45,6 +45,9 @@ public class UrlBean {
@ManagedProperty(value = "#{realm}")
private RealmBean realm;
@ManagedProperty(value = "#{register}")
private RegisterBean registerBean;
@PostConstruct
public void init() {
FacesContext ctx = FacesContext.getCurrentInstance();
@ -64,6 +67,14 @@ public class UrlBean {
this.realm = realm;
}
public RegisterBean getRegisterBean() {
return registerBean;
}
public void setRegisterBean(RegisterBean registerBean) {
this.registerBean = registerBean;
}
public String getAccessUrl() {
return Urls.accountAccessPage(baseURI, realm.getId()).toString();
}
@ -98,7 +109,10 @@ public class UrlBean {
public String getRegistrationAction() {
if (realm.isSaas()) {
// TODO: saas social registration
return Urls.saasRegisterAction(baseURI).toString();
} else if (registerBean.isSocialRegistration()) {
return Urls.socialRegisterAction(baseURI, realm.getId()).toString();
} else {
return Urls.realmRegisterAction(baseURI, realm.getId()).toString();
}

View file

@ -39,6 +39,8 @@
</ui:define>
<ui:define name="info">
<h:panelGroup rendered="#{not register.socialRegistration}">
<p>#{messages.alreadyHaveAccount} <a href="#{url.loginUrl}">#{messages.logIn}</a>.</p>
</h:panelGroup>
</ui:define>
</ui:composition>

View file

@ -39,12 +39,12 @@ body {
</div>
</h:panelGroup>
<h:panelGroup rendered="#{realm.social}">
<h:panelGroup rendered="#{social.displaySocialProviders}">
<section class="social-login"> <span>or</span>
<h3>Social login area</h3>
<p>#{messages.logInWith}</p>
<ul>
<ui:repeat var="p" value="#{forms.providers}">
<ui:repeat var="p" value="#{social.providers}">
<li><a href="#{p.loginUrl}" class="zocial #{p.id}"> <span class="text">#{p.name}</span></a></li>
</ui:repeat>
</ul>

View file

@ -27,34 +27,48 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import javax.imageio.spi.ServiceRegistry;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.container.ResourceContext;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.SocialLinkModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.OAuthFlows;
import org.keycloak.services.resources.flows.PageFlows;
import org.keycloak.services.resources.flows.Urls;
import org.keycloak.social.AuthCallback;
import org.keycloak.social.AuthRequest;
import org.keycloak.social.RequestDetails;
import org.keycloak.social.RequestDetailsBuilder;
import org.keycloak.social.SocialConstants;
import org.keycloak.social.SocialProvider;
import org.keycloak.social.SocialProviderConfig;
import org.keycloak.social.SocialProviderException;
@ -78,6 +92,12 @@ public class SocialResource {
@Context
private HttpRequest request;
@Context
private HttpResponse response;
@Context
ResourceContext resourceContext;
private SocialRequestManager socialRequestManager;
private TokenManager tokenManager;
@ -147,7 +167,7 @@ public class SocialResource {
if (realm.isAutomaticRegistrationAfterSocialLogin()) {
if (realm.getUser(socialUser.getUsername()) != null) {
// TODO: Username is already in realm. Show message and let user to bind accounts
// TODO: Username is already in realm. Show message and let user to bind accounts after he re-authenticate
throw new IllegalStateException("Username " + socialUser.getUsername() +
" already registered in the realm. TODO: bind accounts...");
@ -155,6 +175,9 @@ public class SocialResource {
// already registered. But actually Keycloak allows duplicate emails
} else {
user = realm.addUser(socialUser.getUsername());
user.setFirstName(socialUser.getFirstName());
user.setLastName(socialUser.getLastName());
user.setEmail(socialUser.getEmail());
}
realm.addSocialLink(user, socialLink);
@ -163,7 +186,23 @@ public class SocialResource {
realm.grantRole(user, role);
}
} else {
// TODO: redirect to registration screen with pre-filled info
// Redirect user to registration screen with prefilled data from social provider
MultivaluedMap<String, String> formData = fillRegistrationFormWithSocialData(socialUser);
RequestDetailsBuilder reqDetailsBuilder = RequestDetailsBuilder.createFromRequestDetails(requestData);
reqDetailsBuilder.putSocialAttribute(SocialConstants.ATTR_SOCIAL_LINK, socialLink);
String requestId = UUID.randomUUID().toString();
socialRequestManager.addRequest(requestId, reqDetailsBuilder.build());
boolean secureOnly = !realm.isSslNotRequired();
String cookiePath = Urls.socialBase(uriInfo.getBaseUri()).build().getPath();
logger.info("creating cookie for social registration - name: " + SocialConstants.SOCIAL_REGISTRATION_COOKIE
+ " path: " + cookiePath);
NewCookie newCookie = new NewCookie(SocialConstants.SOCIAL_REGISTRATION_COOKIE, requestId,
cookiePath, null, "Added social cookie", NewCookie.DEFAULT_MAX_AGE, secureOnly);
response.addNewCookie(newCookie);
return Flows.forms(realm, request).setFormData(formData).setSocialRegistration(true).forwardToRegistration();
}
}
@ -213,6 +252,69 @@ public class SocialResource {
}
}
@POST
@Path("{realm}/socialRegistration")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response socialRegistration(@PathParam("realm") final String realmId,
final MultivaluedMap<String, String> formData) {
return new Transaction<Response>() {
protected Response callImpl() {
PageFlows pageFlows = Flows.pages(request);
Cookie cookie = headers.getCookies().get(SocialConstants.SOCIAL_REGISTRATION_COOKIE);
if (cookie == null) {
return pageFlows.forwardToSecurityFailure("Social registration cookie not found");
}
String requestId = cookie.getValue();
if (!socialRequestManager.isRequestId(requestId)) {
logger.error("Unknown requestId found in cookie. Maybe it's expired. requestId=" + requestId);
return pageFlows.forwardToSecurityFailure("Unknown requestId found in cookie. Maybe it's expired.");
}
RequestDetails requestData = socialRequestManager.getData(requestId);
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealm(realmId);
if (realm == null || !realm.isEnabled()) {
return pageFlows.forwardToSecurityFailure("Realm doesn't exists or is not enabled.");
}
TokenService tokenService = new TokenService(realm, tokenManager);
resourceContext.initResource(tokenService);
String clientId = requestData.getClientAttribute("clientId");
String scope = requestData.getClientAttribute("scope");
String state = requestData.getClientAttribute("state");
String redirectUri = requestData.getClientAttribute("redirectUri");
SocialLinkModel socialLink = (SocialLinkModel)requestData.getSocialAttribute(SocialConstants.ATTR_SOCIAL_LINK);
Response response1 = tokenService.processRegisterImpl(clientId, scope, state, redirectUri, formData, true);
// Some error occured during registration
if (response1 == null) {
return null;
}
String username = formData.getFirst("username");
UserModel user = realm.getUser(username);
if (user == null) {
// Normally shouldn't happen
throw new IllegalStateException("User " + username + " not found in the realm");
}
realm.addSocialLink(user, socialLink);
// Expire cookie and invalidate requestData
String cookiePath = Urls.socialBase(uriInfo.getBaseUri()).build().getPath();
NewCookie newCookie = new NewCookie(SocialConstants.SOCIAL_REGISTRATION_COOKIE, "", cookiePath, null,
"Expire social cookie", 0, false);
logger.info("Expiring social registration cookie: " + SocialConstants.SOCIAL_REGISTRATION_COOKIE + ", path: " + cookiePath);
response.addNewCookie(newCookie);
socialRequestManager.retrieveData(requestId);
return response1;
}
}.call();
}
private RequestDetails getRequestDetails(Map<String, String[]> queryParams) {
Iterator<SocialProvider> itr = ServiceRegistry.lookupProviders(SocialProvider.class);
@ -251,4 +353,25 @@ public class SocialResource {
return queryParams;
}
protected MultivaluedMap<String, String> fillRegistrationFormWithSocialData(SocialUser socialUser) {
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<String, String>();
formData.putSingle("username", socialUser.getUsername());
if (socialUser.getEmail() != null) {
formData.putSingle("email", socialUser.getEmail());
}
String fullName = null;
if (socialUser.getFirstName() == null) {
fullName = socialUser.getLastName();
} else if (socialUser.getLastName() == null) {
fullName = socialUser.getFirstName();
} else {
fullName = socialUser.getFirstName() + " " + socialUser.getLastName();
}
formData.putSingle("name", fullName);
return formData;
}
}

View file

@ -249,6 +249,13 @@ public class TokenService {
return new Transaction<Response>() {
@Override
protected Response callImpl() {
return processRegisterImpl(clientId, scopeParam, state, redirect, formData, false);
}
}.call();
}
public Response processRegisterImpl(String clientId, String scopeParam, String state, String redirect,
MultivaluedMap<String, String> formData, boolean isSocialRegistration) {
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!realm.isEnabled()) {
@ -274,7 +281,8 @@ public class TokenService {
String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes);
if (error != null) {
return Flows.forms(realm, request).setError(error).setFormData(formData).forwardToRegistration();
return Flows.forms(realm, request).setError(error).setFormData(formData)
.setSocialRegistration(isSocialRegistration).forwardToRegistration();
}
String username = formData.getFirst("username");
@ -282,7 +290,7 @@ public class TokenService {
UserModel user = realm.getUser(username);
if (user != null) {
return Flows.forms(realm, request).setError(Messages.USERNAME_EXISTS).setFormData(formData)
.forwardToRegistration();
.setSocialRegistration(isSocialRegistration).forwardToRegistration();
}
user = realm.addUser(username);
@ -326,8 +334,6 @@ public class TokenService {
return processLogin(clientId, scopeParam, state, redirect, formData);
}
}.call();
}
@Path("access/codes")
@POST

View file

@ -38,6 +38,7 @@ public class FormFlows {
public static final String ERROR_MESSAGE = "KEYCLOAK_FORMS_ERROR_MESSAGE";
public static final String REALM = Realm.class.getName();
public static final String USER = UserModel.class.getName();
public static final String SOCIAL_REGISTRATION = "socialRegistration";
private String error;
private MultivaluedMap<String, String> formData;
@ -47,6 +48,8 @@ public class FormFlows {
private HttpRequest request;
private UserModel userModel;
private boolean socialRegistration;
FormFlows(RealmModel realm, HttpRequest request) {
this.realm = realm;
this.request = request;
@ -75,6 +78,8 @@ public class FormFlows {
request.setAttribute(USER, userModel);
}
request.setAttribute(SOCIAL_REGISTRATION, socialRegistration);
request.forward(form);
return null;
}
@ -113,6 +118,12 @@ public class FormFlows {
return this;
}
// Set flag whether user registration is triggered from social login
public FormFlows setSocialRegistration(boolean socialRegistration) {
this.socialRegistration = socialRegistration;
return this;
}
public FormFlows setFormData(MultivaluedMap<String, String> formData) {
this.formData = formData;
return this;

View file

@ -100,7 +100,7 @@ public class Urls {
return saasBase(baseUri).path(SaasService.class, "registerPage").build();
}
private static UriBuilder socialBase(URI baseUri) {
public static UriBuilder socialBase(URI baseUri) {
return UriBuilder.fromUri(baseUri).path(SocialResource.class);
}
@ -116,4 +116,8 @@ public class Urls {
private static UriBuilder tokenBase(URI baseUri) {
return realmBase(baseUri).path(RealmsResource.class, "getTokenService");
}
public static URI socialRegisterAction(URI baseUri, String realmId) {
return socialBase(baseUri).path(SocialResource.class, "socialRegistration").build(realmId);
}
}

View file

@ -28,16 +28,16 @@ import java.util.Map;
*/
public class AuthCallback {
private Map<String, String> attributes;
private Map<String, Object> attributes;
private Map<String, String[]> queryParams;
public AuthCallback(Map<String, String> attributes, Map<String, String[]> queryParams) {
public AuthCallback(Map<String, Object> attributes, Map<String, String[]> queryParams) {
this.attributes = attributes;
this.queryParams = queryParams;
}
public String getAttribute(String name) {
public Object getAttribute(String name) {
return attributes.get(name);
}

View file

@ -33,9 +33,9 @@ public class AuthRequest {
private URI authUri;
private Map<String, String> attributes;
private Map<String, Object> attributes;
AuthRequest(String id, URI authUri, Map<String, String> attributes) {
AuthRequest(String id, URI authUri, Map<String, Object> attributes) {
this.id = id;
this.authUri = authUri;
this.attributes = attributes;
@ -49,7 +49,7 @@ public class AuthRequest {
return authUri;
}
public Map<String, String> getAttributes() {
public Map<String, Object> getAttributes() {
return attributes;
}

View file

@ -33,7 +33,7 @@ public class AuthRequestBuilder {
private UriBuilder b;
private Map<String, String> attributes;
private Map<String, Object> attributes;
private String id;
@ -44,7 +44,7 @@ public class AuthRequestBuilder {
AuthRequestBuilder req = new AuthRequestBuilder();
req.id = id;
req.b = UriBuilder.fromUri(path);
req.attributes = new HashMap<String, String>();
req.attributes = new HashMap<String, Object>();
return req;
}
@ -53,7 +53,7 @@ public class AuthRequestBuilder {
return this;
}
public AuthRequestBuilder setAttribute(String name, String value) {
public AuthRequestBuilder setAttribute(String name, Object value) {
attributes.put(name, value);
return this;
}

View file

@ -32,9 +32,9 @@ public class RequestDetails {
private Map<String, String> clientAttributes;
private Map<String, String> socialAttributes;
private Map<String, Object> socialAttributes;
RequestDetails(String providerId, Map<String, String> clientAttributes, Map<String, String> socialAttributes) {
RequestDetails(String providerId, Map<String, String> clientAttributes, Map<String, Object> socialAttributes) {
this.providerId = providerId;
this.clientAttributes = clientAttributes;
this.socialAttributes = socialAttributes;
@ -52,11 +52,11 @@ public class RequestDetails {
return clientAttributes;
}
public String getSocialAttribute(String name) {
public Object getSocialAttribute(String name) {
return socialAttributes.get(name);
}
public Map<String, String> getSocialAttributes() {
public Map<String, Object> getSocialAttributes() {
return socialAttributes;
}

View file

@ -33,7 +33,7 @@ public class RequestDetailsBuilder {
private Map<String, String> clientAttributes;
private Map<String, String> socialAttributes;
private Map<String, Object> socialAttributes;
private RequestDetailsBuilder() {
}
@ -42,7 +42,17 @@ public class RequestDetailsBuilder {
RequestDetailsBuilder req = new RequestDetailsBuilder();
req.providerId = providerId;
req.clientAttributes = new HashMap<String, String>();
req.socialAttributes = new HashMap<String, String>();
req.socialAttributes = new HashMap<String, Object>();
return req;
}
public static RequestDetailsBuilder createFromRequestDetails(RequestDetails from) {
RequestDetailsBuilder req = new RequestDetailsBuilder();
req.providerId = from.getProviderId();
req.clientAttributes = new HashMap<String, String>();
req.clientAttributes.putAll(from.getClientAttributes());
req.socialAttributes = new HashMap<String, Object>();
req.socialAttributes.putAll(from.getSocialAttributes());
return req;
}
@ -56,12 +66,12 @@ public class RequestDetailsBuilder {
return this;
}
public RequestDetailsBuilder putSocialAttribute(String name, String value) {
public RequestDetailsBuilder putSocialAttribute(String name, Object value) {
socialAttributes.put(name, value);
return this;
}
public RequestDetailsBuilder putSocialAttributes(Map<String, String> attributes) {
public RequestDetailsBuilder putSocialAttributes(Map<String, Object> attributes) {
socialAttributes.putAll(attributes);
return this;
}

View file

@ -0,0 +1,11 @@
package org.keycloak.social;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SocialConstants {
public static final String ATTR_SOCIAL_LINK = "ATTR_SOCIAL_LINK";
public static final String SOCIAL_REGISTRATION_COOKIE = "SOCIAL_REGISTRATION_COOKIE";
}

View file

@ -58,6 +58,11 @@ public class SocialRequestManager {
return details;
}
// Just obtain data without expiring it
public synchronized RequestDetails getData(String requestId) {
return map.get(requestId);
}
private void pruneExpired() {
long currentTime = System.currentTimeMillis();
Iterator<Entry<String, Long>> itr = expires.entrySet().iterator();

View file

@ -71,7 +71,7 @@ public class TwitterProvider implements SocialProvider {
twitter.setOAuthConsumer(config.getKey(), config.getSecret());
String verifier = callback.getQueryParam("oauth_verifier");
RequestToken requestToken = new RequestToken(callback.getAttribute("token"), callback.getAttribute("tokenSecret"));
RequestToken requestToken = new RequestToken((String)callback.getAttribute("token"), (String)callback.getAttribute("tokenSecret"));
twitter.getOAuthAccessToken(requestToken, verifier);
twitter4j.User twitterUser = twitter.verifyCredentials();