Monthly Archives: Juni 2011

Teil 2: Eine Facebook App mit Grails bauen

Nach dem ich mich im ersten Teil mit einem Fan-Gate beschäftigt habe, geht es mir in diesem Teil um eine Facebook-App, die auf Benutzerdaten zugreift.

Zunächst einmal kann jede Facebook-App auf bestimmte Informationen eines Benutzers zugreifen, ohne dass sie eine Erlaubnis des Benutzers benötigt. Dazu gehören der Vorname, Nachname und ein Bild des Benutzers. Möchte die App mehr über den Nutzer wissen, muss sie um Erlaubnis fragen und angeben, auf welche Daten zugegriffen wird.

Weil ich plane mit personenbezogenen Daten zu Arbeiten, halte ich es für eine gute Idee einen Controller zu schreiben, der stets sicherstellt, dass der aktuelle Anwender a) bei Facebook angemeldet, er b) meiner App die notwendigen Rechte gegeben hat und c) meine App mit Facebook kommunizieren darf. Den Controller nenne ich UserAwareController. Der grundsätzliche Aufbau sieht wie folgt aus:

abstract class UserAwareController {
  def beforeInterceptor = [action: this.&loadCurrentUser]
  def fbClient, currentUser
 
  def getFacebookUser() {
    ...
  }
 
  def getAccessToken() {
    session.fb?.access_token
  }
 
  void setAccessToken(String token) {
    if (session.fb) { session.fb = [:] }
    session.fb.access_token = token
  }
 
  def getFacebookClient() {
    if (!fbClient) { fbClient = new DefaultFacebookClient(accessToken) }
    fbClient
  }
 
  protected def loadCurrentUser() {
    ...
  }
}

Wann immer ich eine Funktion umsetzen möchte, die einen Facebook Benutzer voraussetzt, leite ich von diesem Controller ab. Das wichtigste Instrument ist der Interceptor, der prüft, ob ein Access Token vorliegt und wenn ja – ist er noch gültig? (Um mit Facebook zu kommunizieren, nutze ich RestFB, eine sehr schöne Bibliothek, die auf der Graph-API aufbaut.)

  ...
  protected def loadCurrentUser() {
    // Wenn ein Access Token verfügbar ist, aber der Facebook Benutzer noch nicht geladen ist,
    // dann holen wir ihn, um zu testen, ob der Access Token gültig ist.
    if (accessToken && !currentUser) {
      try {
        currentUser = facebookClient.fetchObject("me", User.class)
      }
      catch(FacebookOAuthException e) {
        log.warn "Something is wrong with the current access_token. Delete it and request a new one..."
        // Etwas scheint mit dem Access Token nicht i.O. zu sein. Lösche ihn!
        setAccessToken(null)
      }
    }
 
    // Wenn kein Access Token verfügbar ist, dann führe eine Authentifizierung durch.
    if (!accessToken) {
      def returnUrl = createLink(controller: params.controller, action: params.action)
      redirect(controller: 'auth', action: 'authenticate', params: [redirectUrl: returnUrl])
      return false
    }
 
    return true
  }
  ...

Wenn also ein Access Token da ist und der Facebook Benutzer mit der Graph API gelesen werden kann, ist alles gut und der ableitende Controller kann mit dem Benutzer arbeiten. Damit ich mir merke, welche Facebook Benutzer ich bereits kenne, erstelle ich ein Domain Model und speichere ein paar eindeutige Informationen, wie die Facebook-ID in die eigene Datenbank.

  ...
  def getFacebookUser() {
    def facebookUser = FacebookUser.findByFacebookId(currentUser.id)
    if (!facebookUser) {
      facebookUser = new FacebookUser(facebookId: currentUser.id)
      if (facebookUser.save()) {
        log.info "New facebook user saved into database"
      }
      else {
        log.error "Could not create new facebook user. Reason = ${facebookUser.errors}"
      }
    }
 
    facebookUser
  }
  ...

Falls der Access Token aber doch mal ungültig sein sollte, dann wird der Nutzer zum AuthController weitergeleitet, der den Nutzer anmeldet. Die Anmeldung läuft immer gleich ab.

  1. Authentifizierung des Anwenders
  2. Erteilung der Berechtigungen für den Zugriff auf personenbezogene Daten
  3. Authentifizierung der App

