Socraticqs2 Social Auth registration

Introduction

Socraticqs2 users can authorize using SSO.

We a using Python Social Auth library for that process.

Source code available at github.

Available backend(s) now are:

Google OAuth2
Facebook OAuth2
Twitter OAuth
LinkedIn OAuth2
KhanAcademy OAuth1

Configuration Python Social Auth

To configure Python Social Auth (PSA) we need to set appropriate KEY/SECRET for available backends on local_conf.py file:

SOCIAL_AUTH_TWITTER_KEY = 'key'
SOCIAL_AUTH_TWITTER_SECRET = 'secret'

SOCIAL_AUTH_FACEBOOK_KEY = 'key'
SOCIAL_AUTH_FACEBOOK_SECRET = 'secret'

SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY = 'key'
SOCIAL_AUTH_LINKEDIN_OAUTH2_SECRET = 'secret'

SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'key'
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'secret'

SOCIAL_AUTH_KHANACADEMY_OAUTH1_KEY = 'key'
SOCIAL_AUTH_KHANACADEMY_OAUTH1_SECRET = 'secret'

Manuals for backend(s) configuration:

Also we can re-define auth backends on local_conf.py:

AUTHENTICATION_BACKENDS = (
   # 'social.backends.twitter.TwitterOAuth',
   # 'social.backends.facebook.FacebookOAuth2',
   # 'social.backends.google.GoogleOAuth2',
   # 'social.backends.linkedin.LinkedinOAuth2',
   # 'social.backends.khanacademy.KhanAcademyOAuth1',
   # 'social.backends.email.EmailAuth',
   'django.contrib.auth.backends.ModelBackend',
)

There are main settings for PSA in settings/default.py:

MIDDLEWARE_CLASSES = (
  .............
  'ct.middleware.MySocialAuthExceptionMiddleware',
)

INSTALLED_APPS = (
  ........
  # Socials
  'social.apps.django_app.default',
  'psa',
)

TEMPLATE_CONTEXT_PROCESSORS = (
 ................
 'social.apps.django_app.context_processors.backends',
 'social.apps.django_app.context_processors.login_redirect',
)

AUTHENTICATION_BACKENDS = (
 'social.backends.twitter.TwitterOAuth',
 'social.backends.facebook.FacebookOAuth2',
 'social.backends.google.GoogleOAuth2',
 'social.backends.linkedin.LinkedinOAuth2',
 'social.backends.khanacademy.KhanAcademyOAuth1',
 'social.backends.email.EmailAuth',
 'django.contrib.auth.backends.ModelBackend',
)


SOCIAL_AUTH_PIPELINE = (
  'social.pipeline.social_auth.social_details',
  'social.pipeline.social_auth.social_uid',
  'social.pipeline.social_auth.auth_allowed',
  'psa.pipeline.social_user',
  'social.pipeline.user.get_username',
  'psa.pipeline.custom_mail_validation',
  'psa.pipeline.associate_by_email',
  'social.pipeline.user.create_user',
  'psa.pipeline.validated_user_details',
  'psa.pipeline.associate_user',
  'social.pipeline.social_auth.load_extra_data',
  'social.pipeline.user.user_details',
)

SOCIAL_AUTH_DISCONNECT_PIPELINE = (
  'social.pipeline.disconnect.allowed_to_disconnect',
  'social.pipeline.disconnect.get_entries',
  'social.pipeline.disconnect.revoke_tokens',
  'social.pipeline.disconnect.disconnect'
)

PROTECTED_USER_FIELDS = ['first_name', 'last_name', 'email']

FORCE_EMAIL_VALIDATION = True
PASSWORDLESS = True

SOCIAL_AUTH_EMAIL_VALIDATION_FUNCTION = 'psa.mail.send_validation'
SOCIAL_AUTH_EMAIL_VALIDATION_URL = '/email-sent/'

SOCIAL_AUTH_STRATEGY = 'psa.custom_django_strategy.CustomDjangoStrategy'
SOCIAL_AUTH_STORAGE = 'psa.custom_django_storage.CustomDjangoStorage'

SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
    'https://www.googleapis.com/auth/userinfo.email',
    'https://www.googleapis.com/auth/userinfo.profile'
]

# Facebook email scope declaring
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']

# Add email to requested authorizations.
SOCIAL_AUTH_LINKEDIN_OAUTH2_SCOPE = ['r_basicprofile', 'r_emailaddress']
# Add the fields so they will be requested from linkedin.
SOCIAL_AUTH_LINKEDIN_OAUTH2_FIELD_SELECTORS = ['email-address', 'headline', 'industry']
# Arrange to add the fields to UserSocialAuth.extra_data
SOCIAL_AUTH_LINKEDIN_OAUTH2_EXTRA_DATA = [('id', 'id'),
                                          ('firstName', 'first_name'),
                                          ('lastName', 'last_name'),
                                          ('emailAddress', 'email_address'),
                                          ('headline', 'headline'),
                                          ('industry', 'industry')]

