Skip to content

A simple REST API made using DJango Rest Framework to manage user session using OAuth 2.0.

License

Notifications You must be signed in to change notification settings

AdrianoDiDio/PyAuthBackend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Table of contents

PyAuthBackend

A simple implementation of the OAuth2.0 protocol to handle user sessions.

A live demo is available at adrianodd.pythonanywhere.com

Requirements

This guide assumes that a Linux distribution is used, commands for Windows are similar but requires some steps to get the required files. In order to setup this package we need to install python3,pip,mysql using a simple bash command (Note that sudo may be required in order to install it):

$ apt-install python3,pip,mysql-server

After installing the required packages the installation can proceed.

Installation

After unzipping the package, we need to create a new virtual enviroment. Virtual Enviroment let us create a separate enviroment from the system to install local dependencies required by this package. In order to create a new enviroment, open up the shell in the folder where the project was unzipped and run:

$ python3 -m venv env

This will create an hidden folder called .env that will contains all the required files to run the project. Next, we need to activate it by running:

$ source .env/bin/activate

Now, we need to download all the required files by running:

$ pip install -r requirements.txt

After pip is done installing, we need to setup our database using django utilities. Before running the commands, modify the section DATABASES inside the file PyAuthBackend/settings.py by inserting the MySQL Username,Password,DBName and Host. If any of the parameters is wrong or missing DJango will display an error asking to fix it. Next, we need to prepare our query needed to create the Database structure by running the command:

$ python manage.py makemigrations

and then:

$ python manage.py migrate

If all the commands completed without error, we can now start the local development server by running:

$ python manage.py runserver <OptionalIPAddress:Port>

If IP address is not specified, server will be available at localhost:8000, API can be reached at localhost:8000/api. By opening localhost:8000 in a browser user should see the API documentation made using swagger.

Finally, after checking that everything is working, we can create a SuperUser that will manage the User's registration by running the command:

$ python manage.py createsuperuser

This user can now login at localhost:8000/admin (or a custom IP address) to manage all the registered users. When deploying to a real server we also need to set-up a cron job that blacklists all the expired tokens by running the following command in a shell:

$ source /path/to/project/.env/bin/activate
    && python /path/to/project/PyAuthBackend/manage.py
    flushexpiredtokens

API can be tested using built-in ui available at localhost:8000, however due to the model that the API uses (OAuth2.0) not being supported by OpenAPI 2.0, it is recommended to use an external tool like Postman that makes easy adding the bearer token to each protected request.

Architecture

Database

In order to store user information PyAuthBackend uses the default Django user model that contains several fields like username,password,email etc... This model has been extended by declaring a new field called biometricToken, used to login without specifying a password.