Mehr dazu hier.

Wenn ein Nutzer aber bereits bei Facebook angemeldet ist, dann muss er sich nicht nochmal anmelden. Genauso verhält sich das mit der Erteilung der Rechte für die Nutzung seiner Daten. Einmal erteilt, bleibt eine erneute Nachfrage aus (es sei denn er entzieht der App die Rechte wieder).

Authentifizierung bei Facebook

Ich muss zugeben, dass ich eine echte Nuss knacken musste, bevor das alles so funktionierte, wie ich mir das vorgestellt habe.

Für die Anmeldung des Benutzers und der App, habe ich ein AuthController erstellt und den Algorithmus, der in den FB-Docs beschrieben ist, in der authenticate Methode implementiert:

/**
 * Authentifiziert den Anwender und diese Anwendung gegen Facebook
 */
class AuthController {
  ...
  AuthController() {
    if (!session.fb) { session.fb = [:] }
  }
 
  ...
  def authenticate = {
    // Soll nach der Autehntifizierung ein Redirect auf
    // eine bestimmte URL stattfinden?
    if (params.redirectUrl) {
      session.fb.redirectUrl = params.redirectUrl
    }
 
    // Wenn ein Fehler aufgetreten ist, dann zeigen wir
    // dem Benutzer eine Seite mit dem Problem
    if (params.error) {
      log.warn "Some errors occured during authentication: ${params.error}"
      processAuthError()
    }
 
    // Wenn wir kein Code bekommen haben, mit dem wir die App
    // authentifizieren können, beginnen wir mit der Anmeldung
    // des Benutzers...
    else if (!params.code) {
      log.info "authenticate user..."
      authenticateUser()
    }
    // Wir haben ein Code bekommen. Melden wir nun
    // die App an...
    else {
      log.info "authenticate app with code ${params.code}"
      authenticateApp()
 
      // Wenn wir uns zuvor eine Zieladresse gemerkt haben,
      // dann führen wir nun den Redirect durch und löschen
      // die gemerkte Adresse
      if (session.fb.redirectUrl) {
        redirect(url: session.fb.redirectUrl)
        session.fb.redirectUrl = null
      }
      // Keine Redirect-Adresse. Zeige Wurzel der Anwendung
      else {
        redirect(url: config.grails.serverURL)
      }
    }
  }
  ...
}

Der Ablauf gestaltet sich wie folgt:

  1. Beim ersten Aufruf der authenticate Methode merkt sich der Controller, an welche URL weitergeleitet werden soll, sobald die Anmeldung abgeschlossen ist. Dann startet er die Anmeldung des Benutzers. Methode authenticateUser
  2. Wenn die authenticate Methode zum zweiten mal aufgerufen wird, dann ist der Benutzer angemeldet und hat der App die notwendigen Rechte gegeben. Es wird ein code-Parameter mitgeliefert, mit dem nun die App authentifiziert werden kann. Methode authenticateApp
  3. Wenn keine Fehler aufgetreten sind, wird der Browser an die zuvor gemerkte URL umgeleitet.

Anmeldung des Benutzers

Schauen wir uns zunächst die Anmeldung des Benutzers näher an.

  ...
  private def authenticateUser() {
    // Facebook soll diesen Wert in seiner Antwort liefern,
    // um zu beweisen, dass es sich um Facebook handelt.
    session.state = session.id.encodeAsSHA1()
    def redirectParams = [
            // Die ID der App
            'client_id': config.facebook.app.id,
            // Die Umleitung erfolgt auf die Canvas URL von Facebook.
            'redirect_uri':  "${config.facebook.url.base}/${config.facebook.app.name}/auth/authenticate".encodeAsURL(),
            // Die Rechte, die diese App braucht.
            'scope': PERMISSIONS.join(','),
            'state': session.state
    ]
 
    def url = createOAuthUrl(redirectParams)
    log.info "redirect to ${url}"
    // Fix für http://bugs.developers.facebook.net/show_bug.cgi?id=11326
    render(template: "/shared/fb_redirect", model: [redirectUrl: url])
  }
  ...

