User Management - adaptive-learning/flocs GitHub Wiki

User Management

There are, in some sense, four different types of users in the system.

Model name Project What is does
User Django Default basic user representation.
AnonymousUser Django Represents a user that is not authenticated in any way. Cannot be used to track single real user.
LazyUser django-lazysignup Similar to AnonymousUser, but with the possibility to keep track of the user.
UserSocialAuth Python Social Auth Represents user authenticated through 3rd parties like Google and Facebook.

In the frontend, there is only one representation of user. It is based on User model and it is extended with two extra fields to store information about lazy user and social user.

Where to get a user instance

User and AnonymousUser are passed as user attribute of HTTP request received in Django views. Since AnonymousUser implements User interface, it is safe to use all User methods and access User attributes. Both LazyUser and UserSocialAuth require an instance of User that is associated to them. Therefore, you can access LazyUser or UserSocialAuth by supplying associated User instance from request.

Enforcing user authentication and authorization

Do not forget to enforce authentication and authorization on both frontend and backend.

Backend

To enforce users to be authenticated and have proper authorization, you can use custom decorators @login_required and @staff_member_required in your views. If you want to allow lazy users see lazy signup section.

@login_required decorator requires a user to be properly authenticated in the system. Right now, that can be either through username and password or through 3rd party providers, Google and Facebook. Not even lazy user are not sufficient. The following snippet shows usage of this decorator.

from user.decorators import login_required

@login_required
def get_student_statistics(request):
    # code in this view can safely assume there is a user logged in
    your code here...

@staff_member_required decorator requires a user to be set as active and also requires a user to be member of staff (having is_staff attribute set to True). However, it does not explicitly disallow unauthenticated users form accessing the given view. If there happens to be a lazy user, who is also a member of staff, then he satisfies all requirements placed by this decorator. The snippet below shows example usage of this decorator.

from user.decorators import staff_member_required

@staff_member_required
def get_admin_stats(request):
    # code in this view can safely assume there is a staff member accessing this view
    your code here...

Frontend

There is custom accessService with checks for user authentication and permissions. These methods return promise that is resolved in case the condition is satisfied or rejected otherwise. To use accessService first define a state for the URL you wish to be protected. Then add resolve property (see $stateProvider) with some of the accessService methods. The available methods are following.

  • isUserAvailable requires user to be logged in or to be a lazy user
  • isAuthenticated requires user to be logged in, lazy user is not enough. Unfortunately, user object's field authenticated in both backend and frontend is set to True even for lazy user. This makes things a bit messy.
  • isStaff requires user to be logged in and to have staff privileges.

The enforcing itself is done by listening to $stateChangeError events. In case of authentication, current behaviour is to display login modal. If the modal is dismissed (no login made) 401 error page is displayed. For authorisation violation the 403 error page is displayed right away.

The following example will require a user to be fully authenticated (lazy user is not enough) before accessing the URL /profile.

.state('profile', {
      url: '/profile',
      templateUrl: 'profile/profile.tpl.html',
      controller: 'profileCtrl',
      data: {
          titleTraslationKey: 'PROFILE'
      },
      resolve: {
        access: function(accessService) {
          return accessService.isAuthenticated();
        },
      }
    })

Lazy Signup

Lazy signup allows to abstract from authenticated and hopefully-soon-signed-up users. As an example where is this behaviour useful, consider the practice session: we want a user to start the practice without forcing them to sign up before, but we also want to record their progress and save it if they sign up after/during the practice session.

We are using django-lazysignup package. When you want to abstract some controller (Django view) from lazy users, use @allow_lazy_user annotation. If the current user hasn't been authenticated yet, this will create a (temporary) account for them and so allows you to perform any operations with the current user as if they are normal authenticated users.

Example:

## practice/views.py ##
from lazysignup.decorators import allow_lazy_user

@allow_lazy_user
def get_next_task(request):
    task = practice_service.get_next_task(student=request.user)
    return JsonResponse(task)

If you need to know, if the current user is lazy or not, you can use lazysignup.utils.is_lazy_user function (ant there is also @require_nonlazy_user annotation).

Social user

Social users are those authenticated through 3rd party provides of OAuth, currently Google and Facebook. Social users are somewhat similar to lazy users in implementation. They are stored independently and they are associated to exactly one Django User instance. Usually, you will only have to deal with associated Django user and not social users them selves.

To have social login available, it is necessary to set up application credentials for both Google and Facebook. These credentials are application key and application secret. Both can be found in administration pages, https://console.developers.google.com for Google and https://developers.facebook.com/apps for Facebook. These credentials then has to be inserted in private settings file (see Private Settings).

Google project settings

The project has to have Google+ API enabled and has to have authorised redirect URIs set. The redirect URI is of form protocol://hostname/social/complete/google-oauth2/. Note that all the hosts that the application is going to be deployed at has to be entered.

Facebook app settings

The project has to be set as public, is has to have Facebook login 'product' added and all the valid OAuth redirect URIs has to be set in Facebook login settings. The redirect URI is of form protocol://hostname/social/complete/facebook/. Note that all the hosts that the application is going to be deployed at has to be entered.

Frontend user object

Current user is stored in a user variable of userService. It is a JSON object with the following structure.

var user = {
        username: "",
        firstName: "",
        lastName: "",
        authenticated: false,
        isLazyUser: false,
        email: "",
        isStaff: false,
        providers: [],
      };

username field contains either user selected name during sign up, concatenation of first and last name (Facebook login) or part of the email address before @ symbol (Google login). firstName and lastName are either blank in case of standard sign up or filled with details provided by 3rd party (Google and Facebook). Field authenticated contains information whether the user is somehow authenticated. That means being logged in or being lazy user. Basically, whether or not there is a Django User entry for this user. isLazyUser field tells whether the user is lazy user or not. Field email is self explanatory. isStaff field declares whether or not the user has staff privileges, i.e. can access admin statistics. providers field contains list of 3rd party authentication providers for this user. It can be empty list in case the user has signed up in our system. In some cases, if the user has same email for both Facebook and Google account, it may contain multiple providers.