About these and many other parameters you can read at the PSA docs.

User Auth flow procedure

We have next providers:

  • Django user, with primary email
  • for STRANGER, when he adds email, and we show pop-up with validation link, do search email associated with social accounts, and if there are similar one, and propose to login on the social auth
  • LTI user
  • Python social-auth accounts: email and social accounts

Principles: do not merge two social account from same providers

Assumptions: We trust LTI and email providers on email validation they send to us

Social auth

Users merge

We are VALIDATED user.

When we click to one of social/email buttons to associate with - system search such provider and if found start to analyze possible social/email provider conflicts after possible merge.

This mean that if after merge user would have more than one auth records with the same provider (google for example) we prevent this action with pop-up “warning about intersected providers”.

Temporary user validation

We are TEMPORARY user.

We make some progress and click validation link.

After that system will search email provider with email we validating or search django users by primary email.

If found - TEMPORARY user is logged out, start history merging process and new user is logged in.

If we can not find appropriate user via email - we start to modify user detail.

We change username to part before @ in email, remove ‘Temporary user’ full user name. Because of user is currently logged in and has all history we do not doing logout/login and history merge action.

User login process

When STRANGER click on social/email auth button on login page system starts login/register process.

It is possible to login at any time by validate social or email (via confirmation link) auth.

If user will set password after login to the system it will be possible to login using username/password method.

When we login using social/email auth system searching for such social auth records.

If found - login user associated with that social auth.

Social Auth pipelines

Module define custom pipeline to handle custom cases

custom_mail_validation - > implement code obj inspect

psa.pipeline.associate_by_email(backend, details, user=None, *args, **kwargs)

Associate current auth with a user with the same email address in the DB.

This pipeline entry is not 100% secure unless you know that the providers enabled enforce email verification on their side, otherwise a user can attempt to take over another user account by using the same (not validated) email address on some provider. This pipeline entry is disabled by default.

psa.pipeline.associate_user(backend, details, uid, user=None, social=None, *args, **kwargs)

Create UserSocialAuth.

psa.pipeline.custom_mail_validation(strategy, pipeline_index, *args, **kwargs)

Email validation pipeline

Verify email or send email with validation link.

psa.pipeline.not_allowed_to_merge(user, user_social)

Check if two users are allowed to merge

Check all social-auth from two users to predict providers intersection.

psa.pipeline.social_merge(tmp_user, user)

Merge UserSocialAuth

Re-assign UserSocialAuth objects to given user.

psa.pipeline.social_user(backend, uid, user=None, *args, **kwargs)

Search for UserSocialAuth.

psa.pipeline.union_merge(tmp_user, user)

Union merge

Merging Roles, UnitStatus, FSMState, Response, StudentError objects.

In Roles merge doing UNION merge to not repeat roles to the same course with the save role. Also we reassigning UnitStatuses, FSMStates, Responses and StudentErrors here.

psa.pipeline.validated_user_details(strategy, pipeline_index, *args, **kwargs)

Merge actions

Make different merge actions based on user type.

Social Auth Models

class psa.models.AnonymEmail(*args, **kwargs)

Temporary anonymous user emails

Model for temporary storing anonymous user emails to allow to restore anonymous sessions.

class psa.models.SecondaryEmail(*args, **kwargs)

Model for storing secondary emails

We can store emails here from social_auth or LTI login.

class psa.models.UserSession(*args, **kwargs)

User<->Session model

Model for linking user to session. Solution from http://gavinballard.com/associating-django-users-sessions/

psa.models.user_logged_in_handler(sender, request, user, **kwargs)

Create UserSession object to store User<=>Session relation.

Social Auth Views

psa.views.ask_stranger(request, *args, **kwargs)

View to handle stranger whend asking email.

psa.views.context(**extra)

Adding default context to rendered page.

psa.views.custom_login(request)

Custom login to integrate social auth and default login.

psa.views.done(request, *args, **kwargs)

Login complete view, displays user data.

psa.views.set_pass(request, *args, **kwargs)

View to handle password set / change action.

psa.views.validation_sent(request, *args, **kwargs)

View to handle validation_send action.

Custom Django Strategy and Storage

We used in Socraticqs2 Social Auth implementation custom Strategy and Storage to move around issue 557.

Module define improved entries

CustomCode -> add user_id attr to handle user generated validation link CustomDjangoStorage -> Storage to use this CustomCode

class psa.custom_django_storage.CustomCode(*args, **kwargs)

Custom Code object to track user_id through different sessions.

class psa.custom_django_storage.CustomDjangoStorage

Redefine code field to CustomCode model that add user_id to track.

code

alias of CustomCode

Custom Strategy to implement handling user_id attr in Code object

class psa.custom_django_strategy.CustomDjangoStrategy(storage, request=None, tpl=None)

Custom DjangoStrategy

Needed to add custom login and fix different session issue.