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