keycloak-memoizeit

Added support for Facebook login

8/9/2013 3:04:11 PM

Details

diff --git a/examples/as7-eap-demo/server/pom.xml b/examples/as7-eap-demo/server/pom.xml
index ee56844..5cfb75b 100755
--- a/examples/as7-eap-demo/server/pom.xml
+++ b/examples/as7-eap-demo/server/pom.xml
@@ -47,6 +47,11 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-social-facebook</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-sdk-html</artifactId>
             <version>${project.version}</version>
         </dependency>
diff --git a/social/facebook/pom.xml b/social/facebook/pom.xml
new file mode 100644
index 0000000..a2bec96
--- /dev/null
+++ b/social/facebook/pom.xml
@@ -0,0 +1,34 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>keycloak-social-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.0-alpha-1</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+
+    <artifactId>keycloak-social-facebook</artifactId>
+    <name>Keycloak Social Facebook</name>
+    <description/>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-social-core</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-client</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-core-asl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>
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 0000000..dbe4254
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 0000000..8a30fc7
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 0000000..5c147a5
--- /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

social/pom.xml 1(+1 -0)

diff --git a/social/pom.xml b/social/pom.xml
index 9efd60c..2041bae 100755
--- a/social/pom.xml
+++ b/social/pom.xml
@@ -17,6 +17,7 @@
         <module>core</module>
         <module>google</module>
         <module>twitter</module>
+        <module>facebook</module>
     </modules>
 
 </project>