From 13ea0ee60937828c1a777385e84b3b78b40832a3 Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 9 Aug 2013 20:04:11 +0200 Subject: [PATCH] Added support for Facebook login --- examples/as7-eap-demo/server/pom.xml | 5 + social/facebook/pom.xml | 34 ++++ .../social/facebook/FacebookProvider.java | 145 ++++++++++++++++++ .../social/facebook/FacebookUser.java | 77 ++++++++++ .../org.keycloak.social.SocialProvider | 1 + social/pom.xml | 1 + 6 files changed, 263 insertions(+) create mode 100644 social/facebook/pom.xml create mode 100644 social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java create mode 100644 social/facebook/src/main/java/org/keycloak/social/facebook/FacebookUser.java create mode 100644 social/facebook/src/main/resources/META-INF/services/org.keycloak.social.SocialProvider diff --git a/examples/as7-eap-demo/server/pom.xml b/examples/as7-eap-demo/server/pom.xml index ee568447a67a..5cfb75b9b339 100755 --- a/examples/as7-eap-demo/server/pom.xml +++ b/examples/as7-eap-demo/server/pom.xml @@ -45,6 +45,11 @@ keycloak-social-twitter ${project.version} + + org.keycloak + keycloak-social-facebook + ${project.version} + org.keycloak keycloak-sdk-html diff --git a/social/facebook/pom.xml b/social/facebook/pom.xml new file mode 100644 index 000000000000..a2bec96836c3 --- /dev/null +++ b/social/facebook/pom.xml @@ -0,0 +1,34 @@ + + + keycloak-social-parent + org.keycloak + 1.0-alpha-1 + ../pom.xml + + 4.0.0 + jar + + keycloak-social-facebook + Keycloak Social Facebook + + + + + org.keycloak + keycloak-social-core + ${project.version} + provided + + + org.jboss.resteasy + resteasy-client + provided + + + org.codehaus.jackson + jackson-core-asl + provided + + + diff --git a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java new file mode 100644 index 000000000000..dbe42547c22e --- /dev/null +++ b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java @@ -0,0 +1,145 @@ +package org.keycloak.social.facebook; + +import java.net.URI; +import java.util.UUID; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.keycloak.social.AuthCallback; +import org.keycloak.social.AuthRequest; +import org.keycloak.social.AuthRequestBuilder; +import org.keycloak.social.SocialProvider; +import org.keycloak.social.SocialProviderConfig; +import org.keycloak.social.SocialProviderException; +import org.keycloak.social.SocialUser; + +/** + * Social provider for Facebook + * + * @author Marek Posolda + */ +public class FacebookProvider implements SocialProvider { + + private static final String AUTHENTICATION_ENDPOINT_URL = "https://graph.facebook.com/oauth/authorize"; + + private static final String ACCESS_TOKEN_ENDPOINT_URL = "https://graph.facebook.com/oauth/access_token"; + + private static final String PROFILE_ENDPOINT_URL = "https://graph.facebook.com/me"; + + private static final String DEFAULT_RESPONSE_TYPE = "code"; + + private static final String DEFAULT_SCOPE = "email"; + + @Override + public String getId() { + return "facebook"; + } + + @Override + public AuthRequest getAuthUrl(SocialProviderConfig config) throws SocialProviderException { + String state = UUID.randomUUID().toString(); + + AuthRequestBuilder b = AuthRequestBuilder.create(state, AUTHENTICATION_ENDPOINT_URL).setQueryParam("client_id", config.getKey()) + .setQueryParam("response_type", DEFAULT_RESPONSE_TYPE).setQueryParam("scope", DEFAULT_SCOPE) + .setQueryParam("redirect_uri", config.getCallbackUrl()).setQueryParam("state", state); + + b.setAttribute("state", state); + + return b.build(); + } + + @Override + public String getRequestIdParamName() { + return "state"; + } + + @Override + public String getName() { + return "Facebook"; + } + + @Override + public SocialUser processCallback(SocialProviderConfig config, AuthCallback callback) throws SocialProviderException { + String code = callback.getQueryParam(DEFAULT_RESPONSE_TYPE); + + try { + if (!callback.getQueryParam("state").equals(callback.getAttribute("state"))) { + throw new SocialProviderException("Invalid state"); + } + + ResteasyClient client = new ResteasyClientBuilder() + .hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY).build(); + + String accessToken = loadAccessToken(code, config, client); + + FacebookUser facebookUser = loadUser(accessToken, client); + + SocialUser socialUser = new SocialUser(facebookUser.getId()); + socialUser.setEmail(facebookUser.getEmail()); + socialUser.setLastName(facebookUser.getLastName()); + socialUser.setFirstName(facebookUser.getFirstName()); + + return socialUser; + } catch (SocialProviderException spe) { + throw spe; + } catch (Exception e) { + throw new SocialProviderException(e); + } + } + + protected String loadAccessToken(String code, SocialProviderConfig config, ResteasyClient client) throws SocialProviderException { + Form form = new Form(); + form.param("grant_type", "authorization_code") + .param("code", code) + .param("client_id", config.getKey()) + .param("client_secret", config.getSecret()) + .param("redirect_uri", config.getCallbackUrl()); + + Response response = client.target(ACCESS_TOKEN_ENDPOINT_URL).request().post(Entity.form(form)); + + if (response.getStatus() != 200) { + String errorTokenResponse = response.readEntity(String.class); + throw new SocialProviderException("Access token request to Facebook failed. Status: " + response.getStatus() + ", response: " + errorTokenResponse); + } + + String accessTokenResponse = response.readEntity(String.class); + return parseParameter(accessTokenResponse, "access_token"); + } + + protected FacebookUser loadUser(String accessToken, ResteasyClient client) throws SocialProviderException { + URI userDetailsUri = UriBuilder.fromUri(PROFILE_ENDPOINT_URL) + .queryParam("access_token", accessToken) + .queryParam("fields", "id,name,username,first_name,last_name,email") + .build(); + + Response response = client.target(userDetailsUri).request() + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .get(); + if (response.getStatus() != 200) { + String errorTokenResponse = response.readEntity(String.class); + throw new SocialProviderException("Request to Facebook for obtaining user failed. Status: " + response.getStatus() + ", response: " + errorTokenResponse); + } + + return response.readEntity(FacebookUser.class); + } + + // Parses value of given parameter from input string like "my_param=abcd&another_param=xyz" + private String parseParameter(String input, String paramName) { + int start = input.indexOf(paramName + "="); + if (start != -1) { + input = input.substring(start + paramName.length() + 1); + int end = input.indexOf("&"); + return end==-1 ? input : input.substring(0, end); + } else { + throw new IllegalArgumentException("Parameter " + paramName + " not available in response " + input); + } + + } +} diff --git a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookUser.java b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookUser.java new file mode 100644 index 000000000000..8a30fc7a0dde --- /dev/null +++ b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookUser.java @@ -0,0 +1,77 @@ +package org.keycloak.social.facebook; + +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * Wrap info about user from Facebook + * + * @author Marek Posolda + */ +public class FacebookUser { + + @JsonProperty("id") + private String id; + + @JsonProperty("first_name") + private String firstName; + + @JsonProperty("last_name") + private String lastName; + + @JsonProperty("username") + private String username; + + @JsonProperty("name") + private String name; + + @JsonProperty("email") + private String email; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/social/facebook/src/main/resources/META-INF/services/org.keycloak.social.SocialProvider b/social/facebook/src/main/resources/META-INF/services/org.keycloak.social.SocialProvider new file mode 100644 index 000000000000..5c147a510759 --- /dev/null +++ b/social/facebook/src/main/resources/META-INF/services/org.keycloak.social.SocialProvider @@ -0,0 +1 @@ +org.keycloak.social.facebook.FacebookProvider \ No newline at end of file diff --git a/social/pom.xml b/social/pom.xml index 9efd60c0e4f6..2041baecf29a 100755 --- a/social/pom.xml +++ b/social/pom.xml @@ -17,6 +17,7 @@ core google twitter + facebook