views:

889

answers:

5

For a more recent discussion about a similar topic check this question out.

Hi all,

Hoping someone can help me out with this one:

What's the best way to validate whether a particular user has ownership of a website?

Let's say you had this model:

class User < ActiveRecord::Base
   has_many :websites
end

In order to make sure the User does indeed own that website I thought about doing email validations. Example: user lists example.com as their website and an email gets sent to [email protected]. If a user sends a response message from example.com the website is validate.

The problem with this is if there was a website where a large group of people could send an email from a website with that domain name, like gmail.com. I wouldn't want a user to register gmail as their personal website.

Thus it seems the best way to do it is to have the user to embed some code in the HTML and the rails applications makes sure that that code is there.

How would you go about doing this?

Kenji

+3  A: 

You could confirm it in a manner similar to what google analytics does by having them place some javascript/code on their site that you could use to confirm. Alternatively you could have them create a DNS record that you could check for - both of those would show actual ownership of the site rather than just being part of the domain.

paulthenerd
Thanks for the suggestion Paul,How would the rails application make sure that the code/DNS record was there?
Kenji Crosland
Well say you generated a hash code for them to use - just have them place it in a hidden element with an id that you tell them to use and then load their page and look for that id. If it's their and contains their hash code then you know they have the ability to modify the page...I'm still thinking this through I'd recommend looking at google analytics they do something similar iirc
paulthenerd
Thanks a lot! This should get me started. I remember doing the google analytics thing a long while back. I'll have to check it out again.
Kenji Crosland
A: 

Special code embedded on the given page (like Google Analytics).

You may also request that user have to provide a URL within the given domain with the specified content. For example user says that under the URL /vali/date/me on domain www.foo.com you can find page generating "VALIDATED" text.

Henryk Konsek
Wow, Super simple! That's the way I like it. Thanks! Would you happen to know the specific code in the rails app that would look for this text?
Kenji Crosland
+3  A: 

This is is how you could validate a domain using the google subdomain approach in a RESTful style. You will allow a user to create a Site record, which will remain unverified until the user later clicks a link/button to verify the domain at a later time (to allow DNS propagation).

This code is untested but will get you started.

Model:

class Site < ActiveRecord::Base
  # schema
  # create_table "sites", :force => true do |t|
  #  t.string   "domain"
  #  t.string   "cname"
  #  t.integer  "user_id"
  #  t.boolean  "verified"
  #  t.datetime "created_at"
  #  t.datetime "updated_at"
  # end

  require "resolv"

  YOUR_DOMAIN = "example.com"

  belongs_to :user
  before_create :generate_cname

  attr_accessible :domain
  …

  # Validate unless already validated
  def validate!
    validate_cname unless self.verifed == true
  end

  protected

  # Generate a random string for cname
  def generate_cname
    chars = ('a'..'z').to_a
    self.cname = 10.times.collect { chars[rand(chars.length)] }.join
  end

  # Sets verifed to true if there is a CNAME record matching the cname attr and it points to this site.
  def validate_cname
    Resolv::DNS.open do |domain|
      @dns = domain.getresources("#{cname}.#{domain}", Resolv::DNS::Resource::IN::CNAME)
     self.verified = [email protected]? && @dns.first.name.to_s == YOUR_DOMAIN
    end
  end

end

Controller

class SitesController < ActionController::Base
  # Usual RESTful controller actions
  # …

  def validate
    @site = current_user.sites.find(params[:id])
    @site.validate!

    respond_to do |format|
      if @site.save && @site.verified
        flash[:notice] = 'Site verified!'
        format.html { redirect_to(@site) }
        format.xml  { head :ok }
      else
        flash[:Error] = 'Site verification failed!'
        format.html { redirect_to(@site) }
        format.xml  { render :status => :unprocessable_entity }
      end
    end

  end
end

Put this in routes.rb:

map.resources :sites, :member => { :validate => :put }

I'll leave implementing the views as an exercise for you.

