Wykorzystanie RPXnow w Django do autoryzacji użytkowników

Autoryzacja użytkowników z Facebooka, Google Account, Twittera, czy OpenID za pomocą jednego systemu

RPXnow to serwis świadczący usługę agregacji źródeł autoryzacji i autentykacji takich jak OpenID, Facebook Connect, Twitter, Google Account, Blogger, Myspace, czy Windows Live ID. Strona korzystająca z RPXnow w bardzo prosty sposób może umożliwić rejestrację i logowanie użytkowników posiadających wspomniane konta (a w erze serwisów społecznościowych może to być ważnym źródłem nowych użytkowników). Za darmo dostaniemy podstawową funkcjonalność usługi i do 6 źródeł autoryzacji. Konta płatne mogą wybrać do 12 takich źródeł, oraz mają szerszy zakres możliwości.

Konfiguracja usługi

Żeby zacząć używać RPX należy zarejestrować się na stronie i założyć "aplikację" podając jej nazwę, oraz domeny, na których usługa będzie działać. Gdy mamy konto możemy wybrać dostępców i ich kolejność:

rpx1
Niektórzy dostawcy wymagają dodatkowej konfiguracji - np. Facebook wymaga założenia aplikacji Facebook Connect. Po zapisaniu zmian można wykorzystać RPX na własnej stronie.

Po założeniu aplikacji dostanie ona swój klucz API - będzie on potrzebny do żądań wysyłanych do API rpxnow.

Jak to działa?

  • Na własnej stronie umieszczamy JavaScriptowy widżet lub ramkę zawierającą listę dostawców.
  • Użytkownik wybiera dostawcę i podaje swój login/identyfikator. RPX zajmuje się obsługą logowania do danego dostawcy.
    rpx2
    rpx3
  • Po potwierdzeniu zalogowania przed serwis-dostawcę (np. Facebooka) RPX przekierowuje na naszą stronę z tokenem pozwalającym pobrać dane o użytkowniku.
  • Dzięki tokenowi pobieramy dane użytkownika, w tym unikalny identyfikator, po którym możemy zalogować/zarejestrować we własnym serwisie użytkownika.

Implementacja

  • Możesz użyć widżet popupa, lub wyświetlić gotową ramkę - prezentujące listę dostawców. Kody dostępne są w zakładce Quick Start twojego konta
  • Jeżeli chcesz popupa to na końcu strony wklej:
    <script src="https://rpxnow.com/openid/v2/widget"
            type="text/javascript"></script>
    <script type="text/javascript">
      RPXNOW.overlay = true;
      RPXNOW.language_preference = 'pl';
    </script>
    
    Oraz w wybranym miejscu strony link wywołujący go:
    <a class="rpxnow" onclick="return false;"
       href="https://NAZWA_APLIKACJI.rpxnow.com/openid/v2/signin?token_url=TWÓJ_URL">
      Zaloguj się
    </a>
    
    Gdzie TWÓJ_URL to pełen adres URL do strony w twoim serwisie odbierającej token z RPX (o czym za chwilę), a NAZWA_APLIKACJI to nazwa twojej aplikacji na RPX (zobacz wklejki na ich stronie pod "Quick Start").
  • Dla opcji z ramką wystarczy wkleić w wybranym miejscu:
    <iframe src="https://NAZWA_APLIKACJI.rpxnow.com/openid/embed?token_url=TWÓJ_URL"
      scrolling="no" frameBorder="no" style="width:400px;height:240px;">
    </iframe>
    
  • Strona podana pod TWÓJ_URL to strona, na którą zostanie przekierowany użytkownik po zalogowaniu się na stronie dostawcy. Wraz z przekierowaniem przesłany zostanie token (POST, "token"), za pomocą którego będziemy mogli pobrać dane o tym użytkowniku (jeżeli wszystko przebiegło pomyślnie i użytkownik zalogował się u swojego dostawcy), oto fragment widoku Django obsługującego RPX:
    def handle_rpx_post(request):
    	"""
    	RPX posting token after logging
    	"""
    	if 'token' not in request.POST:
    		return HttpResponseRedirect('/')
    	
    	# token z przekierowania
    	token = str(request.POST['token'])
    	# klucz API aplikacji z rpxnow
    	apiKey = settings.RPXNOW
    	# adres URL API rpxnow
    	url = 'https://rpxnow.com/api/v2/auth_info'
    	# żądanie danych użytkownika
    	# przesyłamy token i klucz API
    	req = urllib2.Request(url=url, data='token=%s&apiKey=%s' % (token, apiKey))
    	f = urllib2.urlopen(req)
    	response = f.read()
    	try:
    		jsn = json.loads(response)
    	except:
    		pass
    	else:
    		# stat == ok to poprawna autoryzacja u dostawcy
    		if jsn['stat'] == 'ok':
    			profile = jsn['profile']
    			username = profile['preferredUsername']
    			# identyfikator, openID, czy np. adres profilu (np. Facebook)
    			identifier = profile['identifier']
    			# nie wszyscy dostawcy udostępniają adres email, to pole jest opcjonalne
    			if profile.has_key('email') and len(profile['email']) > 1:
    				email = profile['email']
    			else:
    				email = False
    
  • Mając identyfikator użytkownika (identifier) możemy sprawdzić czy dla niego istnieje już konto, czy też nie. Dobrym rozwiązaniem jest w takim przypadku automatyczna rejestracja i zalogowanie użytkownika, oraz zapamiętanie powiązania identyfikatora z danym kontem użytkownika Django.

