├── Single-Sign-On ├── src │ ├── users │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── tests.py │ │ ├── apps.py │ │ ├── urls.py │ │ ├── management │ │ │ └── commands │ │ │ │ └── viga_setup.py │ │ ├── admin.py │ │ ├── managers.py │ │ ├── serializers.py │ │ ├── permissions.py │ │ ├── views.py │ │ └── models.py │ ├── services │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0001_initial.py │ │ │ └── 0002_auto_20200826_0141.py │ │ ├── tests.py │ │ ├── apps.py │ │ ├── serializers.py │ │ ├── urls.py │ │ ├── admin.py │ │ ├── views.py │ │ └── models.py │ ├── sso_server │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── wsgi.py │ │ ├── jwt.py │ │ ├── urls.py │ │ └── settings.py │ └── manage.py ├── database_model │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png ├── Dockerfile ├── docker-compose.yml ├── requirements.txt ├── README.md └── Single-Sign-On.postman_collection.json ├── jwt_example ├── src │ ├── api_app │ │ ├── __init__.py │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── admin.py │ │ ├── tests.py │ │ ├── apps.py │ │ ├── urls.py │ │ └── views.py │ ├── jwt_server │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── wsgi.py │ │ ├── urls.py │ │ └── settings.py │ └── manage.py ├── README.md ├── requirements.txt └── JWT Example.postman_collection.json ├── StandAloneService ├── src │ ├── api_app │ │ ├── __init__.py │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── admin.py │ │ ├── tests.py │ │ ├── apps.py │ │ ├── urls.py │ │ └── views.py │ ├── users │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── tests.py │ │ ├── apps.py │ │ ├── serializers.py │ │ ├── urls.py │ │ ├── views.py │ │ ├── admin.py │ │ ├── management │ │ │ └── commands │ │ │ │ └── viga_setup.py │ │ ├── permissions.py │ │ ├── managers.py │ │ └── models.py │ ├── service_server │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── wsgi.py │ │ ├── urls.py │ │ └── settings.py │ └── manage.py └── requirements.txt ├── .gitignore └── README.md /Single-Sign-On/src/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jwt_example/src/api_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jwt_example/src/jwt_server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Single-Sign-On/src/sso_server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /StandAloneService/src/api_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /StandAloneService/src/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /StandAloneService/src/service_server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jwt_example/README.md: -------------------------------------------------------------------------------- 1 | # JWT Example 2 | 3 | -------------------------------------------------------------------------------- /jwt_example/src/api_app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /StandAloneService/src/api_app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /StandAloneService/src/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jwt_example/src/api_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /StandAloneService/src/api_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /StandAloneService/src/users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /jwt_example/src/api_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /jwt_example/src/api_app/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /StandAloneService/src/api_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /StandAloneService/src/api_app/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /StandAloneService/src/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /jwt_example/src/api_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiAppConfig(AppConfig): 5 | name = 'api_app' 6 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ServicesConfig(AppConfig): 5 | name = 'services' 6 | -------------------------------------------------------------------------------- /StandAloneService/src/api_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiAppConfig(AppConfig): 5 | name = 'api_app' 6 | -------------------------------------------------------------------------------- /Single-Sign-On/database_model/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibhu-Agarwal/Developing-an-SSO-Service-using-Django/HEAD/Single-Sign-On/database_model/1.png -------------------------------------------------------------------------------- /Single-Sign-On/database_model/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibhu-Agarwal/Developing-an-SSO-Service-using-Django/HEAD/Single-Sign-On/database_model/2.png -------------------------------------------------------------------------------- /Single-Sign-On/database_model/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibhu-Agarwal/Developing-an-SSO-Service-using-Django/HEAD/Single-Sign-On/database_model/3.png -------------------------------------------------------------------------------- /Single-Sign-On/database_model/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibhu-Agarwal/Developing-an-SSO-Service-using-Django/HEAD/Single-Sign-On/database_model/4.png -------------------------------------------------------------------------------- /jwt_example/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.10 2 | Django==3.1.9 3 | djangorestframework==3.11.2 4 | djangorestframework-simplejwt==4.4.0 5 | pkg-resources==0.0.0 6 | PyJWT==1.7.1 7 | pytz==2020.1 8 | sqlparse==0.3.1 9 | -------------------------------------------------------------------------------- /StandAloneService/src/api_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from api_app import views 3 | 4 | app_name = 'api_app' 5 | 6 | urlpatterns = [ 7 | path('protected-resource/', views.HelloPythonistaAPIView.as_view(), name='fetch-protected-resource'), 8 | ] 9 | -------------------------------------------------------------------------------- /jwt_example/src/api_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from api_app import views 3 | 4 | app_name = 'api_app' 5 | 6 | urlpatterns = [ 7 | path('protected-resource/', views.HelloPythonistaAPIView.as_view(), name='fetch-protected-resource'), 8 | ] 9 | 10 | -------------------------------------------------------------------------------- /StandAloneService/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.10 2 | cffi==1.14.3 3 | cryptography==3.3.2 4 | Django==3.1.9 5 | djangorestframework==3.11.2 6 | djangorestframework-simplejwt==4.4.0 7 | pycparser==2.20 8 | PyJWT==1.7.1 9 | pytz==2020.1 10 | six==1.15.0 11 | sqlparse==0.3.1 12 | -------------------------------------------------------------------------------- /StandAloneService/src/users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.contrib.auth import get_user_model 3 | 4 | User = get_user_model() 5 | 6 | 7 | class UserSerializer(serializers.ModelSerializer): 8 | 9 | class Meta: 10 | model = User 11 | fields = ('id', 'email', 12 | 'first_name', 'last_name') 13 | -------------------------------------------------------------------------------- /Single-Sign-On/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.5-slim-buster 2 | ENV PYTHONUNBUFFERRED 1 3 | RUN mkdir /code 4 | WORKDIR /code 5 | COPY requirements.txt /code/ 6 | RUN pip install -r requirements.txt 7 | COPY ./src/ /code/ 8 | CMD python manage.py migrate \ 9 | && python manage.py viga_setup \ 10 | && gunicorn sso_server.wsgi --workers=3 --bind 0.0.0.0:8000 11 | -------------------------------------------------------------------------------- /StandAloneService/src/users/urls.py: -------------------------------------------------------------------------------- 1 | from users.views import UserProfileView, UserCreateAPIView 2 | from django.urls import path 3 | 4 | app_name = 'users' 5 | 6 | urlpatterns = [ 7 | path('user//', UserProfileView.as_view(), name='user-retrieve-update-api'), 8 | path('user/create/', UserCreateAPIView.as_view(), name='user-create-callback-url'), 9 | ] 10 | -------------------------------------------------------------------------------- /jwt_example/src/api_app/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.views import APIView 2 | from rest_framework.response import Response 3 | from rest_framework.permissions import IsAuthenticated 4 | 5 | 6 | class HelloPythonistaAPIView(APIView): 7 | permission_classes = (IsAuthenticated,) 8 | 9 | def get(self, request): 10 | content = {'message': 'Hello, Pythonistas!'} 11 | return Response(content) 12 | 13 | -------------------------------------------------------------------------------- /StandAloneService/src/api_app/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.views import APIView 2 | from rest_framework.response import Response 3 | from rest_framework.permissions import IsAuthenticated 4 | 5 | 6 | class HelloPythonistaAPIView(APIView): 7 | permission_classes = (IsAuthenticated,) 8 | 9 | def get(self, request): 10 | content = {'message': 'Hello, Pythonistas, from Stand-Alone-Service!'} 11 | return Response(content) 12 | -------------------------------------------------------------------------------- /jwt_example/src/jwt_server/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for jwt_server project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jwt_server.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /jwt_example/src/jwt_server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for jwt_server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jwt_server.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /Single-Sign-On/src/sso_server/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for sso_server project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sso_server.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /Single-Sign-On/src/sso_server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for sso_server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sso_server.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /StandAloneService/src/service_server/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for service_server project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'service_server.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /StandAloneService/src/service_server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for service_server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'service_server.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /Single-Sign-On/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: postgres 6 | volumes: 7 | - "ssodbdata:/var/lib/postgresql/data" 8 | environment: 9 | - POSTGRES_DB=postgres 10 | - POSTGRES_USER=postgres 11 | - POSTGRES_PASSWORD=postgres 12 | web: 13 | build: . 14 | ports: 15 | - "8000:8000" 16 | depends_on: 17 | - db 18 | volumes: 19 | ssodbdata: 20 | 21 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ModelSerializer 2 | from .models import Service, Connection 3 | 4 | 5 | class PublicServiceSerializer(ModelSerializer): 6 | 7 | class Meta: 8 | model = Service 9 | fields = ('id', 'name', 'identifier',) 10 | 11 | 12 | class ServiceSerializer(ModelSerializer): 13 | 14 | class Meta: 15 | model = Service 16 | fields = '__all__' 17 | 18 | 19 | class ConnectionSerializer(ModelSerializer): 20 | 21 | class Meta: 22 | model = Connection 23 | fields = '__all__' 24 | -------------------------------------------------------------------------------- /Single-Sign-On/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.10 2 | Babel==2.8.0 3 | certifi==2020.6.20 4 | cffi==1.14.3 5 | chardet==3.0.4 6 | cryptography==3.3.2 7 | Django==3.1.9 8 | django-cors-headers==3.5.0 9 | django-filter==2.3.0 10 | django-phonenumber-field==5.0.0 11 | djangorestframework==3.11.2 12 | djangorestframework-simplejwt==4.4.0 13 | gunicorn==20.0.4 14 | idna==2.10 15 | importlib-metadata==2.0.0 16 | Markdown==3.2.2 17 | phonenumbers==8.12.9 18 | Pillow==8.2.0 19 | psycopg2-binary==2.8.6 20 | pycparser==2.20 21 | PyJWT==1.7.1 22 | pytz==2020.1 23 | requests==2.24.0 24 | six==1.15.0 25 | sqlparse==0.3.1 26 | urllib3==1.26.5 27 | zipp==3.2.0 28 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ( 3 | ListCreateServiceAPIView, CreateConnectionAPIView, 4 | FetchPublicKeyAPIView, ListAllServiceAPIView, 5 | ConnectionDetailAPIView 6 | ) 7 | 8 | app_name = 'services' 9 | 10 | urlpatterns = [ 11 | path('fetch-public-key/', FetchPublicKeyAPIView.as_view(), name='fetch-public-key'), 12 | 13 | path('service/all/', ListAllServiceAPIView.as_view(), name='list-all-service'), 14 | path('service/', ListCreateServiceAPIView.as_view(), name='list-create-service'), 15 | 16 | path('connection/new/', CreateConnectionAPIView.as_view(), name='create-connection'), 17 | path('connection//', ConnectionDetailAPIView.as_view(), name='connection-detail'), 18 | ] 19 | -------------------------------------------------------------------------------- /jwt_example/src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jwt_server.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /Single-Sign-On/src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sso_server.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /StandAloneService/src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'service_server.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Service, Connection, Subscription 3 | 4 | 5 | class ServiceAdmin(admin.ModelAdmin): 6 | 7 | list_display = ('id', 'name', 'identifier') 8 | list_display_links = ('id', 'name', 'identifier') 9 | 10 | 11 | class ConnectionAdmin(admin.ModelAdmin): 12 | 13 | list_display = ('id', 'user', 'service') 14 | list_display_links = ('id', 'user', 'service') 15 | 16 | 17 | class SubscriptionAdmin(admin.ModelAdmin): 18 | 19 | list_display = ('id', 'org', 'service', 'end_date', 'is_active') 20 | list_display_links = ('id', 'org', 'service', 'end_date', 'is_active') 21 | 22 | 23 | admin.site.register(Service, ServiceAdmin) 24 | admin.site.register(Connection, ConnectionAdmin) 25 | admin.site.register(Subscription, SubscriptionAdmin) 26 | -------------------------------------------------------------------------------- /StandAloneService/src/users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework.generics import RetrieveUpdateAPIView, CreateAPIView 3 | from users.serializers import UserSerializer 4 | from users.permissions import (IsUserOwner, IsSSOAdmin) 5 | from rest_framework.permissions import IsAuthenticated 6 | from rest_framework_simplejwt.authentication import JWTTokenUserAuthentication 7 | 8 | User = get_user_model() 9 | 10 | 11 | class UserProfileView(RetrieveUpdateAPIView): 12 | 13 | permission_classes = (IsUserOwner,) 14 | queryset = User.objects.all() 15 | serializer_class = UserSerializer 16 | 17 | 18 | class UserCreateAPIView(CreateAPIView): 19 | 20 | permission_classes = (IsAuthenticated, IsSSOAdmin,) 21 | authentication_classes = (JWTTokenUserAuthentication,) 22 | serializer_class = UserSerializer 23 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/urls.py: -------------------------------------------------------------------------------- 1 | from users.views import (UserSignUpView, UserProfileView, 2 | ListCreateOrganizationsAPIView, OrganizationProfileGetView, 3 | OrganizationUsersAPIView, ListAllUsersAPIView,) 4 | from django.urls import path 5 | 6 | app_name = 'users' 7 | 8 | urlpatterns = [ 9 | path('signup/', UserSignUpView.as_view(), name='signup-api'), 10 | path('user/', UserProfileView.as_view(), name='user-retrieve-update-destroy-api'), 11 | 12 | path('users/all/', ListAllUsersAPIView.as_view(), name='list-all-users-api'), 13 | 14 | path('organizations/', ListCreateOrganizationsAPIView.as_view(), name='list-create-org'), 15 | path('organizations/users/', OrganizationUsersAPIView.as_view(), name='org-users'), 16 | 17 | path('organization//detail/', OrganizationProfileGetView.as_view(), name='org-retrieve-api'), 18 | ] 19 | -------------------------------------------------------------------------------- /StandAloneService/src/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.admin import UserAdmin 2 | from django.contrib import admin 3 | from users.models import User 4 | 5 | 6 | class CustomUserAdmin(UserAdmin): 7 | ordering = ('id',) 8 | list_display = ('id', 'first_name', 'last_name', 'email', 9 | 'is_staff') 10 | list_display_links = ('id', 'first_name', 'last_name', 'email') 11 | list_filter = ('is_staff',) 12 | 13 | fieldsets = ( 14 | (None, {'fields': ('password', 'email')}), 15 | ('Personal info', {'fields': ('first_name', 'last_name')}), 16 | ('User Details', {'fields': ('is_staff',)}), 17 | ) 18 | 19 | add_fieldsets = ( 20 | (None, { 21 | 'classes': ('wide',), 22 | 'fields': ('email', 'first_name', 'last_name', 23 | 'password1', 'password2'), 24 | }), 25 | ) 26 | 27 | 28 | admin.site.register(User, CustomUserAdmin) 29 | -------------------------------------------------------------------------------- /StandAloneService/src/service_server/urls.py: -------------------------------------------------------------------------------- 1 | """service_server URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('', include('users.urls')), 21 | path('api/', include('api_app.urls')), 22 | path('admin/', admin.site.urls), 23 | ] 24 | -------------------------------------------------------------------------------- /Single-Sign-On/src/sso_server/jwt.py: -------------------------------------------------------------------------------- 1 | from services.models import Service 2 | from rest_framework_simplejwt.serializers import TokenObtainPairSerializer 3 | from rest_framework_simplejwt.views import TokenObtainPairView 4 | from django.contrib.auth import get_user_model 5 | 6 | User = get_user_model() 7 | 8 | 9 | class MyTokenObtainPairSerializer(TokenObtainPairSerializer): 10 | @classmethod 11 | def get_token(cls, user): 12 | token = super().get_token(user) 13 | 14 | token['aud'] = [] 15 | user_services = Service.for_user(user) 16 | for service in user_services: 17 | token['aud'].append(service.identifier) 18 | 19 | return token 20 | 21 | # A JWT access-token example 22 | # 23 | # { 24 | # 'token_type': 'access', 25 | # 'exp': 1599980514, 26 | # 'jti': 'a3c77262a57d4df5a657fe12860c8492', 27 | # 'user_id': '9a7b69e2-df4d-4c98-bc04-941c66cff1a0', 28 | # 'aud': ['Docs', 'Sheets'] 29 | # } 30 | # 31 | 32 | 33 | class MyTokenObtainPairView(TokenObtainPairView): 34 | serializer_class = MyTokenObtainPairSerializer 35 | -------------------------------------------------------------------------------- /jwt_example/src/jwt_server/urls.py: -------------------------------------------------------------------------------- 1 | """jwt_server URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | from rest_framework_simplejwt import views as jwt_views 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('api/', include('api_app.urls')), 23 | path('api/token/', jwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'), 24 | path('api/token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'), 25 | ] 26 | 27 | -------------------------------------------------------------------------------- /StandAloneService/src/users/management/commands/viga_setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.core.management.base import BaseCommand 3 | from django.contrib.auth import get_user_model 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | EMAIL_HOST_PASSWORD = os.environ.get('VIGA_HOST_PASSWORD') 7 | if not EMAIL_HOST_PASSWORD: 8 | raise ImproperlyConfigured("'VIGA_HOST_PASSWORD' environment variable is unset") 9 | 10 | User = get_user_model() 11 | 12 | email_superuser = 'superuser@standaloneservice.com' 13 | 14 | help_message = f""" 15 | Sets up the DB, creating: 16 | 1) superuser with admin rights (Email: {email_superuser}) 17 | """ 18 | 19 | 20 | class Command(BaseCommand): 21 | """ 22 | viga_setup: Command to set-up database for the application 23 | """ 24 | help = help_message 25 | 26 | def handle(self, *args, **kwargs): 27 | if not User.objects.filter(email=email_superuser).exists(): 28 | User.objects.create_superuser(first_name="Super User", 29 | email=email_superuser, 30 | password=EMAIL_HOST_PASSWORD) 31 | print('Super-User Created!') 32 | print('Viga Set-Up Complete!') 33 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/management/commands/viga_setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.core.management.base import BaseCommand 3 | from django.contrib.auth import get_user_model 4 | from django.core.exceptions import ImproperlyConfigured 5 | from users.models import email_superuser 6 | 7 | EMAIL_HOST_PASSWORD = os.environ.get('VIGA_HOST_PASSWORD') 8 | if not EMAIL_HOST_PASSWORD: 9 | raise ImproperlyConfigured("'VIGA_HOST_PASSWORD' environment variable is unset") 10 | 11 | User = get_user_model() 12 | 13 | superuser_phone_number = "+919999999999" 14 | 15 | help_message = f""" 16 | Sets up the DB, creating: 17 | 1) superuser with admin rights (Email: {email_superuser}) 18 | """ 19 | 20 | 21 | class Command(BaseCommand): 22 | """ 23 | viga_setup: Command to set-up database for the application 24 | """ 25 | help = help_message 26 | 27 | def handle(self, *args, **kwargs): 28 | if not User.objects.filter(email=email_superuser).exists(): 29 | User.objects.create_superuser(first_name="SSO ADMIN", 30 | email=email_superuser, 31 | password=EMAIL_HOST_PASSWORD, 32 | phone_number=superuser_phone_number) 33 | print('Super-User Created!') 34 | print('Viga Set-Up Complete!') 35 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.admin import UserAdmin 2 | from django.contrib import admin 3 | from users.models import User, Organization 4 | 5 | 6 | class CustomUserAdmin(UserAdmin): 7 | ordering = ('id',) 8 | list_display = ('id', 'first_name', 'last_name', 'email', 9 | 'phone_number', 'is_active', 'is_staff', 'organization', 'admin_org') 10 | list_display_links = ('id', 'first_name', 'last_name', 'email') 11 | list_filter = ('is_staff',) 12 | 13 | fieldsets = ( 14 | (None, {'fields': ('password', 'email', 'nickname')}), 15 | ('Personal info', {'fields': ('avatar', 'first_name', 'last_name', 'phone_number')}), 16 | ('Organization Details', {'fields': ('organization', 'admin_org')}), 17 | ('User Details', {'fields': ('is_active', 'is_staff', 'groups')}), 18 | ) 19 | 20 | add_fieldsets = ( 21 | (None, { 22 | 'classes': ('wide',), 23 | 'fields': ('email', 'first_name', 'last_name', 24 | 'phone_number', 'password1', 'password2'), 25 | }), 26 | ) 27 | 28 | 29 | class OrganizationPanel(admin.ModelAdmin): 30 | list_display = ('id', 'name', 'joining_date') 31 | list_display_links = ('id', 'name', 'joining_date') 32 | 33 | 34 | admin.site.register(User, CustomUserAdmin) 35 | admin.site.register(Organization, OrganizationPanel) 36 | -------------------------------------------------------------------------------- /StandAloneService/src/users/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import BasePermission 2 | from rest_framework_simplejwt.models import TokenUser 3 | from django.contrib.auth import get_user_model 4 | 5 | User = get_user_model() 6 | 7 | SSO_ADMIN_EMAIL = 'superuser@vigastudios.com' 8 | 9 | 10 | class IsSSOAdmin(BasePermission): 11 | """ 12 | Custom permission to only allow owners of an object to edit it. 13 | """ 14 | def has_permission(self, request, view): 15 | try: 16 | assert request.user and request.user.is_authenticated 17 | if request.auth.payload.get('sso_admin'): 18 | return True 19 | user = request.user 20 | if isinstance(request.user, TokenUser): 21 | user_set = User.objects.filter(id=user.id) 22 | if user_set.exists(): 23 | user = user_set.first() 24 | else: 25 | return False 26 | return user.email == SSO_ADMIN_EMAIL 27 | except AssertionError: 28 | return False 29 | 30 | 31 | class IsUserOwner(BasePermission): 32 | """ 33 | Custom permission to only allow owners of an object to edit it. 34 | """ 35 | def has_permission(self, request, view): 36 | return request.user and request.user.is_authenticated 37 | 38 | def has_object_permission(self, request, view, user_obj): 39 | return user_obj == request.user 40 | -------------------------------------------------------------------------------- /Single-Sign-On/src/sso_server/urls.py: -------------------------------------------------------------------------------- 1 | """sso_server URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | from django.conf import settings 19 | from django.conf.urls.static import static 20 | from rest_framework_simplejwt import views as jwt_views 21 | from .jwt import MyTokenObtainPairView 22 | 23 | urlpatterns = [ 24 | path('', include('users.urls')), 25 | path('', include('services.urls')), 26 | path('admin/', admin.site.urls), 27 | path('api/token/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'), 28 | path('api/token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'), 29 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 30 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.base_user import BaseUserManager 2 | 3 | 4 | class CustomUserManager(BaseUserManager): 5 | use_in_migrations = True 6 | 7 | def _create_user(self, email, password, **extra_fields): 8 | """ 9 | Creates and saves a User with the given email and password. 10 | """ 11 | if not email: 12 | raise ValueError('User must have an email') 13 | if not password: 14 | raise ValueError('User must have a password') 15 | email = self.normalize_email(email) 16 | user = self.model(email=email, **extra_fields) 17 | user.set_password(password) 18 | user.save(using=self._db) 19 | return user 20 | 21 | def create_user(self, email, password=None, **extra_fields): 22 | extra_fields.setdefault('is_staff', False) 23 | extra_fields.setdefault('is_superuser', False) 24 | return self._create_user(email, password, **extra_fields) 25 | 26 | def create_superuser(self, email, password, **extra_fields): 27 | extra_fields.setdefault('is_staff', True) 28 | extra_fields.setdefault('is_superuser', True) 29 | 30 | if extra_fields.get('is_staff') is not True: 31 | raise ValueError('Superuser must have is_staff=True.') 32 | if extra_fields.get('is_superuser') is not True: 33 | raise ValueError('Superuser must have is_superuser=True.') 34 | 35 | return self._create_user(email, password, **extra_fields) 36 | -------------------------------------------------------------------------------- /StandAloneService/src/users/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.base_user import BaseUserManager 2 | 3 | 4 | class CustomUserManager(BaseUserManager): 5 | use_in_migrations = True 6 | 7 | def _create_user(self, email, password, **extra_fields): 8 | """ 9 | Creates and saves a User with the given email and password. 10 | """ 11 | if not email: 12 | raise ValueError('User must have an email') 13 | if not password: 14 | raise ValueError('User must have a password') 15 | email = self.normalize_email(email) 16 | user = self.model(email=email, **extra_fields) 17 | user.set_password(password) 18 | user.save(using=self._db) 19 | return user 20 | 21 | def create_user(self, email, password=None, **extra_fields): 22 | extra_fields.setdefault('is_staff', False) 23 | extra_fields.setdefault('is_superuser', False) 24 | return self._create_user(email, password, **extra_fields) 25 | 26 | def create_superuser(self, email, password, **extra_fields): 27 | extra_fields.setdefault('is_staff', True) 28 | extra_fields.setdefault('is_superuser', True) 29 | 30 | if extra_fields.get('is_staff') is not True: 31 | raise ValueError('Superuser must have is_staff=True.') 32 | if extra_fields.get('is_superuser') is not True: 33 | raise ValueError('Superuser must have is_superuser=True.') 34 | 35 | return self._create_user(email, password, **extra_fields) 36 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2020-08-25 20:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Connection', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ], 19 | ), 20 | migrations.CreateModel( 21 | name='Service', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('name', models.CharField(max_length=50)), 25 | ('identifier', models.CharField(max_length=10, unique=True)), 26 | ('callback_url', models.URLField()), 27 | ('created_at', models.DateTimeField(auto_now_add=True)), 28 | ], 29 | ), 30 | migrations.CreateModel( 31 | name='Subscription', 32 | fields=[ 33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('created_at', models.DateTimeField(auto_now_add=True)), 35 | ('end_date', models.DateTimeField(blank=True, null=True)), 36 | ('is_active', models.BooleanField(default=False)), 37 | ], 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /Single-Sign-On/README.md: -------------------------------------------------------------------------------- 1 | # Single-Sign-On 2 | 3 | ## Set-Up the Repository 4 | ``` 5 | $ cd Single-Sign-On/ 6 | $ python3 -m venv venv 7 | $ source venv/bin/activate 8 | (venv)$ pip install -r requirements.txt 9 | ``` 10 | 11 | ## Set Environment Variables 12 | 13 | #### Superuser Password 14 | ``` 15 | VIGA_HOST_PASSWORD="..." 16 | ``` 17 | #### Database Configuration 18 | ``` 19 | USE_FILE_BASED_DB=1 20 | ``` 21 | **OR** 22 | ``` 23 | DATABASE_NAME="..." 24 | DATABASE_USERNAME="..." 25 | DATABASE_PASSWORD="..." 26 | DATABASE_HOST="..." 27 | DATABASE_PORT="..." 28 | ``` 29 | 30 | ### Apply Migrations 31 | ``` 32 | $ source venv/bin/activate 33 | (venv)$ cd src/ 34 | (venv)$ python manage.py migrate 35 | ``` 36 | 37 | ### Create the Admin 38 | ``` 39 | (venv)$ cd src/ 40 | (venv)$ python manage.py viga_setup 41 | ``` 42 | 43 | ## Running-Up Server 44 | ``` 45 | $ source venv/bin/activate 46 | (venv)$ cd src/ 47 | (venv)$ python manage.py runserver 48 | ``` 49 | 50 | ## Integrating a `Service` 51 | ### Step 1 52 | - Create a Service 53 | ### Step 2 54 | - Reserve a `callback_url` which receives **`POST`** request 55 | - The `callback_url` will receive User-Profile Data as fields specified in **[UserSerializer](./src/users/serializers.py)** 56 | - This `callback_url` will be hit on two triggers: 1) `Service` Creation 2) `Connection` Creation 57 | - On Service Creation, it will receive SSO-Admin Details (which will be used to authenticate any requests from the SSO in future) 58 | - On Connection Creation, it will receive the User's details which got connected to the service 59 | ### Step 3 60 | - Hit [the URL](./src/services/urls.py) for Service Creation 61 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.password_validation import validate_password 4 | from .models import Organization 5 | 6 | User = get_user_model() 7 | 8 | 9 | class PublicOrganizationSerializer(serializers.ModelSerializer): 10 | 11 | class Meta: 12 | model = Organization 13 | fields = ('id', 'name',) 14 | 15 | 16 | class OrganizationSerializer(serializers.ModelSerializer): 17 | 18 | class Meta: 19 | model = Organization 20 | fields = '__all__' 21 | 22 | 23 | class PublicUserSerializer(serializers.ModelSerializer): 24 | 25 | class Meta: 26 | model = User 27 | fields = ('id', 'avatar', 'email', 'nickname', 28 | 'first_name', 'last_name', 'phone_number',) 29 | 30 | 31 | class UserSerializer(serializers.ModelSerializer): 32 | 33 | is_sso_admin = serializers.ReadOnlyField(source='is_staff') 34 | admin_org = OrganizationSerializer() 35 | organization = PublicOrganizationSerializer() 36 | 37 | class Meta: 38 | model = User 39 | fields = ('id', 'avatar', 'email', 'nickname', 40 | 'first_name', 'last_name', 'phone_number', 41 | 'admin_org', 'organization', 'is_sso_admin',) 42 | read_only_fields = ('admin_org', 'organization',) 43 | 44 | 45 | class CreateUserSerializer(serializers.ModelSerializer): 46 | 47 | class Meta: 48 | model = User 49 | fields = ('avatar', 'email', 'nickname', 50 | 'first_name', 'last_name', 'phone_number', 'password') 51 | 52 | def validate_password(self, value): 53 | validate_password(value) 54 | return value 55 | -------------------------------------------------------------------------------- /StandAloneService/src/users/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.exceptions import ValidationError 3 | from rest_framework.exceptions import APIException 4 | from django.contrib.auth.base_user import AbstractBaseUser 5 | from django.contrib.auth.models import PermissionsMixin 6 | from .managers import CustomUserManager 7 | import uuid 8 | 9 | from django.utils.translation import ugettext_lazy as _ 10 | 11 | 12 | class User(AbstractBaseUser, PermissionsMixin): 13 | """ 14 | Model to store local users in the database. 15 | """ 16 | 17 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name='ID') 18 | email = models.EmailField(_('email address'), unique=True) 19 | first_name = models.CharField(_('first name'), max_length=30) 20 | last_name = models.CharField(_('last name'), max_length=30, blank=True, null=True) 21 | 22 | is_staff = models.BooleanField(_('staff'), default=False) 23 | created_at = models.DateTimeField(auto_now_add=True) 24 | objects = CustomUserManager() 25 | 26 | EMAIL_FIELD = 'email' 27 | USERNAME_FIELD = 'email' 28 | REQUIRED_FIELDS = ['first_name'] 29 | 30 | class Meta: 31 | verbose_name = _('user') 32 | verbose_name_plural = _('users') 33 | 34 | def __str__(self): 35 | user_representation = self.first_name 36 | if self.last_name: 37 | user_representation += f" {self.last_name}" 38 | return user_representation 39 | 40 | def save(self, force_insert=False, force_update=False, using=None, 41 | update_fields=None): 42 | try: 43 | self.clean() 44 | except ValidationError as e: 45 | raise APIException(str(e)) 46 | super(User, self).save() 47 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/migrations/0002_auto_20200826_0141.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2020-08-25 20:11 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('users', '0001_initial'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ('services', '0001_initial'), 16 | ] 17 | 18 | operations = [ 19 | migrations.AddField( 20 | model_name='subscription', 21 | name='org', 22 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_subscriptions', to='users.Organization'), 23 | ), 24 | migrations.AddField( 25 | model_name='subscription', 26 | name='service', 27 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='org_subscriptions', to='services.Service'), 28 | ), 29 | migrations.AddField( 30 | model_name='connection', 31 | name='service', 32 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_connections', to='services.Service'), 33 | ), 34 | migrations.AddField( 35 | model_name='connection', 36 | name='user', 37 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_connections', to=settings.AUTH_USER_MODEL), 38 | ), 39 | migrations.AlterUniqueTogether( 40 | name='subscription', 41 | unique_together={('service', 'org')}, 42 | ), 43 | migrations.AlterUniqueTogether( 44 | name='connection', 45 | unique_together={('service', 'user')}, 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import BasePermission, SAFE_METHODS 2 | from django.contrib.auth import get_user_model 3 | from services.models import Connection 4 | 5 | User = get_user_model() 6 | 7 | 8 | class IsOwner(BasePermission): 9 | """ 10 | Custom permission to only allow owners of an object to edit it. 11 | """ 12 | def has_permission(self, request, view): 13 | return request.user and request.user.is_authenticated 14 | 15 | def has_object_permission(self, request, view, user_obj): 16 | return user_obj == request.user 17 | 18 | 19 | class IsSSOAdminOrReadOnly(BasePermission): 20 | """ 21 | Allows access only to admin users. 22 | """ 23 | def has_permission(self, request, view): 24 | return bool( 25 | request.method in SAFE_METHODS or 26 | request.user and 27 | request.user.is_staff 28 | ) 29 | 30 | 31 | class IsOrganizationAdminOrSSOAdmin(BasePermission): 32 | def has_permission(self, request, view): 33 | return bool( 34 | request.user and 35 | (request.user.is_staff or request.user.admin_org) 36 | ) 37 | 38 | def has_object_permission(self, request, view, obj): 39 | # obj -> Organization 40 | return request.user.is_staff or request.user.admin_org == obj 41 | 42 | 43 | class IsOrganizationAdmin(BasePermission): 44 | 45 | def has_permission(self, request, view): 46 | return bool( 47 | request.user and 48 | request.user.admin_org 49 | ) 50 | 51 | def has_object_permission(self, request, view, obj): 52 | perm = { 53 | Connection: lambda conn: conn.user.organization == request.user.admin_org, 54 | User: lambda user: user.organization == request.user.admin_org, 55 | } 56 | return perm.get(type(obj), lambda x: False)(obj) 57 | -------------------------------------------------------------------------------- /StandAloneService/src/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-09-18 17:08 2 | 3 | from django.db import migrations, models 4 | import users.managers 5 | import uuid 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('auth', '0012_alter_user_first_name_max_length'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='User', 19 | fields=[ 20 | ('password', models.CharField(max_length=128, verbose_name='password')), 21 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 22 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 23 | ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 25 | ('first_name', models.CharField(max_length=30, verbose_name='first name')), 26 | ('last_name', models.CharField(blank=True, max_length=30, null=True, verbose_name='last name')), 27 | ('is_staff', models.BooleanField(default=False, verbose_name='staff')), 28 | ('created_at', models.DateTimeField(auto_now_add=True)), 29 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 30 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 31 | ], 32 | options={ 33 | 'verbose_name': 'user', 34 | 'verbose_name_plural': 'users', 35 | }, 36 | managers=[ 37 | ('objects', users.managers.CustomUserManager()), 38 | ], 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /jwt_example/JWT Example.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "cca9b877-6d8a-4431-acd2-31a3db52486e", 4 | "name": "JWT Example", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Get Token Pair", 10 | "request": { 11 | "method": "POST", 12 | "header": [ 13 | { 14 | "key": "Content-Type", 15 | "name": "Content-Type", 16 | "value": "application/json", 17 | "type": "text" 18 | } 19 | ], 20 | "body": { 21 | "mode": "raw", 22 | "raw": "{\n\t\"username\": \"vibhu\",\n\t\"password\": \"password\"\n}" 23 | }, 24 | "url": { 25 | "raw": "{{url}}/api/token/", 26 | "host": [ 27 | "{{url}}" 28 | ], 29 | "path": [ 30 | "api", 31 | "token", 32 | "" 33 | ] 34 | } 35 | }, 36 | "response": [] 37 | }, 38 | { 39 | "name": "Get Access Token", 40 | "request": { 41 | "method": "POST", 42 | "header": [ 43 | { 44 | "key": "Content-Type", 45 | "name": "Content-Type", 46 | "value": "application/json", 47 | "type": "text" 48 | } 49 | ], 50 | "body": { 51 | "mode": "raw", 52 | "raw": "{\n\t\"refresh\": \"refresh-token\"\n}" 53 | }, 54 | "url": { 55 | "raw": "{{url}}/api/token/refresh/", 56 | "host": [ 57 | "{{url}}" 58 | ], 59 | "path": [ 60 | "api", 61 | "token", 62 | "refresh", 63 | "" 64 | ] 65 | } 66 | }, 67 | "response": [] 68 | }, 69 | { 70 | "name": "Access Protected Resource", 71 | "request": { 72 | "auth": { 73 | "type": "bearer", 74 | "bearer": [ 75 | { 76 | "key": "token", 77 | "value": "Access_Token", 78 | "type": "string" 79 | } 80 | ] 81 | }, 82 | "method": "GET", 83 | "header": [], 84 | "url": { 85 | "raw": "{{url}}/api/protected-resource/", 86 | "host": [ 87 | "{{url}}" 88 | ], 89 | "path": [ 90 | "api", 91 | "protected-resource", 92 | "" 93 | ] 94 | } 95 | }, 96 | "response": [] 97 | } 98 | ], 99 | "auth": { 100 | "type": "bearer", 101 | "bearer": [ 102 | { 103 | "key": "token", 104 | "value": "", 105 | "type": "string" 106 | } 107 | ] 108 | }, 109 | "variable": [ 110 | { 111 | "id": "ab84e7e2-c43c-4047-ad38-4b6d8e779e41", 112 | "key": "url", 113 | "value": "http://127.0.0.1:8001", 114 | "type": "string" 115 | } 116 | ] 117 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Pycharm stuff 132 | .idea/ 133 | 134 | # Configuration Files 135 | config/ 136 | 137 | # Reharsal Content 138 | reharsal/ 139 | 140 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import get_user_model 3 | from rest_framework.exceptions import PermissionDenied 4 | from rest_framework.generics import ( 5 | ListCreateAPIView, RetrieveDestroyAPIView, 6 | CreateAPIView, ListAPIView, 7 | ) 8 | from rest_framework.permissions import IsAuthenticated 9 | from rest_framework.response import Response 10 | from rest_framework.views import APIView 11 | 12 | from users.permissions import (IsSSOAdminOrReadOnly, 13 | IsOrganizationAdminOrSSOAdmin, 14 | IsOrganizationAdmin) 15 | from .models import Service, Connection 16 | from .serializers import (ServiceSerializer, ConnectionSerializer, 17 | PublicServiceSerializer) 18 | 19 | User = get_user_model() 20 | PUBLIC_KEY = open(settings.JWT_PUBLIC_KEY_PATH).read() 21 | 22 | 23 | class FetchPublicKeyAPIView(APIView): 24 | 25 | def get(self, request): 26 | return Response({'key': PUBLIC_KEY}) 27 | 28 | 29 | class ListAllServiceAPIView(ListAPIView): 30 | serializer_class = PublicServiceSerializer 31 | permission_classes = (IsAuthenticated, IsOrganizationAdminOrSSOAdmin,) 32 | queryset = Service.objects.all() 33 | 34 | 35 | class ListCreateServiceAPIView(ListCreateAPIView): 36 | permission_classes = (IsAuthenticated, IsSSOAdminOrReadOnly,) 37 | 38 | def get_queryset(self): 39 | user = self.request.user 40 | return Service.for_user(user) 41 | 42 | def get_serializer_class(self): 43 | ser = { 44 | 'GET': PublicServiceSerializer, 45 | 'POST': ServiceSerializer 46 | } 47 | return ser[self.request.method] 48 | 49 | 50 | class CreateConnectionAPIView(CreateAPIView): 51 | serializer_class = ConnectionSerializer 52 | permission_classes = (IsAuthenticated, IsOrganizationAdmin) 53 | 54 | def perform_create(self, serializer): 55 | service = serializer.validated_data['service'] 56 | user = serializer.validated_data['user'] 57 | if user.organization != self.request.user.organization: 58 | raise PermissionDenied(f"{user} does not belong to your organization") 59 | if service.org_subscriptions.filter(org=self.request.user.organization).count() == 0: 60 | raise PermissionDenied(f"Your organization is not subscribed to {service}") 61 | serializer.save() 62 | 63 | 64 | class ConnectionDetailAPIView(RetrieveDestroyAPIView): 65 | serializer_class = ConnectionSerializer 66 | permission_classes = (IsAuthenticated, IsOrganizationAdmin) 67 | queryset = Connection.objects.all() 68 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import status 3 | from rest_framework.generics import (RetrieveUpdateDestroyAPIView, ListAPIView, 4 | ListCreateAPIView, RetrieveAPIView) 5 | from rest_framework.permissions import (IsAuthenticated, SAFE_METHODS) 6 | from rest_framework.response import Response 7 | from rest_framework.views import APIView 8 | 9 | from users.models import Organization 10 | from users.permissions import (IsOwner, IsOrganizationAdminOrSSOAdmin, 11 | IsOrganizationAdmin, IsSSOAdminOrReadOnly) 12 | from users.serializers import (CreateUserSerializer, UserSerializer, 13 | OrganizationSerializer, PublicOrganizationSerializer, 14 | PublicUserSerializer) 15 | 16 | User = get_user_model() 17 | 18 | 19 | class ListCreateOrganizationsAPIView(ListCreateAPIView): 20 | permission_classes = (IsAuthenticated, IsSSOAdminOrReadOnly,) 21 | 22 | def get_serializer_class(self): 23 | if self.request.method in SAFE_METHODS: 24 | return PublicOrganizationSerializer 25 | else: 26 | return OrganizationSerializer 27 | 28 | def get_queryset(self): 29 | return Organization.objects.all() 30 | 31 | 32 | class OrganizationUsersAPIView(ListAPIView): 33 | 34 | permission_classes = (IsAuthenticated, IsOrganizationAdmin,) 35 | serializer_class = PublicUserSerializer 36 | 37 | def get_queryset(self): 38 | request_user = self.request.user 39 | return request_user.admin_org.users.exclude(id=request_user.id) 40 | 41 | 42 | class ListAllUsersAPIView(ListAPIView): 43 | serializer_class = PublicUserSerializer 44 | 45 | def get_queryset(self): 46 | request_user = self.request.user 47 | return User.objects.exclude(id=request_user.id) 48 | 49 | 50 | class OrganizationProfileGetView(RetrieveAPIView): 51 | permission_classes = (IsAuthenticated,) 52 | queryset = Organization.objects.all() 53 | 54 | def retrieve(self, request, *args, **kwargs): 55 | organization = self.get_object() 56 | serializer = PublicOrganizationSerializer(organization) 57 | if IsOrganizationAdminOrSSOAdmin().has_object_permission(request, self, organization): 58 | serializer = OrganizationSerializer(organization) 59 | return Response(serializer.data) 60 | 61 | 62 | class UserProfileView(RetrieveUpdateDestroyAPIView): 63 | permission_classes = (IsAuthenticated, IsOwner,) 64 | queryset = User.objects.all() 65 | serializer_class = UserSerializer 66 | 67 | def get_object(self): 68 | return self.request.user 69 | 70 | 71 | class UserSignUpView(APIView): 72 | 73 | def post(self, request, *args, **kwargs): 74 | serialized = CreateUserSerializer(data=request.data) 75 | if serialized.is_valid(): 76 | serializer_data = serialized.validated_data 77 | User.objects.create_user(**serializer_data) 78 | serializer_data = serialized.data 79 | serializer_data.pop('password') 80 | return Response(serializer_data, status=status.HTTP_201_CREATED) 81 | else: 82 | return Response(serialized.errors, status=status.HTTP_400_BAD_REQUEST) 83 | -------------------------------------------------------------------------------- /jwt_example/src/jwt_server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for jwt_server project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '!#t7q0-a1p$sni8k=#0*bj0a66xyb@c*02_bv%8&q58_n+z)be' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'rest_framework' 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'jwt_server.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'jwt_server.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': BASE_DIR / 'db.sqlite3', 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | 123 | REST_FRAMEWORK = { 124 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 125 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 126 | ], 127 | } 128 | 129 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.contrib.auth.base_user import AbstractBaseUser 4 | from django.contrib.auth.models import PermissionsMixin 5 | from django.core.exceptions import ValidationError 6 | from django.db import models 7 | from django.utils.translation import ugettext_lazy as _ 8 | from phonenumber_field.modelfields import PhoneNumberField 9 | from rest_framework.exceptions import APIException 10 | 11 | from .managers import CustomUserManager 12 | 13 | email_superuser = 'superuser@vigastudios.com' 14 | 15 | 16 | class Organization(models.Model): 17 | 18 | name = models.CharField(max_length=50) 19 | joining_date = models.DateTimeField(auto_now_add=True) 20 | updated_at = models.DateTimeField(auto_now=True) 21 | 22 | def __str__(self): 23 | return self.name 24 | 25 | 26 | class User(AbstractBaseUser, PermissionsMixin): 27 | """ 28 | Model to store all kinds of users in the database. 29 | """ 30 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, 31 | serialize=False, verbose_name='ID') 32 | email = models.EmailField(_('email address'), unique=True) 33 | avatar = models.ImageField(upload_to='static', null=True, blank=True) 34 | first_name = models.CharField(_('first name'), max_length=30) 35 | last_name = models.CharField(_('last name'), max_length=30, blank=True, null=True) 36 | nickname = models.CharField(_('nickname'), max_length=30, blank=True, null=True) 37 | phone_number = PhoneNumberField() 38 | organization = models.ForeignKey(Organization, on_delete=models.SET_NULL, 39 | related_name='users', null=True, blank=True) 40 | admin_org = models.OneToOneField(Organization, on_delete=models.SET_NULL, 41 | related_name='admin', null=True, blank=True) 42 | 43 | is_active = models.BooleanField(_('is active'), default=True) 44 | is_staff = models.BooleanField(_('staff'), default=False) 45 | created_at = models.DateTimeField(auto_now_add=True) 46 | objects = CustomUserManager() 47 | 48 | EMAIL_FIELD = 'email' 49 | USERNAME_FIELD = 'email' 50 | REQUIRED_FIELDS = ['first_name', 'phone_number'] 51 | 52 | class Meta: 53 | verbose_name = _('user') 54 | verbose_name_plural = _('users') 55 | 56 | def __str__(self): 57 | user_representation = self.first_name 58 | if self.last_name: 59 | user_representation += f" {self.last_name}" 60 | return user_representation 61 | 62 | def save(self, force_insert=False, force_update=False, using=None, 63 | update_fields=None): 64 | try: 65 | self.clean() 66 | except ValidationError as e: 67 | raise APIException(str(e)) 68 | if self.admin_org: 69 | if self.organization: 70 | if self.organization != self.admin_org: 71 | raise APIException(str(f'User is a part of {self.organization}')) 72 | else: 73 | self.organization = self.admin_org 74 | super(User, self).save() 75 | 76 | 77 | class OrganizationJoinRequest(models.Model): 78 | 79 | org = models.ForeignKey(Organization, on_delete=models.CASCADE, 80 | related_name='join_requests') 81 | user = models.ForeignKey(User, on_delete=models.CASCADE, 82 | related_name='org_requests') 83 | 84 | class Meta: 85 | unique_together = ['org', 'user'] 86 | 87 | def save(self, force_insert=False, force_update=False, using=None, 88 | update_fields=None): 89 | if self.user.organization == self.org: 90 | raise APIException(f'User is already a member of {self.org}') 91 | super().save(force_insert=False, force_update=False, using=None, 92 | update_fields=None) 93 | 94 | def accept(self): 95 | self.user.organization = self.org 96 | self.user.save() 97 | self.delete() 98 | -------------------------------------------------------------------------------- /Single-Sign-On/src/services/models.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from django.contrib.auth import get_user_model 3 | from django.db import models 4 | from rest_framework.exceptions import APIException 5 | from rest_framework_simplejwt.tokens import RefreshToken 6 | 7 | from users.models import Organization, email_superuser 8 | from users.serializers import UserSerializer 9 | 10 | User = get_user_model() 11 | 12 | 13 | def make_request_to_callback_url(service, user: User = None): 14 | admin_SSO = User.objects.get(email=email_superuser) 15 | token = RefreshToken.for_user(admin_SSO) 16 | token['aud'] = service.identifier 17 | if user is None: 18 | user = admin_SSO 19 | token['sso_admin'] = True 20 | headers = { 21 | 'Authorization': f"Bearer {token.access_token}" 22 | } 23 | user_data = UserSerializer(user).data 24 | response = requests.post(service.callback_url, data=user_data, headers=headers) 25 | if not response.ok: 26 | raise APIException(f"Call to Service ({service}) Failed!" 27 | f"Returned with status code: {response.status_code}") 28 | 29 | 30 | class Service(models.Model): 31 | 32 | name = models.CharField(max_length=50) 33 | identifier = models.CharField(max_length=10, unique=True) 34 | callback_url = models.URLField() 35 | created_at = models.DateTimeField(auto_now_add=True) 36 | 37 | def __str__(self): 38 | return self.name 39 | 40 | def save(self, force_insert=False, force_update=False, using=None, 41 | update_fields=None): 42 | if self._state.adding: 43 | make_request_to_callback_url(self) 44 | super(Service, self).save(force_insert=force_insert, force_update=force_update, 45 | using=using, update_fields=update_fields) 46 | 47 | @classmethod 48 | def for_user(cls, user: User): 49 | # Returns Queryset of Services the 50 | # User is connected to 51 | if not user.is_staff: 52 | org = user.organization 53 | if org: 54 | org_services = cls.objects.filter(org_subscriptions__org=org, 55 | org_subscriptions__is_active=True) 56 | user_services = org_services.filter(user_connections__user=user) 57 | else: 58 | return cls.objects.none() 59 | else: 60 | user_services = cls.objects.all() 61 | return user_services 62 | 63 | 64 | class Connection(models.Model): 65 | 66 | user = models.ForeignKey(User, on_delete=models.CASCADE, 67 | related_name='service_connections') 68 | service = models.ForeignKey(Service, on_delete=models.CASCADE, 69 | related_name='user_connections') 70 | 71 | class Meta: 72 | unique_together = ['service', 'user'] 73 | 74 | def __str__(self): 75 | return f"{self.user} | {self.service}" 76 | 77 | def save(self, force_insert=False, force_update=False, using=None, 78 | update_fields=None): 79 | if self._state.adding: 80 | make_request_to_callback_url(self.service, self.user) 81 | super(Connection, self).save(force_insert=force_insert, force_update=force_update, 82 | using=using, update_fields=update_fields) 83 | 84 | 85 | class Subscription(models.Model): 86 | 87 | service = models.ForeignKey(Service, on_delete=models.CASCADE, 88 | related_name='org_subscriptions') 89 | org = models.ForeignKey(Organization, on_delete=models.CASCADE, 90 | related_name='service_subscriptions') 91 | created_at = models.DateTimeField(auto_now_add=True) 92 | end_date = models.DateTimeField(null=True, blank=True) 93 | is_active = models.BooleanField(default=False) 94 | 95 | class Meta: 96 | unique_together = ['service', 'org'] 97 | 98 | def __str__(self): 99 | return f"{self.service} | {self.org}" 100 | -------------------------------------------------------------------------------- /Single-Sign-On/src/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2020-08-25 20:11 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import phonenumber_field.modelfields 7 | import users.managers 8 | import uuid 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | ('auth', '0011_update_proxy_permissions'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='Organization', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('name', models.CharField(max_length=50)), 25 | ('joining_date', models.DateTimeField(auto_now_add=True)), 26 | ('updated_at', models.DateTimeField(auto_now=True)), 27 | ], 28 | ), 29 | migrations.CreateModel( 30 | name='User', 31 | fields=[ 32 | ('password', models.CharField(max_length=128, verbose_name='password')), 33 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 34 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 35 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 37 | ('avatar', models.ImageField(blank=True, null=True, upload_to='static')), 38 | ('first_name', models.CharField(max_length=30, verbose_name='first name')), 39 | ('last_name', models.CharField(blank=True, max_length=30, null=True, verbose_name='last name')), 40 | ('nickname', models.CharField(blank=True, max_length=30, null=True, verbose_name='nickname')), 41 | ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region=None)), 42 | ('is_active', models.BooleanField(default=True, verbose_name='is active')), 43 | ('is_staff', models.BooleanField(default=False, verbose_name='staff')), 44 | ('created_at', models.DateTimeField(auto_now_add=True)), 45 | ('admin_org', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='admin', to='users.Organization')), 46 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 47 | ('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='users.Organization')), 48 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 49 | ], 50 | options={ 51 | 'verbose_name': 'user', 52 | 'verbose_name_plural': 'users', 53 | }, 54 | managers=[ 55 | ('objects', users.managers.CustomUserManager()), 56 | ], 57 | ), 58 | migrations.CreateModel( 59 | name='OrganizationJoinRequest', 60 | fields=[ 61 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 62 | ('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='join_requests', to='users.Organization')), 63 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='org_requests', to=settings.AUTH_USER_MODEL)), 64 | ], 65 | options={ 66 | 'unique_together': {('org', 'user')}, 67 | }, 68 | ), 69 | ] 70 | -------------------------------------------------------------------------------- /StandAloneService/src/service_server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for service_server project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | from datetime import timedelta 15 | from pathlib import Path 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = '6zac#glv%p-b58f8rin0g3^630l9w-g1gz=g(km2t7b9c_rayp' 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | ALLOWED_HOSTS = [] 31 | 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | 'rest_framework', 43 | 'users', 44 | 'api_app' 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'service_server.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'service_server.wsgi.application' 76 | AUTH_USER_MODEL = 'users.User' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': BASE_DIR / 'db.sqlite3', 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | SSO_JWT_PUBLIC_KEY_PATH = os.path.join(Path(BASE_DIR).parent, 'config', 'sso_jwt_key.pub') 109 | 110 | SIMPLE_JWT = { 111 | 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), 112 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 113 | 'ROTATE_REFRESH_TOKENS': False, 114 | 115 | 'ALGORITHM': 'RS256', 116 | 'SIGNING_KEY': None, 117 | 'VERIFYING_KEY': open(SSO_JWT_PUBLIC_KEY_PATH).read(), 118 | 'AUDIENCE': None, 119 | 'ISSUER': None, 120 | 121 | 'AUTH_HEADER_TYPES': ('Bearer',), 122 | 'USER_ID_FIELD': 'id', 123 | 'USER_ID_CLAIM': 'user_id', 124 | 125 | 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), 126 | 'TOKEN_TYPE_CLAIM': 'token_type', 127 | 128 | 'JTI_CLAIM': 'jti', 129 | } 130 | 131 | REST_FRAMEWORK = { 132 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 133 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 134 | ], 135 | } 136 | 137 | # Internationalization 138 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 139 | 140 | LANGUAGE_CODE = 'en-us' 141 | 142 | TIME_ZONE = 'Asia/Kolkata' 143 | 144 | USE_I18N = True 145 | 146 | USE_L10N = True 147 | 148 | USE_TZ = True 149 | 150 | 151 | # Static files (CSS, JavaScript, Images) 152 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 153 | 154 | STATIC_URL = '/static/' 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Developing-a-Single-Sign-On-Service-using-Django 3 | 4 | | Conference | Date and Time | Venue | Slide Deck | Video 5 | |--|--|--|--|--| 6 | | [**PyCon Sweden 2020**](http://www.pycon.se/2020/) | 14:15 CET, November 12, 2020 | Online | [Link](https://speakerdeck.com/vibhu4agarwal/developing-an-sso-service-using-django-pycon-sweden-2020) | [YouTube](https://youtu.be/4BN2Np7fUqY) | 7 | | [**PyCon India 2020**](https://in.pycon.org/2020/) | 15:20 IST, October 3, 2020 | Online | [Link](https://speakerdeck.com/vibhu4agarwal/developing-an-sso-service-using-django) | [YouTube](https://youtu.be/JOMHAJNiQg8), [Hopin](https://app.hopin.to/events/pyconindia2020/) | 8 | 9 | 10 | ## **Description:** 11 | 12 | Single-Sign-On **(SSO)** allows users to authenticate with a single ID and password to any of several related, yet independent, software systems.[[1]](#references) Google's authentication system is one such example through which it allows users to sign-in to YouTube, G-Mail, Docs and several other products. 13 | 14 | We'll be discussing how a SSO works and how it can be designed, architected and implemented in Python using Django (REST Framework). This will also feature the particular implementation, being used at [Viga Studios](https://vigastudios.com/) to develop a SSO service for all of their products. 15 | 16 | ### Who's this talk for? 17 | 18 | - Anyone who's curious to know what goes on behind services like _'One-Account for all of Google'_ 19 | - Anyone who wants to know how a Single-Sign-On can be implemented for their own business 20 | - Anyone who wants to maintain a central database for storing their user data for a bunch of applications under them 21 | - Anyone who wants a way to separate their auth-server from their application-specific back-end 22 | - Anyone who wants to dive deep into authentication with Django 23 | 24 | ## How SSO works 25 | 26 | - The SSO is developed to provide a single point for managing authorization and authentication for individual services which can be on any platform: Mobile, Desktop or Web. The SSO service handles all the authorization part and _most_ of the authentication part is carried out by individual services based on the particular service's use-case. 27 | - Users are redirected to SSO when requested for resources which need authentication. Authentication is then handled by the SSO following some protocol (most common ones listed below). 28 | - Sessions store the data for making further authorized requests and can be maintained at different points: SSO-level, Local Session or Identity Provider Session. 29 | 30 | #### Different Protocols 31 | 32 | - [Security Assertion Markup Language (SAML)](https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language) 33 | - [Lightweight Directory Access Protocol](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) 34 | - [OpenID Connect](https://openid.net/connect/) 35 | 36 | #### OpenID Connect (OIDC) 37 | 38 | > OIDC is an authentication protocol, based on the OAuth 2.0 family of specifications. It uses simple JSON Web Tokens (JWT), which can be obtained using flows conforming to the [OAuth 2.0 specifications](https://www.oauth.com/oauth2-servers/map-oauth-2-0-specs/).[[2]](#references)[[3]](#references) 39 | 40 | - **Access Tokens** are credentials used to access protected resources. An access token is a string representing an authorization issued to the client.[[4]](#references) 41 | - **Refresh Tokens** are credentials used to obtain access tokens.[[4]](#references) 42 | 43 | We'll be following OIDC and using [JSON Web Tokens (JWT)](https://jwt.io/) for transferring [Access Tokens](https://tools.ietf.org/html/rfc6749#section-1.4) and [Refresh Tokens](https://tools.ietf.org/html/rfc6749#section-1.5) through HTTP(s). We'll also have a short demo using [Postman](https://www.postman.com/) to see how to use JWT. 44 | 45 | ### Using Django to develop a SSO service 46 | 47 | We will walk through each of these sections discussing the implementation, what was the need and **why** a particular method was adopted. 48 | 49 | 50 | Discussion and a short demo on **Access and Refresh Tokens** 51 | 52 | - Using [JWT](https://jwt.io/) with [Django-Rest-Framework **(DRF)**](https://www.django-rest-framework.org/) 53 | - Customizing Token Claims (adding custom properties or key-value pairs in generated tokens) 54 | 55 | Introduction to **Asymmetric Keys** and their usage 56 | 57 | - The need for using asymmetric algorithms for encryption 58 | - Using [cryptography](https://cryptography.io/en/latest/hazmat/primitives/asymmetric/) for generating public and private keys 59 | - Private-keys can be used to decrypt messages which were encrypted with the _corresponding Public-key_, as well as to create signatures, which can be verified with the _corresponding Public-key_ [[5]](#references) 60 | 61 | **Designing Database**: Walk through the UML of the project 62 | 63 | - Key [models](https://docs.djangoproject.com/en/3.0/topics/db/models/) needed to set-up the service 64 | 65 | **Using Business-Specific Permissions and developing APIs** (Code Walk-through) 66 | 67 | - Writing [Custom Permssions](https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions) 68 | - Quickly develop APIs using DRF's [Generic-API-Views](https://www.django-rest-framework.org/api-guide/generic-views/) 69 | 70 | **Integrating Services** 71 | 72 | - Configuring SSO to integrate individual services 73 | - As the new services and products are created, their integration with SSO should require minimum effort and how we can configure the SSO to do that 74 | 75 | ### **Prerequisites:** 76 | 77 | - Basic understanding of [JWT](https://jwt.io/introduction/) 78 | - Basic understanding of (RESTful) Web APIs 79 | - To understand the last 30% of the talk, basic knowledge [Django](https://www.djangoproject.com/) and [REST Framework](https://www.django-rest-framework.org/) 80 | 81 | #### References: 82 | 1. Single-Sign-On - [Wikipedia](https://en.wikipedia.org/wiki/Single_sign-on) 83 | 2. OpenID Connect - [Auth0](https://auth0.com/docs/sso/current#openid-connect) 84 | 3. Map of Oauth 2.0 Specs - [oauth.com](https://www.oauth.com/oauth2-servers/map-oauth-2-0-specs/) 85 | 4. [Access Tokens](https://tools.ietf.org/html/rfc6749#section-1.4) and [Refresh Tokens](https://tools.ietf.org/html/rfc6749#section-1.5) - IETF RFC #6749 86 | 5. Public and Private Keys - [pyca/cryptography Docs](https://cryptography.io/en/latest/glossary/#term-private-key) 87 | 88 | ### **Speaker Info:** 89 | 90 | I am an avid Pythonista and an Open-Source Enthusiast, currently working at [Viga Studios](https://vigastudios.com/) as a back-end developer, Intern. I've always been a community guy, organizing workshops in college on weekdays and spending weekends attending meetups and conferences all over Delhi-NCR at PyDelhi, PyData, ILUG-D, AWS and GDG. 91 | 92 | ### **Speaker Links:** 93 | 94 | - [Talk on Google Cloud](https://www.linkedin.com/pulse/google-cloud-program-vibhu-agarwal/) at Noida, October 2019 95 | - [A session on Web Scraping with Python](https://github.com/InternityFoundation/Web-Scraping-Session-04-01-2019/), January 2019 96 | - [Portfolio](https://vibhu-agarwal.github.io/) | [Twitter](https://twitter.com/vibhu4agarwal) | [GitHub](https://github.com/Vibhu-Agarwal) | [Playstore](https://play.google.com/store/apps/developer?id=Vibhu%20Agarwal) | [Projects](https://vibhu-agarwal.github.io/projects/) 97 | -------------------------------------------------------------------------------- /Single-Sign-On/src/sso_server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for sso_server project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | from datetime import timedelta 15 | from pathlib import Path 16 | 17 | from cryptography.hazmat.primitives import serialization 18 | from cryptography.hazmat.backends import default_backend 19 | from cryptography.hazmat.primitives.asymmetric import rsa 20 | 21 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 22 | BASE_DIR = Path(__file__).resolve().parent.parent 23 | 24 | 25 | # Quick-start development settings - unsuitable for production 26 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 27 | 28 | # SECURITY WARNING: keep the secret key used in production secret! 29 | SECRET_KEY = 'h83#*q+*4208r4=@*db%-7q#tl6wx$9e8+u6t*!#sk%!*9_g+s' 30 | 31 | # SECURITY WARNING: don't run with debug turned on in production! 32 | DEBUG = True 33 | 34 | ALLOWED_HOSTS = ['*'] 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'django.contrib.admin', 41 | 'django.contrib.auth', 42 | 'django.contrib.contenttypes', 43 | 'django.contrib.sessions', 44 | 'django.contrib.messages', 45 | 'django.contrib.staticfiles', 46 | 'rest_framework', 47 | 'rest_framework.authtoken', 48 | 'users', 49 | 'services', 50 | 'corsheaders' 51 | ] 52 | 53 | MIDDLEWARE = [ 54 | 'corsheaders.middleware.CorsMiddleware', 55 | 'django.middleware.security.SecurityMiddleware', 56 | 'django.contrib.sessions.middleware.SessionMiddleware', 57 | 'django.middleware.common.CommonMiddleware', 58 | 'django.middleware.csrf.CsrfViewMiddleware', 59 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 60 | 'django.contrib.messages.middleware.MessageMiddleware', 61 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 62 | ] 63 | 64 | CORS_ORIGIN_ALLOW_ALL = True 65 | 66 | ROOT_URLCONF = 'sso_server.urls' 67 | 68 | TEMPLATES = [ 69 | { 70 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 71 | 'DIRS': [], 72 | 'APP_DIRS': True, 73 | 'OPTIONS': { 74 | 'context_processors': [ 75 | 'django.template.context_processors.debug', 76 | 'django.template.context_processors.request', 77 | 'django.contrib.auth.context_processors.auth', 78 | 'django.contrib.messages.context_processors.messages', 79 | ], 80 | }, 81 | }, 82 | ] 83 | 84 | WSGI_APPLICATION = 'sso_server.wsgi.application' 85 | AUTH_USER_MODEL = 'users.User' 86 | 87 | 88 | # Database 89 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 90 | local_db_config = { 91 | 'ENGINE': 'django.db.backends.sqlite3', 92 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 93 | } 94 | 95 | prod_db_config = { 96 | 'ENGINE': 'django.db.backends.postgresql', 97 | 'NAME': os.getenv('DATABASE_NAME', 'postgres'), 98 | 'USER': os.getenv('DATABASE_USERNAME', 'postgres'), 99 | 'PASSWORD': os.getenv('DATABASE_PASSWORD', 'postgres'), 100 | 'HOST': os.getenv('DATABASE_HOST', 'db'), 101 | 'PORT': os.getenv('DATABASE_PORT', 5432), 102 | } 103 | 104 | use_file_based_db = os.getenv('USE_FILE_BASED_DB', False) 105 | db_config = local_db_config if use_file_based_db else prod_db_config 106 | 107 | DATABASES = { 108 | 'default': db_config 109 | } 110 | 111 | 112 | # Password validation 113 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 114 | 115 | AUTH_PASSWORD_VALIDATORS = [ 116 | { 117 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 118 | }, 119 | { 120 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 121 | }, 122 | { 123 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 124 | }, 125 | { 126 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 127 | }, 128 | ] 129 | 130 | CONFIG_DIR = os.path.join(Path(BASE_DIR).parent, 'config') 131 | JWT_PRIVATE_KEY_PATH = os.path.join(CONFIG_DIR, 'jwt_key') 132 | JWT_PUBLIC_KEY_PATH = os.path.join(CONFIG_DIR, 'jwt_key.pub') 133 | 134 | if (not os.path.exists(JWT_PRIVATE_KEY_PATH)) or (not os.path.exists(JWT_PUBLIC_KEY_PATH)): 135 | 136 | if not os.path.exists(CONFIG_DIR): 137 | os.makedirs(CONFIG_DIR) 138 | 139 | private_key = rsa.generate_private_key( 140 | public_exponent=65537, 141 | key_size=4096, 142 | backend=default_backend() 143 | ) 144 | pem = private_key.private_bytes( 145 | encoding=serialization.Encoding.PEM, 146 | format=serialization.PrivateFormat.TraditionalOpenSSL, 147 | encryption_algorithm=serialization.NoEncryption() 148 | ) 149 | with open(JWT_PRIVATE_KEY_PATH, 'w') as pk: 150 | pk.write(pem.decode()) 151 | 152 | public_key = private_key.public_key() 153 | pem_public = public_key.public_bytes( 154 | encoding=serialization.Encoding.PEM, 155 | format=serialization.PublicFormat.SubjectPublicKeyInfo 156 | ) 157 | with open(JWT_PUBLIC_KEY_PATH, 'w') as pk: 158 | pk.write(pem_public.decode()) 159 | print('PUBLIC/PRIVATE keys Generated!') 160 | 161 | # Visit this page to see all the registered JWT claims: 162 | # https://tools.ietf.org/html/rfc7519#section-4.1 163 | SIMPLE_JWT = { 164 | 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # "exp" (Expiration Time) Claim 165 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # "exp" (Expiration Time) Claim 166 | 167 | 'ALGORITHM': 'RS256', # 'alg' (Algorithm Used) specified in header 168 | 169 | 'SIGNING_KEY': open(JWT_PRIVATE_KEY_PATH).read(), 170 | 'VERIFYING_KEY': open(JWT_PUBLIC_KEY_PATH).read(), 171 | 172 | 'AUDIENCE': None, # "aud" (Audience) Claim 173 | 'ISSUER': None, # "iss" (Issuer) Claim 174 | 175 | 'USER_ID_CLAIM': 'user_id', # The field name to use for identifying user 176 | 'USER_ID_FIELD': 'id', # The field in the DB which will be filled in USER_ID_CLAIM 177 | 178 | 'JTI_CLAIM': 'jti', # A token’s unique identifier 179 | 180 | 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), 181 | 'TOKEN_TYPE_CLAIM': 'token_type', 182 | 'AUTH_HEADER_TYPES': ('Bearer',), 183 | 'ROTATE_REFRESH_TOKENS': False, 184 | } 185 | # A JWT access-token example 186 | # 187 | # { 188 | # 'token_type': 'access', 189 | # 'exp': 1599980514, 190 | # 'jti': 'a3c77262a57d4df5a657fe12860c8492', 191 | # 'user_id': '9a7b69e2-df4d-4c98-bc04-941c66cff1a0', 192 | # } 193 | # 194 | 195 | REST_FRAMEWORK = { 196 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 197 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 198 | ], 199 | } 200 | 201 | 202 | # Internationalization 203 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 204 | 205 | LANGUAGE_CODE = 'en-us' 206 | 207 | TIME_ZONE = 'Asia/Kolkata' 208 | 209 | USE_I18N = True 210 | 211 | USE_L10N = True 212 | 213 | USE_TZ = True 214 | 215 | 216 | # Static files (CSS, JavaScript, Images) 217 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 218 | STATICFILES_DIRS = ( 219 | os.path.join(BASE_DIR, 'static'), 220 | ) 221 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 222 | STATIC_URL = '/static/' 223 | 224 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 225 | MEDIA_URL = '/media/' 226 | -------------------------------------------------------------------------------- /Single-Sign-On/Single-Sign-On.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "b96c0bb8-f8ef-43a4-ae3a-2164d328b6f9", 4 | "name": "Single-Sign-On", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Sign-Up", 10 | "request": { 11 | "method": "POST", 12 | "header": [ 13 | { 14 | "key": "Content-Type", 15 | "name": "Content-Type", 16 | "value": "application/json", 17 | "type": "text" 18 | } 19 | ], 20 | "body": { 21 | "mode": "raw", 22 | "raw": "{\n\t\"email\": \"testuser@vigastudios.com\",\n\t\"first_name\": \"Test User 1\",\n\t\"phone_number\": \"+917777777777\",\n\t\"password\": \"passoword\"\n}" 23 | }, 24 | "url": { 25 | "raw": "{{url}}/signup/", 26 | "host": [ 27 | "{{url}}" 28 | ], 29 | "path": [ 30 | "signup", 31 | "" 32 | ] 33 | } 34 | }, 35 | "response": [] 36 | }, 37 | { 38 | "name": "Login", 39 | "request": { 40 | "method": "POST", 41 | "header": [ 42 | { 43 | "key": "Content-Type", 44 | "name": "Content-Type", 45 | "value": "application/json", 46 | "type": "text" 47 | } 48 | ], 49 | "body": { 50 | "mode": "raw", 51 | "raw": "{\n\t\"email\": \"orgadmin@vigastudios.com\",\n\t\"password\": \"cheezyadmin\"\n}" 52 | }, 53 | "url": { 54 | "raw": "{{url}}/api/token/", 55 | "host": [ 56 | "{{url}}" 57 | ], 58 | "path": [ 59 | "api", 60 | "token", 61 | "" 62 | ] 63 | } 64 | }, 65 | "response": [] 66 | }, 67 | { 68 | "name": "Get New Access Token", 69 | "request": { 70 | "method": "POST", 71 | "header": [ 72 | { 73 | "key": "Content-Type", 74 | "name": "Content-Type", 75 | "value": "application/json", 76 | "type": "text" 77 | } 78 | ], 79 | "body": { 80 | "mode": "raw", 81 | "raw": "{\n\t\"refresh\": \"refresh-token\"\n}" 82 | }, 83 | "url": { 84 | "raw": "{{url}}/api/token/refresh/", 85 | "host": [ 86 | "{{url}}" 87 | ], 88 | "path": [ 89 | "api", 90 | "token", 91 | "refresh", 92 | "" 93 | ] 94 | } 95 | }, 96 | "response": [] 97 | }, 98 | { 99 | "name": "Organization Users", 100 | "request": { 101 | "auth": { 102 | "type": "bearer", 103 | "bearer": [ 104 | { 105 | "key": "token", 106 | "value": "", 107 | "type": "string" 108 | } 109 | ] 110 | }, 111 | "method": "GET", 112 | "header": [], 113 | "url": { 114 | "raw": "{{url}}/organizations/users/", 115 | "host": [ 116 | "{{url}}" 117 | ], 118 | "path": [ 119 | "organizations", 120 | "users", 121 | "" 122 | ] 123 | } 124 | }, 125 | "response": [] 126 | }, 127 | { 128 | "name": "Fetch all Services", 129 | "request": { 130 | "auth": { 131 | "type": "bearer", 132 | "bearer": [ 133 | { 134 | "key": "token", 135 | "value": "", 136 | "type": "string" 137 | } 138 | ] 139 | }, 140 | "method": "GET", 141 | "header": [], 142 | "url": { 143 | "raw": "{{url}}/service/all/", 144 | "host": [ 145 | "{{url}}" 146 | ], 147 | "path": [ 148 | "service", 149 | "all", 150 | "" 151 | ] 152 | } 153 | }, 154 | "response": [] 155 | }, 156 | { 157 | "name": "Create Connection", 158 | "request": { 159 | "auth": { 160 | "type": "bearer", 161 | "bearer": [ 162 | { 163 | "key": "token", 164 | "value": "", 165 | "type": "string" 166 | } 167 | ] 168 | }, 169 | "method": "POST", 170 | "header": [ 171 | { 172 | "key": "Content-Type", 173 | "name": "Content-Type", 174 | "value": "application/json", 175 | "type": "text" 176 | } 177 | ], 178 | "body": { 179 | "mode": "raw", 180 | "raw": "{\n\t\"user\": \"user-id\",\n\t\"service\": \"service-id\"\n}" 181 | }, 182 | "url": { 183 | "raw": "{{url}}/connection/new/", 184 | "host": [ 185 | "{{url}}" 186 | ], 187 | "path": [ 188 | "connection", 189 | "new", 190 | "" 191 | ] 192 | } 193 | }, 194 | "response": [] 195 | }, 196 | { 197 | "name": "Fetch Public Key", 198 | "request": { 199 | "auth": { 200 | "type": "noauth" 201 | }, 202 | "method": "GET", 203 | "header": [], 204 | "url": { 205 | "raw": "{{url}}/fetch-public-key/", 206 | "host": [ 207 | "{{url}}" 208 | ], 209 | "path": [ 210 | "fetch-public-key", 211 | "" 212 | ] 213 | } 214 | }, 215 | "response": [] 216 | }, 217 | { 218 | "name": "List and Create Services", 219 | "request": { 220 | "auth": { 221 | "type": "bearer", 222 | "bearer": [ 223 | { 224 | "key": "token", 225 | "value": "", 226 | "type": "string" 227 | } 228 | ] 229 | }, 230 | "method": "POST", 231 | "header": [ 232 | { 233 | "key": "Content-Type", 234 | "name": "Content-Type", 235 | "value": "application/json", 236 | "type": "text" 237 | } 238 | ], 239 | "body": { 240 | "mode": "raw", 241 | "raw": "{\n\t\"name\": \"Service Name\",\n\t\"identifier\": \"SERVICE-ID\",\n\t\"callback_url\": \"http://127.0.0.1:8000/user/create/\"\n}" 242 | }, 243 | "url": { 244 | "raw": "{{url}}/service/", 245 | "host": [ 246 | "{{url}}" 247 | ], 248 | "path": [ 249 | "service", 250 | "" 251 | ] 252 | } 253 | }, 254 | "response": [] 255 | }, 256 | { 257 | "name": "Get User Detail", 258 | "request": { 259 | "auth": { 260 | "type": "bearer", 261 | "bearer": [ 262 | { 263 | "key": "token", 264 | "value": "", 265 | "type": "string" 266 | } 267 | ] 268 | }, 269 | "method": "GET", 270 | "header": [ 271 | { 272 | "key": "Content-Type", 273 | "name": "Content-Type", 274 | "value": "application/json", 275 | "type": "text" 276 | } 277 | ], 278 | "url": { 279 | "raw": "{{url}}/user/", 280 | "host": [ 281 | "{{url}}" 282 | ], 283 | "path": [ 284 | "user", 285 | "" 286 | ] 287 | } 288 | }, 289 | "response": [] 290 | }, 291 | { 292 | "name": "Create Subscription", 293 | "request": { 294 | "auth": { 295 | "type": "bearer", 296 | "bearer": [ 297 | { 298 | "key": "token", 299 | "value": "", 300 | "type": "string" 301 | } 302 | ] 303 | }, 304 | "method": "POST", 305 | "header": [ 306 | { 307 | "key": "Content-Type", 308 | "name": "Content-Type", 309 | "value": "application/json", 310 | "type": "text" 311 | } 312 | ], 313 | "body": { 314 | "mode": "raw", 315 | "raw": "{\n\t\"service\": \"service-id\"\n}" 316 | }, 317 | "url": { 318 | "raw": "{{url}}/subscription/new/", 319 | "host": [ 320 | "{{url}}" 321 | ], 322 | "path": [ 323 | "subscription", 324 | "new", 325 | "" 326 | ] 327 | } 328 | }, 329 | "response": [] 330 | } 331 | ], 332 | "event": [ 333 | { 334 | "listen": "prerequest", 335 | "script": { 336 | "id": "f2c9c8a9-e805-4994-ad37-105649f5c1ee", 337 | "type": "text/javascript", 338 | "exec": [ 339 | "" 340 | ] 341 | } 342 | }, 343 | { 344 | "listen": "test", 345 | "script": { 346 | "id": "c2bb057c-4df6-47ac-9537-9f844e5ffe07", 347 | "type": "text/javascript", 348 | "exec": [ 349 | "" 350 | ] 351 | } 352 | } 353 | ], 354 | "variable": [ 355 | { 356 | "id": "5c7e79f6-fb1a-4642-a4d8-4f4961a68af9", 357 | "key": "url", 358 | "value": "http://127.0.0.1:8001", 359 | "type": "string" 360 | } 361 | ] 362 | } --------------------------------------------------------------------------------