CREATE TABLE `AuthRESTAPI_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `password` varchar(128) NOT NULL,
  `last_login` datetime(6) DEFAULT NULL,
  `is_superuser` tinyint(1) NOT NULL,
  `username` varchar(150) NOT NULL,
  `first_name` varchar(150) NOT NULL,
  `last_name` varchar(150) NOT NULL,
  `email` varchar(254) NOT NULL,
  `is_staff` tinyint(1) NOT NULL,
  `is_active` tinyint(1) NOT NULL,
  `date_joined` datetime(6) NOT NULL,
  `biometricToken` varchar(256) DEFAULT NULL,
  `biometricChallenge` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 |

SimpleJWT Tokens

This library simplify the implementation of the OAuth 2.0 protocol by generating and managing the JWT tokens. There are only two tokens generated by the backend: Refresh and Access token.

Access token is a short-lived JWT token that is used as a bearer token inside each request's authorization header while the Refresh token, which has a longer lifespan, is used to let the user refresh the expired access token.

Tokens lifespan can be set in the settings.py file, and the main paramters that have been changed were:

ACCESS_TOKEN_LIFETIME : timedelta(minutes=5)
REFRESH_TOKEN_LIFETIME : timedelta(days=1)

This makes sure that the access token gets renewed every 5 minutes and can be refreshed using the endpoint Refresh. When the refresh token expires, users are forced to login again in order to generate a new pair. If the user logout then the refresh token is blacklisted and the access token won't be renewed. Inside each JWT token there is only one information about the user which is the Id field needed to personalize all the protected endpoints to show information only for the current user. SimpleJWT contains an app that can be installed that adds a command to the django manage interface that flushes all the expired tokens as seen in the installation process when setting up a new cron job.

Authorization Header

Every request made to a protected point, which requires user to be authenticated, has to set an authorization header containing the access token.

Authorization: "Bearear eyJ0eXAiOiJKV1QiLCJhbG..."

Endpoints

All the endpoints are available at: serverIporAddress:Port/api/EndpointName.

Login

This endpoint,available at /api/login, let the user authenticate in order to obtain the access and refresh token. It is implemented using SimpleJWT library that exposes a POST only endpoint taking the username and password and returning a TokenPair containing the Access and Refresh Token. It uses DJango builtin authentication schema to verify that the user exists and the password is valid. It requires two parameters that are passed as JSON object inside the POST body request:

{
	"username": "foo",
	"password": "foobarpassword"
}

If the Username/Password are valid it returns the token pair as seen below:

{
	"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
	"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}

Otherwise a response containing the error is returned:

{
	"detail": "Invalid Username/Password"
}

Biometric Login

This endpoint, available at api/biometricLogin, let the user authenticate by using a biometricToken and the userId. It is implemented by adding a new authentication method to DJango and implementing our own TokenObtainPair from the SimpleJWT library. In particular, every time a request is made to this endpoint, a query to the database is done to check if the userId and biometricToken matches the one stored inside the database. It requires two parameters that are passed as JSON object inside the POST body request, and the biometricToken must be encoded in Base64 as seen below:

{
	"userId": 1,
	"biometricToken": "YmlvbWV0cmljVG9rZW4"
}

If the userId/biometricToken are valid it returns the token pair as seen below:

{
	"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
	"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}

Otherwise a response containing the error is returned:

{
	"detail": "Invalid Biometric Token"
}

Refresh

This endpoint, available at api/login/refresh, let the user refresh his access token. It takes one parameter, the refresh token, and doesn't require the user to be authenticated since the access token is no longer valid. The refresh token is passed in the body of the POST request as a JSON string as seen below:

{
	"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
}

Returns a new access token if the Refresh token was valid:

{
	"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
}

Or 401 (unauthorized) if refresh token is not valid or blacklisted.

Registration

This endpoint is available at api/registration and requires three parameters that are passed in the body of the POST request as a JSON String as seen below:

{
	"username": "foo",
	"email":  "foo@bar.com",
	"password": "foobarpassword"
}

The server will then verify if all the fields are present and valid and will return a JSON response containing the details of the new user if valid:

{
  "id": 1,
	"username": "foo",
	"email":  "foo@bar.com",
}

Or a response containing the error messages:

{
	"email":  [
		"Enter a valid email address."
	],
	"password": [
		"Ensure this field has at least 8 characters"
	]
}

User Info

This endpoint, available at api/userDetails, let the user view his own data by accepting a GET request. Since server needs to be sure that only authenticated user can see their own data, this endpoint requires an authorization header. If the user is authorized then the server will return the following JSON object:

{
	"id": 1,
	"username":  "foobar",
	"email": "foo@bar.com"
}

If user is not authenticated or the access token is no longer valid then a 401 (unauthorized) response is returned.

Get Biometric Challenge

This endpoint, available at api/getBiometricChallenge, let the user start the enrollment process by generating an UUID4 and sending it back to the client as a JSON object. It requires the user to be authenticated,using authorization header and doesn't require requires any additional parameters. Response can be either the biometricChallenge encoded using base64:

{
  "biometricChallenge":"MDZiMmVmYWYtYTc1Ni00OTBkLTk1NmQtMTA1YjA3OWI2OWUx"
}

or 401 (unauthorized) if the access token was not valid or missing. If the biometricChallenge was generated successfully then it is stored in the database to later complete the enrollment.

Generate Biometric Token

This endpoint, available at /api/generateBiometricToken, let the user complete the enrollment process by generating a new biometric token. It requires the user to be authenticated, through the authorization header, and takes three parameters: Public Key (encoded using base64),Signed Challenge (encoded using base64) and a nonce (a random number added to the challenge). These parameters are passed in the POST request body as a JSON string:

{
  "signedChallenge" : "c2lnbmVkQ2hhbGxlbmdl",
  "nonce" : 10201,
  "publicKey" : "cHVibGljS2V5"
}

signedChallenge and publicKey must be encoded as Base64 before sending. The server uses the PyCrypto library to verify if the challenge was signed correctly,by using the public key and the original challenge stored in the database. In particular, it takes the original challenge, appends the sent nonce to it and uses PyCrypto to verify it.

publicKey = base64.urlsafe_b64decode(request.data['publicKey'])
publicKeyObject = RSA.importKey(publicKeyObject)
verifierRSA = pkcs1_15.new(publicKeyObject)
digest = SHA256.new()
originalChallenge = base64.urlsafe_b64decode(userInstance.biometricChallenge)
digest.update(originalChallenge.encode())
verifierRSA.verify(digest,sentChallenge)

If the signature is valid it generates a random string of 32 bytes using python's library secrets that it is then encrypted with PyCrypto using the public key.

authBiometricToken = secrets.token_bytes(32)
cipherRSA = PKCS1_OAEP.new(publicKeyObject,
            hashAlgo=SHA256,
            mgfunc=lambda x,y: pss.MGF1(x,y,SHA1))
cipherRSA.encrypt(authBiometricToken)

It is finally encoded using base64,stored inside the database and returned to the user as a JSON object as seen below:

{
  "biometricToken" : "ZW5jcnlwdGVkQmlvbWV0cmljVG9rZW4="
}

Only if the signature was not valid it returns 400 (Bad request).

Logout

This endpoint, available at api/logout, handles user's logout. Since this REST API uses Oauth2.0 and it is stateless, the logout flow simply blacklist the refresh token forcing the user to login again after the access token is expired. It takes only one parameter, the refresh token, but in order to call it, the user must be logged in and thus it must set the authorization header of the request. Sample request can be seen below:

{
	"refresh": 	"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
}

If the request was successful then the server will return 205 ( Reset Content ) if the refresh token was valid, 401 if user was not authorized or 400 (Bad request) if refresh token was invalid or blacklisted.

About

A simple REST API made using DJango Rest Framework to manage user session using OAuth 2.0.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published