Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add support for "login with Facebook" and MS live #153

Closed
wants to merge 4 commits into from

7 participants

@apmon

Annoyingly Facebook and MS Live accounts do not implement OpenID which is the correct way to do authentication. Therefore they are not covered by the existing implementation of third party authentication. Imho they are, however, important enough to (reluctantly) warrant some special casing to support them and make it easier for new users to create an OpenStreetMap account.

This implementation uses the OAuth2 standard to retrieve user information from either Facebook or a Windows Live account that can then be used for authentication. It reuses a lot of the openID infrastructure to simplify the code and minimize the special casing.

As it uses OAuth2 and OAuth requires applications to register with the provider, the website needs to be
registered with Facebook and MS Live to use this feature. The oauth app id and app secret are stored
in application.yml

@pnorman

@simonpool, is the status of facebook logins held at LWG?

@apmon, you have a dev instance that can be used for testing, right?

@simonpoole

We need to ask Facebook (or rather their legal department) if they would be OK with us using it. Nominally we would not be adhering to their terms,

@apmon

Yes, there is a test instance at http://apmon.dev.openstreetmap.org/ with up-to-date code

For some background on the legal issues for others reading this ticket:

Unlike Google and Yahoo, Facebook doesn't implement the appropriate APIs for federated authentication, which would be OpenID. Instead it (miss)uses OAuth for this purpose (as it presumably expects wanting to also retrieve the full social graph and other private information of a user from Facebook). With OAuth, however the "relying party" has to register with each "identity provider" explicitly. I.e. OSM(F) has to create an account with Facebook and register osm.org as an OAuth app. As such it has to explicitly sign the Terms of Use contract with facebook.

If I remember the issue correctly, the problem with the Terms of Use is the clause that the facebook app (in this case osm.org) has to be legal in any country it is used. However, mapping might not be legal in all countries, however osm.org (and as extension as a facebook app) is technically accessible in those countries. Now the worry is that facebook could sue OSMF for violating their TOU, due to this.

I can't remember if there were other issues raised.

This patch however also implements "login with Microsoft Account" (which is in the same technical boat of using OAuth and therefore needing an application ID by registering osm.org with Microsoft). Although Microsoft-Account might not be as "sexy", there are still over 500 million users with Microsoft accounts and it integrates nicely with Windows 8. So a big potential for use as single sign on solution. Has the LWG looked at the status of the TOU of Microsoft? Are there any issues with that? Can this part of the patch be deployed?

@pnorman

@apmon do you have a link to the Microsoft TOU?

As a more general question, what happens to these users if Facebook/MS cut off the service, either because of legal concerns or technical reasons. Will the users still be able to log in to the OSM website somehow?

@apmon

