Wykorzystanie RPXnow w Django do autoryzacji użytkowników
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ść:

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.


- 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:
Oraz w wybranym miejscu strony link wywołujący go:
<script src="https://rpxnow.com/openid/v2/widget" type="text/javascript"></script> <script type="text/javascript"> RPXNOW.overlay = true; RPXNOW.language_preference = 'pl'; </script>
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").<a class="rpxnow" onclick="return false;" href="https://NAZWA_APLIKACJI.rpxnow.com/openid/v2/signin?token_url=TWÓJ_URL"> Zaloguj się </a>
- 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))
Comment article