Diese Funktion baut eine URL in der Form https://www.facebook.com/dialog/oauth?client_id=15***8&redirect_uri=CANVAS_URL/auth/authenticate&state=49***d6f&scope=email,offline_access zusammen. An dieser URL gibt es eine Besonderheit: der redirect_uri-Parameter. Aus den Docs geht nicht klar hervor, wie sich dieser URL zusammenzusetzen hat. Deshalb habe ich bisher angenommen, dass es die URL meines Servers ist. Das ist falsch.

Die URL muss diese Form haben: http://apps.facebook.com/CANVAS_PAGE. Wobei CANVAS_PAGE den Einstellungen der App entnommen werden muss. Wenn dass nicht gegeben ist, leitet Facebook nach der Anmeldung den Benutzer auf meinen Server weiter und lädt die Anwendung nicht wieder in ein iFrame.

Hänge ich außerdem weitere Parameter an die URL an, werden sie an meine App weitergegeben. So wie in diesem Fall /auth/authenticate den Aufruf der authenticate-Methode meines AuthControllers bewirkt.

Eine zweite Besonderheit ist das Rendern des Templates "/shared/fb_redirect", dem die zuvor erstellte URL übergeben wird. Ein einfaches Redirect funktioniert hier Aufgrund eines Facebook-Bugs nicht. Deshalb muss eine JavaScript-Krücke her:

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
  <body>
    <script type="text/javascript">top.location.href = "${redirectUrl}";</script>
  </body>
</html>

Hat der Benutzer sich angemeldet und der App die Rechte erteilt, ruft Facebook erneut die authenticate-Methode auf und übergibt einen code-Parameter mit dem nun die App authentifiziert wird. (Davon bekommt der Benutzer nichts mit)

  ...
  private def authenticateApp() {
    def code = params.code
    def urlParams = [
            // Die ID der App
            'client_id': config.facebook.app.id,
            // Der geheime Schlüssel der App
            'client_secret': config.facebook.app.secret,
            // Ist für unsere Zwecke nicht erforderlich, aber Facebook verlangt diesen Parameter
            'redirect_uri': "${config.facebook.url.base}/${config.facebook.app.name}/auth/authenticate".encodeAsURL(),
            // Der eben gelieferte Code
            'code': code
    ]
 
    def url = createAccessTokenUrl(urlParams)
    log.info "fetch resource: ${url}"
    // Authentifiziere die App und lese die Antwort von Facebook
    String result = fetchResource(url)
 
    // An dieser Stelle ist das Access Token verfügbar
    result.split(/&/).each { pair ->
      def (key, value) = pair.split('=')
      session['fb'][key] = value
    }
  }
  ...

An dieser Stelle ist der Benutzer angemeldet, hat der App Zugriffsrechte erteilt und auch die App ist für die Kommunikation mit Facebook bereit. Der Access Token ist in der Session (session.fb.access_token) gespeichert und kann genutzt werden.

Teil 1: Ein Facebook Fan-Gate/Reveal mit Grails bauen

Ok, auch wenn es einwenig nach Industriespionage klingt, ich habe eine Erlaubnis das hier zu schreiben. Soviel vorab 😉 Meine Aufgabe besteht die nächsten Tage darin für snaeckbox.de ein Fan-Gate für Facebook zu schreiben. Es gibt eine Menge Artikel im Netz, wie das mit PHP zu realisieren ist, aber ein direktes Beispiel in Grails habe ich nicht gefunden. Deshalb gebe ich mal meine Lösung wieder:

Schritt 0: Erstelle eine leere und lauffähige Grails Anwendung.

Ich arbeite mit Maven und habe mit folgenden Befehl zunächst eine neue Grails Anwendung erzeugt…

mvn archetype:generate -DarchetypeGroupId=org.grails -DarchetypeArtifactId=grails-maven-archetype -DarchetypeVersion=1.3.4 -DgroupId=DEINE_GROUP_ID -DartifactId=DEINE_ARTIFACT_ID

Ich gehe im Nachfolgenden davon aus, dass du lokal eine Grails Anwendung hast und sie auch starten kannst. In diesem Beispiel läuft meine Anwendung unter http://localhost:8080/sb-cms-fb/

Schritt 1: Facebook Anwendung erstellen

Ich habe als erstes bei Facebook eine Anwendung erstellt. Namen der App eingegeben, die Bestimmungen akzeptiert und dann zu den Einstellungen navigiert.