Steve Graham
This looks promising. I'll see what I can do to implement it. Thanks a lot!
Kenji Crosland
if you found it useful, please up vote it! :)
Steve Graham
Trying to build up the old reputation!! Would if I could!
Kenji Crosland
This solution fails when dealing with people who don't have a control over a website but not the server/domain name. Eg: a blogger account, where a user's web page could be http://someone.blogger.com.
EmFi
They might have control over a website, but they do not own it. That is the key. One might also say that if one doesn't have control over the domain then one cannot claim to have control over a website anyway.Putting an arbitrarily named file in a directory would also fail under these conditions. The only way I could think to accommodate such use cases would be to put arbitrary text within a document and parse it out. Then again, what is to stop me masquerading as the owner of your blog and putting the required arbitrary text in a post comment? That system will have been duped well and truly!
Steve Graham
+1  A: 

Google Website tools allows you to just upload a file with a very specific name. so, you could require that the user create a file in order for you to verify that they own the website

require 'digest/sha1'
require 'net/http'

# you may want to store a unique value in the website table instead
unique_id = Digest::SHA1.hexdigest("#{website.id}/#{website.created_at}")
response = Net::HTTP.start(website.url, 80) {|http| http.head("/verify_#{unique_id}.html") }
website.verified = true if response.code == "200"
Amiel Martin
I like this. I was thinking it might be nice to have two models, an unverified_website and a verified_website model (which I could just call "website". The unique id would be generated for the unverified website. When the user puts the unique file on the website they can go back to a list of their unverified websites and click "verify" which would initiate a create action for "website"(ensuring that the unique value was there before creation), which would simply create a website based on the attributes of the unverified site. Going to try this and the above solution out.
Kenji Crosland
A: 

Thanks a lot guys. I used a blend of Steve's and Amiel's suggestions to find a solution that works:

In the website model:

class Website < ActiveRecord::Base
  require 'net/http'

  belongs_to :user
  before_create :generate_unique_id

  # Validate unless already validated
  def verify!
    verify_unique_id unless self.verified == true
  end

  protected

  # Generate a random string for unique_id
  def generate_unique_id
    self.unique_id = ActiveSupport::SecureRandom.hex(10)
  end

  def verify_unique_id
  response = Net::HTTP.start(self.domain, 80) {|http| http.head("/#   {unique_id}.html") }
    self.verified = true if response.code == "200"
  end

end

The controller is nearly unchanged from Steve's suggestion except for the part where the app finds the id of the website:

    def verify
    @website = Website.find_by_id(params[:website])
    @website.verify!

    respond_to do |format|
      if @website.save && @website.verified == true
        flash[:notice] = 'Site verified!'
        format.html { redirect_to(websites_path) }
        format.xml  { head :ok }
      else
        flash[:notice] = 'Site verification failed!'
        format.html { redirect_to(websites_path) }
        format.xml  { render :status => :unprocessable_entity }
      end
    end
end

Finally I have an index view of the user's websites (It's ugly but it does the job):

<% for website in @websites %>
<%= link_to "#{website.domain}", website %> | 
<% if website.verified? %> VERIFIED |  
<% else %> NOT VERIFIED 
<%= link_to "Verify your website", verify_website_path(:website => website.id), :id => website.id %>
Verification key: <%= website.unique_id %>
<% end %><% end %>
<%= link_to "Add a website", new_website_path %>

Anyway, I put this through a few manual tests with one of my existing websites and it works without a problem. I still of course have to implement other validations but this was the one I really needed help on. Thanks guys!

Kenji

Kenji Crosland
I have to say I prefer your usage of ActiveSupport::SecureRandom to generate the string. I don't know why I didn't think if that! Much simpler!
Steve Graham
One extra note though. I would use button_to with a PUT instead of link_to to verify the record. As you are updating an existing record it's the RESTful way!
Steve Graham
This solution fails when dealing with people who don't have a control over a website but not the server/domain name. Eg: a blogger account, where a user's web page could be http://someone.blogger.com.Probably better to have the user insert a hidden Div and grep the http response for your unique id.
EmFi
I see! THAT's why you specified put in the routes. Will be sure to fix that one.
Kenji Crosland
Steve Graham
Just noticed this message now. Reversed!
Kenji Crosland