HOW TO MAKE A FULL FLEDGED REST API with DJANGO OAUTH TOOLKIT
FEDERICO FRENGUELLI @synasius http://evonove.it
GOALS OAuth2 protected REST API with Django
WHY?
INTRODUCING the marvelous TIMETRACKER
ONCE UPON A TIME... one tool single project deploy once and everything is fine... (more or less)
THE TIMES THEY ARE A-CHANGIN'
Web UIs evolve Smarter Users Multiple Devices to Support
APPLICATION MITOSIS! timetracker-backend timetracker-web timetracker-android timetracker-ios timetracker-desktop (linux, win, osx) moreover...
SERVICES ARE CONNECTED! Third party service want your user's data!
WHAT'S IN THE BACKEND? A service that expose an amazing and reliable REST API
THE REAL APP TIMETRACKER timetracker-backend timetracker-web timetracker-android timetracker-ios timetracker-desktop (linux, max, osx)
UI RECIPE Gumby css framework Ember.js javascript framework jquery No matter what you use.. it's a pain in the ass!
BACKEND RECIPE Django Django REST Framework Django OAuth Toolkit
MODELS class Activity(models.Model): name = models.charfield(max_length=100) description = models.textfield(blank=true) class TimeEntry(models.Model): activity = models.foreignkey(activity) user = models.foreignkey(settings.auth_user_model) description = models.textfield(blank=true) start = models.datetimefield(blank=true, null=true) end = models.datetimefield(blank=true, null=true)
API ENDPOINTS Url Methods Semantic /api/activities/ GET, POST list, create /api/activities/<id>/ GET, PUT/PATCH, DELETE detail, update, remove /api/tracks/ GET, POST list, create /api/tracks/<id>/ GET, PUT/PATCH, DELETE detail, update, remove
DEEP INTO DRF IN 5 MINUTES
SERIALIZE DATA class ActivitySerializer(serializers.Serializer): pk = serializers.field() name = serializers.charfield(max_length=100) description = serializers.charfield(required=false) def restore_object(self, attrs, instance=none): if instance: # Update existing instance instance.name = attrs.get('name', instance.name) instance.description = attrs.get('description', instance.description) return instance # Create new instance return Activity(**attrs) serializer = ActivitySerializer(activity) serializer.data # {'pk': 1, 'name': u'timetracker', 'description': u'workin on time tracker'}
SIMPLIFY! MODEL SERIALIZER class ActivitySerializer(serializers.ModelSerializer): class Meta: model = Activity
API ENDPOINTS VIEWS What do we need? respect REST semantic user authentication permissions checks (also object level permission) pagination response and request formatting it's a lot of stuff!
KEEP CALM AND USE DRF!
SETTINGS REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.sessionauthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.isauthenticated', ), 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.jsonrenderer', ), 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.jsonparser', ) }
APIVIEW class ActivityList(APIView): """ List all activities, or create a new activity. """ def get(self, request, format=none): activities = Activity.objects.all() serializer = ActivitySerializer(activities, many=true) return Response(serializer.data) def post(self, request, format=none): serializer = ActivitySerializer(data=request.DATA) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.http_201_created) return Response(serializer.errors, status=status.http_400_bad_request) urlpatterns = patterns('', url(r'^api/activities/$', ActivityList.as_view()), #... )
SIMPLIFY! GENERIC CLASS BASED VIEWS class ActivityList(generics.ListCreateAPIView): queryset = Activity.objects.all() serializer_class = ActivitySerializer class ActivityDetail(generics.RetrieveUpdateDestroyAPIView): queryset = Activity.objects.all() serializer_class = ActivitySerializer class TimeEntryList(generics.ListCreateAPIView): queryset = TimeEntry.objects.all() serializer_class = TimeEntrySerializer class TimeEntryDetail(generics.RetrieveUpdateDestroyAPIView): queryset = TimeEntry.objects.all() serializer_class = TimeEntrySerializer
LAZY DEVS? VIEWSETS class ActivityViewSet(viewsets.ModelViewSet): model = Activity class TimeEntryViewSet(viewsets.ModelViewSet): model = TimeEntry router = routers.defaultrouter() router.register(r'activities', ActivityViewSet) router.register(r'tracks', TimeEntryViewSet) urlpatterns = patterns('', url(r'^api/', include(router.urls)), )
BONUS! BUILTIN BROWSABLE API
HOW DO YOUR CLIENTS AUTHENTICATE? AND WHAT IF A THIRD PARTY APP WANTS TO ACCESS YOUR USER'S DATA??
PROBLEMS Store the user password in the app The app has a full access to user account User has to change his password to revoke the access Compromised apps expose the user password Reference: http://www.slideshare.net/aaronpk/an-introduction-to-oauth2
THE OAUTH2 AUTHORIZATION FRAMEWORK How does it work?
USE CASE
ACTORS Resource Owner: The User Resource Server: Timetracker API Authorization Server: The same as the Resource Server Client: Songify App
STEPS Client registers with the Authorization Server The Authorization Server provides client id and client secret Client directs the Resource Owner to an authorization server via its user-agent The Authorization Server authenticates the Resource Owner and obtains authorization The Authorization Server directs the Resource Owner back to the client with the authorization code The Client exchange the authorization code for a token The token is used by the Client to authenticate requests
DJANGO OAUTH TOOLKIT Django 1.4, 1.5, 1.6, 1.7 Python2 & Python3 built on top of oauthlib https://github.com/evonove/django-oauth-toolkit
DOT AND DJANGO INSTALLED_APPS += ('oauth2_provider',) urlpatterns += patterns('', url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), ) Create a protected endpoint from oauth2_provider.views.generic import ProtectedResourceView class ApiEndpoint(ProtectedResourceView): def get(self, request, *args, **kwargs): return HttpResponse('Protected with OAuth2!')
BATTERIES INCLUDED builtin views to register developer apps form view for user authorization
INTEGRATES WITH DRF REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'oauth2_provider.ext.rest_framework.oauth2authentication', ) }
LET'S TEST IT! Authorization endpoint http://localhost:8000/o/authorize?response_type=code&client_id=&redirect_uri=ht Exchange the code curl -X POST -d "grant_type=authorization_code&code= &redirect_uri=http://example.com/" http://:@localhost:8000/o/token/ Unauthenticated access curl http://localhost:8000/api/activities/ Authenticated access curl -H "Authorization: Bearer " http://localhost:8000/api/activities/
FUTURE PLANS OAuth1 support OpenID connector NoSQL storages support HELP NEEDED
THANKS