Während der Entwicklung wäre es natürlich quatsch für jeden Test meine Anwendung auf einen im Netz erreichbaren Server zu deployen. Facebook öffnet die App in einem iFrame und da spricht nichts dagegen, dass die Anwendung während der Entwicklung lokal gehostet wird. Deshalb soll die Website-> Site URL auf meine lokal laufende Anwendung zeigen.
bildschirmfoto-2011-06-16-um-100249


Unter dem Punkt Facebook Integration -> Canvas Page habe ich einen Bezeichner für meine Anwendung vergeben. Es dürfen nur Buchstaben, Unterstriche und Bindestriche verwendet werden. Diesen Bezeichnet hängt Facebook an die URL http://apps.facebook.com/MEIN_BEZEICHNER an. Der Name sollte also schon etwas sein, was ich der Welt da draußen auch zumuten möchte.

Unter Facebook Integration -> Canvas URL stelle ich vorerst ebenfalls die lokale Serveradresse ein. Später wird das die Wurzeladresse meiner Anwendung im Netz.
bildschirmfoto-2011-06-16-um-100313


Zu guter letzt aktiviere ich noch den Sandbox-Modus. Damit stelle ich sicher, dass während der Entwicklung außer mir, keiner der Facebook Benutzer Zugriff auf die Anwendung bekommt. Wer der Administrator bzw. der Entwickler ist, bestimme ich übrigens unter Info -> Manage Users
bildschirmfoto-2011-06-16-um-101046

Schritt 2: Verbindung testen

Theoretisch funktioniert das so. Ich starte einen Server, der lokal unter http://localhost:8080/sb-cms-fb/ erreichbar ist. Dann navigiere ich im Browser auf http://apps.facebook.com/MEIN_BEZEICHNER. Facebook erzeugt ein iFrame, dass auf meine lokale Anwendung zeigt, und stellt mir den Inhalt dar.

Um das Ganze sauber zu gestalten, erstelle ich einen FanGateController und verdrahte in den UrlMappings.groovy die Wurzel mit diesem Controller.

class UrlMappings {
  static mappings = {
    ...
    "/"(controller: 'fanGate')
    ...
}

Im Controller implementiere ich das index-Closure mit dem folgendem Inhalt:

package de.snaeckbox.fb
 
class FanGateController {
 def index = {
    render(text: params.signed_request ?: 'params.signed_request missing')
  }
}

Ich starte nun Grails und navigiere über die Facebook-URL auf meine Anwendung. Was ich zu sehen bekomme ist ein kryptischer String, der bei mir wie folgt aussieht: dNnxTShtVfI0o0-Fvk(...)taW4iOjIxfX19

Facebook schickt beim Aufruf der Anwendung Informationen mit, die von Bedeutung sind. Der s.g. Signed Request. Diese Daten sind aus Sicherheitsgründen verschlüsselt und müssen dekodiert werden. Um die Implementierung ernsthaft anzugehen, würde ich vorschlagen einen FacebookService zu implementieren, das diese Arbeit übernimmt. Und weil zu jeder Klasse auch eine Testklasse gehört, dient der gesendete String von Facebook als Testdaten. Also in die Zwischenablage kopieren, Facebook Service erstellen und als statische Variable merken…

Schritt 3: Den Signed Request dekodieren

In der Zusammenfassung der Anwendung unter Facebook sind Anwendungsnummer, API-Schlüssel, etc. dargestellt. Diese habe ich in der Config.groovy erstmal als Konfiguration hinterlegt, damit der FacebookService später drauf zugreifen kann:

facebook {
  api {
    id        = '7226********'
  }
  app {
    id        = '2367******'
    secret    = '7d01********'
    baseurl   = 'http://apps.facebook.com'
    name      = 'sna****'
  }
}

Wie man den Request in Groovy/Grails dekodiert habe ich bei Matthias Gall gefunden und den Algorithmus größtenteils übernommen.

import grails.converters.JSON
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
 
class FacebookService {
  def grailsAppliction
 