Yes, users will still be able to log in to the OSM website. The way "login with openID", "login with google" (which just uses plain OpenID, "login with facebook" and "login with Microsoft" are set up, it is simply an (additional) alternative to a password. Everything else is identical.

If users specify their own password in addition to using "login with FB/MS", they can simply use that to log-in, if for what ever reason the the service is cut off.

If users haven't specified a password during signup, the system generates a long random password in the background (that no one knows). They can then simply go through the standard password recovery mechanism to obtain a new password.

@apmon

The page you need to register the website on for Microsoft is: https://account.live.com/developers/applications/index
This then links to the following terms of use: http://msdn.microsoft.com/en-US/library/live/ff765012

@apmon

I have rebased my facebook-login branch to the latest code and checked that it still works.

@pnorman

A quick skim of the Microsoft policy doesn't find anything with the issues of the FB one. The equivalent statement in the MS terms is "you must comply with all applicable laws and regulations", which is a meaningless statement, because as a matter of law you must comply with all applicable laws and regulations.

Most of the terms are concerned with either desktop applications or applications using features other than identification (e.g. apps sending messages through MSN Messenger).

This is a cursory review, but encouraging.

Kai Krueger added some commits
Kai Krueger Add login with facebook and MS live
Facebook and MS Live accounts don't implement OpenID, so aren't covered by the existing
implementation of third party authentication. However, imho they are important enough
to warrant some special casing to support them.

This implementation uses OAuth2 standard to retrieve user information from either Facebook or a Windows Live
account that can then be used for authentication. It reuses a lot of the openID infrastructure to simplify
the code and minimize the special casing.

As it uses OAuth2 and OAuth requires applications to register with the provider, the website needs to be
registered with Facebook and MS Live to use this feature. The oauth app id and app secret are stored
in application.yml
3c760df
Kai Krueger Fix Facebook login, who changed URLs de9cdcc
Kai Krueger Fixed missed merge conflict 16cadb1
Kai Krueger Change url_field to text_field to support "facebook urls"
In order to be able to reuse as much of the OpenID code, we
shoehorn the facebook and MS live identities into a custom "URL",
however this URL is deliberately non valid to not clash with any valid
urls. So we need to disable rails URL check
1f1378c
@mourner

Any updates on this?

@simonpoole

@mourner the LWG gave a green light from the legal pov at its last meeting.

@mourner

Nice!

@pnorman

To collect everything, link to minutes: https://docs.google.com/document/d/1uemhXWKwbu3RNjAWcG0R-nEFaN1FHjZl1McFwjXkNSc/pub

10. FB and MS Logins

Kai Krüger would like a minuted “OK” that he can proceed.
The LWG has no objection to adding Facebook and MS login support and considers the risk for legal issues minimal.

@pnorman pnorman commented on the diff
app/controllers/user_controller.rb
((6 lines not shown))
else
update_user(@user, params)
end
- elsif using_open_id?
+ elsif using_federated_login?
# The redirect from the OpenID provider reenters here
# again and we need to pass the parameters through to
# the open_id_authentication function
@pnorman
pnorman added a note

open_id_authentication is no longer the name of the function, and it's not OpenID specific

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pnorman pnorman commented on the diff
app/controllers/user_controller.rb
@@ -265,7 +265,7 @@ def create
elsif @user.openid_url.present?
# Verify OpenID before moving on
@pnorman
pnorman added a note

Not just OpenID

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pnorman pnorman commented on the diff
app/controllers/user_controller.rb
((51 lines not shown))
##
# handle OpenID authentication
def openid_authentication(openid_url)
+ #logger.debug "OpenID_authentication" + openid_url
@pnorman
pnorman added a note

Should this be deleted or just commented?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pnorman pnorman commented on the diff
config/example.application.yml
@@ -80,6 +80,13 @@ defaults: &defaults
#oauth_key: ""
# OAuth consumer key for iD
#id_key: ""
+ #OAuth2 parameters for facebook login
@pnorman
pnorman added a note

Should we document how to set this up and where to register with FB/live?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pnorman pnorman commented on the diff
config/example.application.yml
@@ -80,6 +80,13 @@ defaults: &defaults
#oauth_key: ""
# OAuth consumer key for iD
#id_key: ""
+ #OAuth2 parameters for facebook login
+ facebook_app_id:
@pnorman
pnorman added a note

Other unset config items in the defaults (oauth consumer keys, etc) are commented out - should this be the same?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pnorman

Setup notes from @apmon

04:04 < apmon> pnormal: To register a new oauth app on facebook, go to https://developers.facebook.com/ and then choose the "app", "register new app" drop down
               menu item
04:16 < apmon> pnormal: For the MS live account, you can register the application at https://account.live.com/developers/applications/index
@ppawel

What is the current status? @apmon Are you working on this? If not then I could possibly try to take a stab at rebasing and dealing with Google phasing out OpenID.

@pnorman

I don't believe anyone is actively working on this.

@mvexel

Vaguely related, is this an OSM web site bug?

@mvexel

It looks like Google is retiring OpenID 2.0 support, moving everything to something called OpenID Connect which seems to be based on the OAuth 2.0 flow. More info here - is this something we're already supporting, or looking to support? (potentially related to #894)

@tomhughes
Owner

Oh for crying out loud, can we please stop this splattering comments across any bug vaguely related to your current personal bugbear.

The google issue is being dealt with in #897 not here...

@mvexel

Thanks for pointing that out @tomhughes !

@tomhughes
Owner

Support for Facebook and Windows Live has now been added via #960 and is live.

Thanks for your work on this @apmon even if we didn't wind up using this version in the end, and apologies that it all took so long to get resolved.

@tomhughes tomhughes closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 24, 2013
  1. Add login with facebook and MS live

    Kai Krueger authored
    Facebook and MS Live accounts don't implement OpenID, so aren't covered by the existing
    implementation of third party authentication. However, imho they are important enough
    to warrant some special casing to support them.
    
    This implementation uses OAuth2 standard to retrieve user information from either Facebook or a Windows Live
    account that can then be used for authentication. It reuses a lot of the openID infrastructure to simplify
    the code and minimize the special casing.
    
    As it uses OAuth2 and OAuth requires applications to register with the provider, the website needs to be
    registered with Facebook and MS Live to use this feature. The oauth app id and app secret are stored
    in application.yml
  2. Fix Facebook login, who changed URLs

    Kai Krueger authored
  3. Fixed missed merge conflict

    Kai Krueger authored
  4. Change url_field to text_field to support "facebook urls"

    Kai Krueger authored
    In order to be able to reuse as much of the OpenID code, we
    shoehorn the facebook and MS live identities into a custom "URL",
    however this URL is deliberately non valid to not clash with any valid
    urls. So we need to disable rails URL check
This page is out of date. Refresh to see the latest.
View
BIN  app/assets/images/facebook.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  app/assets/images/live.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
145 app/controllers/user_controller.rb
@@ -127,16 +127,16 @@ def account
# valid OpenID and one the user has control over before saving
# it as a password equivalent for the user.
session[:new_user_settings] = params
- openid_verify(params[:user][:openid_url], @user)
+ federated_verify(params[:user][:openid_url], @user)
else
update_user(@user, params)
end
- elsif using_open_id?
+ elsif using_federated_login?
# The redirect from the OpenID provider reenters here
# again and we need to pass the parameters through to
# the open_id_authentication function
@pnorman
pnorman added a note

open_id_authentication is no longer the name of the function, and it's not OpenID specific

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
settings = session.delete(:new_user_settings)
- openid_verify(nil, @user) do |user|
+ federated_verify(nil, @user) do |user|
update_user(user, settings)
end
end
@@ -208,13 +208,13 @@ def new
@title = t 'user.new.title'
@referer = params[:referer] || session[:referer]
- if using_open_id?
+ if using_federated_login?
# The redirect from the OpenID provider reenters here
# again and we need to pass the parameters through to
# the open_id_authentication function
@user = session.delete(:new_user)
- openid_verify(nil, @user) do |user, verified_email|
+ federated_verify(nil, @user) do |user, verified_email|
user.status = "active" if user.email == verified_email
end
@@ -265,7 +265,7 @@ def create
elsif @user.openid_url.present?
# Verify OpenID before moving on
@pnorman
pnorman added a note

Not just OpenID

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
session[:new_user] = @user
- openid_verify(@user.openid_url, @user)
+ federated_verify(@user.openid_url, @user)
else
# Save the user record
session[:new_user] = @user
@@ -275,12 +275,12 @@ def create
end
def login
- if params[:username] or using_open_id?
+ if params[:username] or using_federated_login?
session[:remember_me] ||= params[:remember_me]
session[:referer] ||= params[:referer]
- if using_open_id?
- openid_authentication(params[:openid_url])
+ if using_federated_login?
+ federated_authentication(params[:openid_url])
else
password_authentication(params[:username], params[:password])
end
@@ -519,9 +519,57 @@ def password_authentication(username, password)
end
end
+ def using_federated_login?(identity_url = params[:openid_url]) #:doc:
+ using_open_id? || params[:code] || params[:error]
+ end
+
+ def federated_authentication(identity_url)
+ auth_method = session.delete(:federated_auth_method)
+ if (params[:error] && params[:error_description])
+ failed_login(params[:error_description])
+ elsif ((params[:code] && (auth_method == 'facebook')) || (identity_url && identity_url.match(/#facebook#:\/\/(.*)/)))
+ facebook_authentication()
+ elsif ((params[:code] && (auth_method == 'mslive')) || (identity_url && identity_url.match(/#live#:\/\/(.*)/)))
+ mslive_authentication()
+ else
+ openid_authentication(identity_url)
+ end
+
+ end
+
+ def facebook_authentication()
+ if params[:code]
+ consumer = OAuth2::Client.new(FACEBOOK_APP_ID,FACEBOOK_APP_SHARED_SECRET, :site => 'https://graph.facebook.com', :authorize_url => '/dialog/oauth', :token_url => '/oauth/access_token')
+ access_token = consumer.auth_code.get_token(params[:code], {:redirect_uri => "#{request.protocol}#{request.host_with_port}#{request.fullpath}", :parse => :query})
+ user_info = access_token.get('/me').parsed
+ identity_url = "#facebook#://" + user_info["id"]
+ login_with_identity_url(identity_url, user_info["name"], user_info["email"])
+ else
+ session[:federated_auth_method] = 'facebook'
+ consumer = OAuth2::Client.new(FACEBOOK_APP_ID,FACEBOOK_APP_SHARED_SECRET, :site => 'https://www.facebook.com/', :authorize_url => '/dialog/oauth', :token_url => '/oauth/access_token')
+ redirect_to consumer.auth_code.authorize_url(:scope => 'email', :redirect_uri => "#{request.protocol}#{request.host_with_port}#{request.fullpath}")
+ end
+ end
+
+ def mslive_authentication()
+ if params[:code]
+ consumer = OAuth2::Client.new(LIVE_APP_ID,LIVE_APP_SHARED_SECRET, :site => 'https://login.live.com/', :authorize_url => '/oauth20_authorize.srf', :token_url => '/oauth20_token.srf')
+ access_token = consumer.auth_code.get_token(params[:code], {:redirect_uri => session.delete(:federated_redirect_url), :parse => :json})
+ user_info = access_token.get('https://apis.live.net/v5.0/me').parsed
+ identity_url = "#live#://" + user_info["id"]
+ login_with_identity_url(identity_url, user_info["name"], user_info["emails"]["preferred"])
+ else
+ session[:federated_auth_method] = 'mslive'
+ session[:federated_redirect_url] = "#{request.protocol}#{request.host_with_port}#{request.fullpath}"
+ consumer = OAuth2::Client.new(LIVE_APP_ID,LIVE_APP_SHARED_SECRET, :site => 'https://login.live.com/', :authorize_url => '/oauth20_authorize.srf', :token_url => '/oauth20_token.srf')
+ redirect_to consumer.auth_code.authorize_url(:scope => 'wl.basic, wl.signin, wl.emails', :redirect_uri => session[:federated_redirect_url])
+ end
+ end
+
##
# handle OpenID authentication
def openid_authentication(openid_url)
+ #logger.debug "OpenID_authentication" + openid_url
@pnorman
pnorman added a note

Should this be deleted or just commented?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
# If we don't appear to have a user for this URL then ask the
# provider for some extra information to help with signup
if openid_url and User.find_by_openid_url(openid_url)
@@ -533,6 +581,14 @@ def openid_authentication(openid_url)
# Start the authentication
authenticate_with_open_id(openid_expand_url(openid_url), :method => :get, :required => required) do |result, identity_url, sreg, ax|
if result.successful?
+ # Guard against not getting any extension data
+ sreg = Hash.new if sreg.nil?
+ ax = Hash.new if ax.nil?
+ ax["http://axschema.org/namePerson/friendly"] = [] if ax["http://axschema.org/namePerson/friendly"].nil?
+ ax["http://axschema.org/contact/email"] = [] if ax["http://axschema.org/contact/email"].nil?
+ nickname = sreg["nickname"] || ax["http://axschema.org/namePerson/friendly"].first
+ email = sreg["email"] || ax["http://axschema.org/contact/email"].first
+
# We need to use the openid url passed back from the OpenID provider
# rather than the one supplied by the user, as these can be different.
#
@@ -574,6 +630,57 @@ def openid_authentication(openid_url)
end
end
+ def federated_verify(identity_url, user)
+ auth_method = session.delete(:federated_auth_method)
+ if (params[:error] && params[:error_description] && auth_method)
+ flash.now[:error] = params[:error_description]
+ elsif ((params[:code] && (auth_method == 'facebook')) || (identity_url && (identity_url.match(/#facebook#:\/\/(.*)/))))
+ facebook_verify(identity_url, user) do |user,verified_email|
+ yield user, verified_email
+ end
+ elsif ((params[:code] && (auth_method == 'mslive')) || (identity_url && (identity_url.match(/#live#:\/\/(.*)/))))
+ mslive_verify(identity_url, user) do |user,verified_email|
+ yield user, verified_email
+ end
+ else
+ openid_verify(identity_url, user) do |user,verified_email|
+ yield user, verified_email
+ end
+ end
+ end
+
+ def facebook_verify(identity_url, user)
+ if params[:code]
+ consumer = OAuth2::Client.new(FACEBOOK_APP_ID,FACEBOOK_APP_SHARED_SECRET, :site => 'https://graph.facebook.com', :authorize_url => '/dialog/oauth', :token_url => '/oauth/access_token')
+ access_token = consumer.auth_code.get_token(params[:code], {:redirect_uri => "#{request.protocol}#{request.host_with_port}#{request.fullpath}", :parse => :query})
+ user_info = access_token.get('/me').parsed
+ identity_url = "#facebook#://" + user_info["id"]
+ user.openid_url = identity_url
+ yield user, user_info["email"]
+ else
+ session[:federated_auth_method] = 'facebook'
+ consumer = OAuth2::Client.new(FACEBOOK_APP_ID,FACEBOOK_APP_SHARED_SECRET, :site => 'https://www.facebook.com', :authorize_url => '/dialog/oauth', :token_url => '/oauth/access_token')
+ redirect_to consumer.auth_code.authorize_url(:scope => 'email', :redirect_uri => "#{request.protocol}#{request.host_with_port}#{request.fullpath}")
+ end
+ end
+
+ def mslive_verify(identity_url, user)
+ if params[:code]
+ consumer = OAuth2::Client.new(LIVE_APP_ID,LIVE_APP_SHARED_SECRET, :site => 'https://login.live.com/', :authorize_url => '/oauth20_authorize.srf', :token_url => '/oauth20_token.srf')
+ access_token = consumer.auth_code.get_token(params[:code], {:redirect_uri => session.delete(:federated_redirect_url) , :parse => :json})
+ user_info = access_token.get('https://apis.live.net/v5.0/me').parsed
+ identity_url = "#live#://" + user_info["id"]
+ user.openid_url = identity_url
+ #Windows live accounts don't guarantee emails to be verified, so don't return a verified email address
+ yield user, nil
+ else
+ session[:federated_auth_method] = 'mslive'
+ session[:federated_redirect_url] = "#{request.protocol}#{request.host_with_port}#{request.fullpath}"
+ consumer = OAuth2::Client.new(LIVE_APP_ID,LIVE_APP_SHARED_SECRET, :site => 'https://login.live.com/', :authorize_url => '/oauth20_authorize.srf', :token_url => '/oauth20_token.srf')
+ redirect_to consumer.auth_code.authorize_url(:scope => 'wl.basic, wl.signin, wl.emails', :redirect_uri => session[:federated_redirect_url])
+ end
+ end
+
##
# verify an OpenID URL
def openid_verify(openid_url, user)
@@ -634,6 +741,26 @@ def openid_email_verified(openid_url)
openid_url.match(/https:\/\/me.yahoo.com\/(.*)/)
end
+ def login_with_identity_url(identity_url, tentative_name, tentative_email)
+ if user = User.find_by_openid_url(identity_url)
+ case user.status
+ when "pending" then
+ failed_login t('user.login.account not active', :reconfirm => url_for(:action => 'confirm_resend', :display_name => user.display_name))
+ when "active", "confirmed" then
+ successful_login(user)
+ when "suspended" then
+ failed_login t('user.login.account is suspended', :webmaster => "mailto:webmaster@openstreetmap.org")
+ else
+ failed_login t('user.login.auth failure')
+ end
+ else
+ # We don't have a user registered to this identity_url, so redirect
+ # to the create account page with username and email filled
+ # in if they have been given by the identity provider.
+ redirect_to :controller => 'user', :action => 'new', :nickname => tentative_name, :email => tentative_email, :openid => identity_url
+ end
+ end
+
##
# process a successful login
def successful_login(user)
View
2  app/views/user/account.html.erb
@@ -44,7 +44,7 @@
<fieldset>
<div class="form-row">
<label class="standard-label"><%= t 'user.account.openid.openid' %></label>
- <%= f.url_field :openid_url, {:id => "openid_url", :class => "openid_url"} %>
+ <%= f.text_field :openid_url, {:id => "openid_url", :class => "openid_url"} %>
<span class="form-help deemphasize">(<a href="<%= t 'user.account.openid.link' %>" target="_new"><%= t 'user.account.openid.link text' %></a>)</span>
</diV>
</fieldset>
View
2  app/views/user/login.html.erb
@@ -43,6 +43,8 @@
<ul class='clearfix' id="login_openid_buttons">
<li><%= link_to image_tag("openid.png", :alt => t("user.login.openid_providers.openid.title")), "#", :id => "openid_open_url", :title => t("user.login.openid_providers.openid.title") %></li>
<li><%= openid_button "google", "gmail.com" %></li>
+ <li><%= openid_button "facebook", "#facebook#://" %></li>
+ <li><%= openid_button "live", "#live#://" %></li>
<li><%= openid_button "yahoo", "me.yahoo.com" %></li>
<li><%= openid_button "myopenid", "myopenid.com" %></li>
<li><%= openid_button "wordpress", "wordpress.com" %></li>
View
2  app/views/user/new.html.erb
@@ -41,7 +41,7 @@
<label for="openid_url" class="standard-label">
<%= raw t 'user.new.openid', :logo => openid_logo %>
</label>
- <%= url_field(:user, :openid_url, { :id => "openid_url", :tabindex => 4, :class => "openid_url" }) %>
+ <%= text_field(:user, :openid_url, { :id => "openid_url", :tabindex => 4, :class => "openid_url" }) %>
<%= error_message_on(:user, :openid_url) %>
</div>
<span class="form-help deemphasize"><%= t 'user.new.openid no password' %></span>
View
7 config/example.application.yml
@@ -80,6 +80,13 @@ defaults: &defaults
#oauth_key: ""
# OAuth consumer key for iD
#id_key: ""
+ #OAuth2 parameters for facebook login
@pnorman
pnorman added a note

Should we document how to set this up and where to register with FB/live?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ facebook_app_id:
@pnorman
pnorman added a note

Other unset config items in the defaults (oauth consumer keys, etc) are commented out - should this be the same?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ facebook_app_shared_secret:
+ #OAuth2 parameters for MS live login
+ live_app_id:
+ live_app_shared_secret:
+
# Whether to require users to view the CTs before continuing to edit...
require_terms_seen: false
# Whether to require users to agree to the CTs before editing
View
1  config/initializers/oauth.rb
@@ -1,4 +1,5 @@
require 'oauth/rack/oauth_filter'
+require 'oauth2'
Rails.configuration.middleware.use OAuth::Rack::OAuthFilter
View
6 config/locales/en.yml
@@ -1631,6 +1631,12 @@ en:
aol:
title: Login with AOL
alt: Login with an AOL OpenID
+ facebook:
+ title: Login with Facebook
+ alt: Login with a Facebook account
+ live:
+ title: Login with Windows Live ID
+ alt: Login with your Windows Live ID
logout:
title: "Logout"
heading: "Logout from OpenStreetMap"
Something went wrong with that request. Please try again.