Facebook's deletion request callback with Rails
I've been adding social sign-in buttons, aka "Sign In With Facebook/Twitter/Google" for IdeaFit, and stumbled upon this.
Facebook won't allow you to make an OAuth app live until you provide a Data Deletion Request Callback. The requirement seems to be coming from GDPR policy (on the other hand, Twitter doesn't force it).
Anyways, let me walk you through the implementation of the data deletion request callback in a typical Ruby on Rails application.
Deletion request callback
What is this callback thing?
It's a URL on your website that Facebook can post a request to in order to delete a user's data.
Facebook sends a POST request, and you should delete all the data associated with a particular user (personal data, posts, comments, etc.). It can be done asynchronously and even manually.
In any case, it should return a URL, which the user can visit later to see the status of the removal request.
From the user's perspective, it looks like this.
After she removes your application on Facebook, she is provided with an optional button labeled "Request to delete data". Clicking it results in Facebook sending you a signed request and then displaying the returned URL to the user to check the status later.
Let's code!
We are going to create a dedicated model that will store the data and encapsulate some logic.
But first let's add the routes.
# Facebook requires this in order to make OAUTH app live
resources :deletion_requests, only: [:show] do
collection do
post :facebook
end
end
It generates two paths:
- GET
/deletion_request/<id>
- POST
/deletion_request/facebook
The first one will be used to check the status of the deletion request, and the second is the webhook we'll submit to Facebook.
While we're at it, let's also create a thin controller.
# app/controllers/deletion_requests_controller.rb
class DeletionRequestsController < ApplicationController
# disable CSRF protection, as it doesn't make sense in this case
protect_from_forgery with: :null_session
def facebook
DeletionRequest
.from_signed_fb(params['signed_request'])
.run
render json: {
url: deletion_request_url(dr.pid),
confirmation_code: dr.pid,
}
end
def show
dr = DeletionRequest.find_by_pid!(params[:id])
render text: dr.deleted? ?
"Your data has been completely deleted" :
"Your deletion request is still in progress"
end
end
According to documentation, Facebook sends its encrypted request as a param named signed_request
. We'll see how to decrypt it in a moment; for now, notice that we delegate it to a "factory method" of our model from_signed_fb
.
Once we create the deletion request, we immediately call run
on it, which a method that actually should delete the user and all related data. The exact implementation of this method is specific to your application.
Note that we return a hash with two keys url
and confirmation_code
as required by Facebook.
The URL to check the status would look like this /deletion_requests/a34avcv3
. As you can see in show
method, we simply load the model by pid and display a text message (Facebook requires "human-readable message", so I assume JSON won't do) depending on whether or not the job is done.
Model
Let's generate migration.
bundle exec rails generate migration CreateDeletionRequest \
provider:string \
uid:string \
pid:string:index
Our model is going to have three columns:
provider
("facebook", "twitter", etc. — assuming there are more providers who requires the deletion callback)uid
(a unique identifier within a given provider. Typically, you should already store it on your database after the user signed in)pid
(public id, just a random unique string we'll use as ID)
And, finally, let's create our model:
class DeletionRequest < ApplicationRecord
validates_presence_of :uid, :provider, :pid
# there can only be one entry with given provider + uid
validates_uniqueness_of :uid, scope: :provider
before_validation :set_pid
def run
u = User.where(provider: provider, uid: uid).first
if u.present?
u.destroy!
end
end
def deleted?
User.where(provider: provider, uid: uid).count == 0
end
# ============
# UTIL
# ============
def self.from_signed_fb(req)
data = DeletionRequest.parse_fb_request(req)
DeletionRequest.create(provider: 'facebook', uid: data['user_id'])
end
def self.parse_fb_request(req)
encoded, payload = req.split('.', 2)
decoded = Base64.urlsafe_decode64(encoded)
data = JSON.load(Base64.urlsafe_decode64(payload))
# we need to verify the digest is the same
exp = OpenSSL::HMAC.digest("SHA256", ENV['FACEBOOK_APP_SECRET'], payload)
if decoded != exp
puts 'FB deletion callback called with weird data'
return nil
end
data
end
private
def set_pid
if self.pid.blank?
self.pid = random_pid
end
end
def random_pid
SecureRandom.hex(4)
end
end
Whoa, there's a lot to unpack there.
The "before validation" callback sets the pid
, a unique random string.
In the controller, we create a new request with from_signed_fb
method. It takes a request, decodes it with parse_fb_request
, and then fetches "user_id" out of it.
This field is user's ID in Facebook. If user signed through Facebook before, we store her uid
in our database already. It allows us to find the user in our database by uid
and destroy it (see run
method).
The run
method doesn't have to be synchronous, you could fire a Sidekiq job instead, or send an automatically letter to another employer to manually remove the user later.
How exactly you destroy the data depends on your app and the context. In my case, I always use dependent: :destroy
on associations which means that all the related data will get destroyed automatically.
For example,
has_many :boards, inverse_of: :user, dependent: :destroy
Now, parse_fb_request
is just a ripoff from the official documentation. We decode the request, and validate its authenticity using FACEBOOK_APP_SECRET
of our Facebook app.
What's next?
I hope some of you guys found this article helpful. Feel free to reach out via Twitter if you have any questions (my DMs are always open).
And here are some useful links to learn more on the topic.