  /**
   * Dekodiert den signierten FB Request und liefert ein JSON Objekt mit den Daten.
   *
   * @param request Der Request
   * @return Die dekodierten Daten
   */
  def decodeSignedRequest(String request) {
    if (!request || request.empty) {
      throw new IllegalArgumentException("Could not decode facebook request. It's empty!")
    }
 
    def parts = request.split(/\./, 2 )
    def (signature, payload) = [parts[0], parts[1]]*.tr( '-_', '+/' )*.decodeBase64()
 
    def data = JSON.parse(new String(payload))
    if (data.algorithm != "HMAC-SHA256") {
      throw new IllegalArgumentException("Unknown signature request algorithm: ${data.algorithm}")
    }
 
    def secret = config.app.secret
    def expectedSignature = Mac.getInstance("HmacSHA256").with {
      init(new SecretKeySpec(secret.bytes, "HmacSHA256"))
      update(parts[1].bytes)
      doFinal()
    }
 
    if(signature != expectedSignature) {
      throw new IllegalArgumentException("Request does not seem to be from Facebook. Signature mismatch!")
    }
 
    return data
  }
 
  def getConfig() {
    grailsAppliction.config.facebook
  }
}

In dem FacebookServiceTests teste ich mit dem zuvor kopierten String, ob der Dekodierungsmechanismus auch wirklich funktioniert. Testdaten sind schließlich alles. Wichtig ist natürlich, dass du deine Anwendungsdaten einträgst und nicht meine übernimmst. Also den signierten String und den Anwendungs-Geheimcode in der gemockten Konfiguration (config.app.secret, siehe setUp()).

class FacebookServiceTests extends GrailsUnitTestCase {
  def static SIGNED_REQUEST = "dNnxTShtVfI0o*********aW4iOjIxfX19"
  def service
 
  protected void setUp() {
    super.setUp()
    service = new FacebookService()
 
    service.metaClass.getConfig = { ->
      def config = new ConfigObject()
      config.app.secret  = '7d014***'
      return config
    }
  }
 
  void testDecodeSignedRequest() {
    def data = service.decodeSignedRequest(SIGNED_REQUEST)
    assertEquals(1308210538, data.issued_at)
    assertEquals(21, data.user.age.min)
    assertEquals("de_DE", data.user.locale)
    assertEquals("de", data.user.country)
  }
}

Natürlich wird der Test bei dir fehlschlagen, denn ich überprüfe im Test einen Zeitstempel, der von deinem abweichen wird und evtl. auch die Sprache. In dem Fall ist der Debugger dein Freund 🙂

Schritt 4: Test, ob ein „Like“ bereits vorliegt, oder nicht

bildschirmfoto-2011-06-16-um-122222 Bis heute ist mir nicht ganz klar, wie ich es geschafft habe, dass die Anwendung in meiner Seitenleiste auftaucht. Sie war irgendwann nach etlichen Bemühungen einfach drin. :-/ Ich bin keine Facebook-Crack und musste mich durch den Einstellungs-Dschungel kämpfen.

Wie auch immer… Den FanGateController habe ich nun dahingehend abgeändert, dass der signierte FB Request an den FacebookService gegeben wird, der mir anschließend die JSON Daten liefert.

class FanGateController {
  def facebookService
  def index = {
    try {
      JSONElement data = facebookService.decodeSignedRequest(params.signed_request)
      log.info "FB user entered our fan gate: ${data}"
      def liked = facebookService.isLiked(data)
      render(view: liked ? 'liked' : 'notliked')
    }
    catch (IllegalArgumentException e) {
      render(text: 'Wir haben keine Daten von Facebook bekommen')
    }
  }
}

Diese Daten variieren in Abhängigkeit des Facebook-Nutzers. Es spielt z.B. eine Rolle, ob er angemeldet ist oder nicht. Oder ob er sich auf einer bestimmten Seite befindet, in der die App verknüpft ist oder nicht. Kommt der Request z.B. von einer Seite, dann wird ein page Objekt geliefert. Genau dieses Objekt zapfe ich an, um festzustellen, ob der der Benutzer der Facebook-App bereits sein Like gegeben hat oder nicht. Dazu erweitere ich den FacebookService.

  ...
  boolean isLiked(JSONElement jsonData) {
    jsonData?.page?.liked
  }
  ...

An dieser Stelle kannst du ansetzen und deine Views bauen, damit deine Fans auch ordentlich was hübsches zu sehen bekommen. Ich für mein Teil habe noch etwas zu tun und melde mich mit einem Nachtrag, sobald unser Fan-Gate online geht. Denn einfach einen Text zu hinterlegen reicht da nicht. Wir wollen unseren Fans auch was bieten 😉 Was das sein wird? Dazu später…