Przykładowa integracja z Django

Potrzebujemy obsługę logowania po RPX w AUTHENTICATION_BACKENDS, oraz model powiązań. Dodatkowo warto dać opcje takie jak przypisanie identyfikatora do starego konta. Poniżej przedstawiłem kluczowe elementy mojej implementacji obsługi RPXnow w Bibliotekach. Dostępna jest też aplikacja Django - django-rpx, lecz jej akurat nie testowałem.

  • Backend autoryzacji wygląda tak:
    from django.contrib.auth.models import User
    from django.conf import settings
    
    from diamandas.userpanel.models import *
    
    class RPXBackend:
    	"""
    	Authenticate a user that has associated RPX to his account
    	"""
    	def authenticate(self, user_id=False, rpx=False):
    		if user_id and rpx:
    			try:
    				user = User.objects.get(id=user_id)
    			except:
    				return None
    			if rpx.user == user:
    				return user
    		return None
    
    	def get_user(self, user_id):
    		try:
    			return User.objects.get(pk=user_id)
    		except User.DoesNotExist:
    			return None
    
  • Model powiązań może wyglądać tak:
    class RPXAssociation(models.Model):
    	"""
    	Assoction of user accounts and RPX
    	"""
    	user = models.ForeignKey(User, verbose_name=_('User'), limit_choices_to={'is_staff': False})
    	identifier = models.CharField(max_length=255, verbose_name=_('Identifier'))
    	ask_for_mail = models.BooleanField(blank=True, default=False, verbose_name=_('No email specified'))
    	is_new = models.BooleanField(blank=True, default=True, verbose_name=_('Account made by social source'))
    	def __str__(self):
    		return self.identifier
    	def __unicode__(self):
    		return self.identifier
    	class Meta:
    		verbose_name = _('RPX Association')
    		verbose_name_plural = _('RPX Associations')
    
  • Kompletny widok obsługujący przekierowanie z RPX przedstawiony został poniżej. Zajmuje się on także zalogowaniem/rejestracją użytkownika:
    from random import choice
    import urllib2
    import json
    from django.contrib.auth import authenticate, login
    #....
    
    def handle_rpx_post(request):
    	"""
    	RPX posting token after logging
    	"""
    	if 'token' not in request.POST:
    		return HttpResponseRedirect('/')
    	
    	token = str(request.POST['token'])
    	apiKey = settings.RPXNOW
    	url = 'https://rpxnow.com/api/v2/auth_info'
    	req = urllib2.Request(url=url, data='token=%s&apiKey=%s' % (token, apiKey))
    	f = urllib2.urlopen(req)
    	response = f.read()
    	try:
    		jsn = json.loads(response)
    	except:
    		pass
    	else:
    		if jsn['stat'] == 'ok':
    			profile = jsn['profile']
    			username = profile['preferredUsername']
    			username = normalise_string(username)
    			identifier = profile['identifier']
    			if profile.has_key('email') and len(profile['email']) > 1:
    				email = profile['email']
    			else:
    				email = False
    			
    			try:
    				rpx = RPXAssociation.objects.get(identifier=identifier)
    			except:
    				# no account
    				randompass = ''.join([choice('1234567890qwertyuiopasdfghjklzxcvbnm') for i in range(7)])
    				bla = ''.join([choice('1234567890qwertyuiopasdfghjklzxcvbnm') for i in range(3)])
    				if email:
    					try:
    						user = User.objects.create_user(username, email, randompass)
    					except:
    						username = '%s_%s' % (username, bla)
    						user = User.objects.create_user(username, email, randompass)
    					r = RPXAssociation(identifier=identifier, user=user)
    				else:
    					try:
    						user = User.objects.create_user(username, randompass, randompass)
    					except:
    						username = '%s_%s' % (username, bla)
    						user = User.objects.create_user(username, randompass, randompass)
    					r = RPXAssociation(identifier=identifier, user=user, ask_for_mail=True)
    				r.save()
    				user = authenticate(user_id = str(user.id), rpx=r)
    				if user is not None:
    					login(request, user)
    					if email:
    						request.user.message_set.create(message=_('You have been logged-in successfully :)'))
    					else:
    						request.user.message_set.create(message=_('You have been logged-in successfully. You can set your email address in account preferences :) '))
    			else:
    				# we have a user for that identifier
    				user = authenticate(user_id = str(rpx.user.id), rpx=rpx)
    				if user is not None:
    					login(request, user)
    					request.user.message_set.create(message=_('You have been logged-in successfully :)'))
    			
    			return HttpResponseRedirect('/')
    	# this is error
    	return HttpResponseRedirect('/user/rpx/error/')
    
  • Dodałem także możliwość przypisania identyfikatora do istniejącego konta - użytkownik podaje login i hasło do niego i jeżeli jest poprawne to konto stworzone automatycznie jest kasowane, a przypisanie RPX przestawiane na stare konto. Oto kod widoku:
    class AssignRPXForm(forms.Form):
    	login = forms.CharField(label=_("Username"), max_length=30, widget=forms.TextInput())
    	password = forms.CharField(label=_("Password"), widget=forms.PasswordInput())
    
    @login_required
    def assign_rpx(request):
    	#only for autocreated accounts from RPX
    	r = RPXAssociation.objects.get(user=request.user)
    	if not r.is_new:
    		return HttpResponseRedirect('/')
    	
    	form =  AssignRPXForm()
    	if request.POST:
    		form = AssignRPXForm(request.POST)
    		if form.is_valid():
    			data = form.cleaned_data
    			# try to authenticate the user
    			user = authenticate(username=data['login'], password=data['password'])
    			if user is not None:
    				try:
    					request.user.get_profile().delete()
    				except:
    					pass
    				# delete the new user account
    				request.user.delete()
    				# login on the old account
    				login(request, user)
    				# update the rpx accociation
    				r.user = user
    				r.is_new = False
    				r.save()
    				request.user.message_set.create(message=_('You have been connected to the existing account :)'))
    				return HttpResponseRedirect('/')
    	
    	return render_to_response(
    		'userpanel/assign_rpx.html',
    		{'form': form, 'rpx': r},
    		context_instance=RequestContext(request))
    

blog comments powered by Disqus

Kategorie

Strony