DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for From Scratch: User Authentication
Abdessamad MOUHASSINE
Abdessamad MOUHASSINE

Posted on • Updated on

From Scratch: User Authentication

Definitions

User Authentication is loosely defined as identifying the user based on his credentials. In other words, it provides access control for systems, by checking to see if the user's credentials match those saved in a database, in a data authentication server, or anywhere else.

The credentials, in the other hand, could be anything: an identifier, password, pin numbers, certificate, fingerprint, retina, voice. I mean anything that differentiate a user from another.

You can get more information about the authentication, its types and factors on this wonderful article.

Solutions

Developers and frameworks provide different solutions and visions, simple and complex, to manage user identification.

Let see some examples:

Zend Framework 2

Zend framework provides a dedicated module zend-authentication, which includes adapters for different authentication methods, and an AuthenticationService class.

use Zend\Authentication\AuthenticationService;

// Instantiate the authentication service
$auth = new AuthenticationService();

// Instantiate a dummy authentication adapter
$authAdapter = new Adapter($email, $password);

// Attempt authentication, saving the result
$result = $auth->authenticate($authAdapter);

After a successful authentication attempt, subsequent requests can query the authentication service to determine if an identity is present, and, if so, retrieve it:

if ($auth->hasIdentity()) {
  // Identity exists; get it
  $identity = $auth->getIdentity();
}

Laravel 5

Laravel is a huge framework in fact, offering also different solutions and tools to handle authentication.

Laravel's authentication services could be accessed via the Auth facade.

use Illuminate\Support\Facades\Auth;

if (Auth::attempt($credentials)) {
  // Authentication passed
}
else {
  // Authentication failed
}

The attempt method accepts an array of key/value pairs as its first argument, and will return a boolean if the user is found in the database using the values in the array.

The authenticated user can be retrieved again via the Auth facade.

use Illuminate\Support\Facades\Auth;

// Get the currently authenticated user...
$user = Auth::user();

// Get the currently authenticated user's ID...
$id = Auth::id();

Symfony 4

The Symfony's solution is quite complicated. The security module provides multiple classes and interfaces to handle authentication.

use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

// instances of Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface
$providers = array(...)

// Instantiate the authentication manager
$authenticationManager = new AuthenticationProviderManager($providers);

// create a token, containing the user's credentials
$unauthenticatedToken = new UsernamePasswordToken($username, $password, $providerKey);

// validate the given token, and return an authenticated token
try {
  $authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken);
} catch (AuthenticationException $exception) {
  // authentication failed
}

After authentication, the User object of the current user can be accessed via the getUser() from inside a controller, this will look like:

public function index()
{
  $user = $this->getUser();
}

or via the Security class, available from version 3.4:

use Symfony\Component\Security\Core\Security;

public function indexAction(Security $security)
{
  $user = $security->getUser();
}

Django 2

Django uses, by default, sessions and middlewares to hook the authentication system into request objects.

from django.contrib.auth import authenticate

def my_view(request):
  user = authenticate(username='john', password='secret')

  if user is not None:
    # A backend authenticated the credentials
  else:
    # No backend authenticated the credentials

The user, authenticated or not, is always accessible within the request object:

if request.user.is_authenticated:
  # Do something for authenticated users.
  ...
else:
  # Do something for anonymous users.
  ...

Passport.js

Authenticating requests is as simple as calling passport.authenticate() and specifying which strategy to employ. authenticate()'s function signature is a standard Connect middleware, which makes it convenient to use as route middleware in Express applications.

app.post('/login', passport.authenticate('strategy-name'), function (req, res) {
  // If this function gets called, authentication was successful.
  // `req.user` contains the authenticated user.
  res.redirect('/users/' + req.user.username);
});

Unfortunately, passport can only be used in an express-like applications, since authenticate() returns a middleware function. It doesn't provide any API to use independently.

Conclusion

In my opinion, the Django solution is the best one, because, unlike the others, it separates the authentication from login.

  • the authenticate function checks the user credentials against each authentication backend, and returns a User object if the credentials are valid for a backend.
  • but the login function saves the user’s ID in the session, making it persistent for several requests.

Signing in users is application specific. But user identification is shared between apps and systems, only the backend differs.

From the above, and other examples not cited here, I ended up with a simple, stupid authentication manager, that dispatches the user credentials to a list of handlers.

interface AuthManager {
  /**
   * Check the credentials and return the user
   */
  attempt (credentials: object): Promise<User | undefined>

  /**
   * Register an authenticator
   */
  use (name: string, handler: Authenticator): this
}

The manager is an implementation of chain of responsibility design pattern, and internally uses authenticators with the following contract:

interface Authenticator {
  /**
   * Try to handle the credentials, or delegate to the next handler.
   */
  process (credentials: object, next: () => any): Promise<User | undefined>
}

I'm still working on this implementation to make it clean and framework-agnostic.
You may take a look at the develop branch.

What do you think guys ?

Top comments (0)

🌱 DEV runs on 100% open source code known as Forem.

Β 
Contribute to the codebase or learn how to host your own.