DEV Community

Katerina Braide
Katerina Braide

Posted on • Edited on

Secure Web Authentication with a Serverless API: A Quick Guide

Introduction

In the realm of web development, robust security measures are crucial, particularly when it comes to user authentication. This article will guide you through the process of implementing a secure web authentication system using a Serverless API.

By the end, you'll have the knowledge and tools to create a scalable and resilient authentication system for your web applications.

What Is Web Authentication?

Web authentication verifies the identity of users accessing a web application, ensuring that they are who they claim to be. Achieved through credentials like usernames and passwords, it often includes multi-factor authentication (MFA) for added security. The goal is to protect sensitive data and restrict access to authorized individuals.

Benefits of Web Authentication

  • Security: Web authentication provides a crucial layer of security by preventing unauthorized access to sensitive user data and application features.
  • Data Protection: By verifying user identities, web authentication safeguards personal and confidential information, protecting users from potential data breaches.
  • User Accountability: Authentication enables the tracking and accountability of user actions, helping administrators monitor and manage user activity within the application.
  • Trust and User Confidence: Robust authentication mechanisms build trust among users, enhancing their confidence in the application's ability to protect their data and privacy.
  • Compliance: Many regulatory frameworks and industry standards require robust authentication practices to ensure compliance with data protection and privacy regulations.
  • Customization of User Experience: Authentication allows for personalized user experiences, tailoring access and permissions based on user roles and responsibilities.

What Is an API?

An API, or Application Programming Interface, is a set of rules and tools that allows different software applications to communicate with each other. It defines the methods and data formats that applications can use to request and exchange information. APIs facilitate the integration of different systems, enabling them to work together seamlessly.

What Is a Serverless API?

A serverless API, also known as a serverless function or serverless endpoint, is a type of backend service that runs in a serverless computing environment. Unlike traditional server-based architectures, serverless APIs do not require the provisioning or management of servers. Instead, they automatically scale in response to demand and execute functions in ephemeral, stateless containers. Serverless APIs are event-driven, responding to triggers such as HTTP requests, database changes, or file uploads. Commonly hosted on cloud platforms, serverless APIs simplify infrastructure management, reduce costs, and allow developers to focus on code functionality rather than server maintenance.

Why Use a Serverless API?

  • Scalability: Serverless APIs automatically scale to handle varying workloads, ensuring optimal performance without manual intervention.
  • Cost Efficiency: With serverless, you pay only for the compute resources used during function execution, leading to potential cost savings compared to maintaining dedicated servers.
  • Simplified Deployment: Serverless APIs simplify deployment with rapid development cycles, automatic scaling, and effortless management of infrastructure.
  • Focus on Code: Developers can concentrate on writing code and building features without the burden of server provisioning, maintenance, or scaling concerns.
  • Event-Driven Architecture: Serverless APIs thrive on event-driven architectures, responding to specific events or triggers, making them well-suited for various use cases, including web authentication.
  • Reduced Operational Overhead: Serverless APIs abstract away infrastructure management tasks, reducing the operational overhead traditionally associated with maintaining servers.
  • Flexibility and Agility: Serverless architecture allows for quick development and deployment, fostering an agile development process and enabling faster time-to-market for applications.

A Step-by-Step Guide On How To Use One

Step 1: Define Authentication Requirements

Before diving into implementation, it's essential to clearly define your authentication requirements. Consider the following aspects:

  • User Roles and Access Levels: Identify different user roles and define their respective access levels. Consider the actions each role should be able to perform and the data they can access.
  • Authentication Methods: Choose the appropriate authentication methods for your application, such as Username/Password, OAuth, or JSON Web Tokens (JWT).
  • Security Requirements: Specify security measures, including secure password storage, token-based authentication, and the use of HTTPS to protect data in transit.

Step 2: Choose a Serverless Provider

Select a serverless provider that aligns with your application's needs. Let's use AWS Lambda for this example:

Install AWS SDK
npm install aws-sdk 
Enter fullscreen mode Exit fullscreen mode

Configure your AWS credentials:

// serverlessFunctions.js

const AWS = require('aws-sdk');
AWS.config.update({ region: 'your-region', accessKeyId: 'your-access-key', secretAccessKey: 'your-secret-key' });
Enter fullscreen mode Exit fullscreen mode

Step 3: Set Up Serverless Functions

Create serverless functions to handle user registration, login, and token generation. Use Node.js and the AWS SDK:


// serverlessFunctions.js

const AWS = require('aws-sdk');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

const dynamoDB = new AWS.DynamoDB.DocumentClient();
const tableName = 'UsersTable';

// Function to register a new user
module.exports.registerUser = async (event) => {
  const { username, password } = JSON.parse(event.body);
  const hashedPassword = await bcrypt.hash(password, 10);

  const params = {
    TableName: tableName,
    Item: {
      username,
      password: hashedPassword,
    },
  };

  await dynamoDB.put(params).promise();

  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'User registered successfully' }),
  };
};

// Function to authenticate and generate a JWT token
module.exports.login = async (event) => {
  const { username, password } = JSON.parse(event.body);
  const params = {
    TableName: tableName,
    Key: { username },
  };

  const userData = await dynamoDB.get(params).promise();

  if (!userData.Item || !(await bcrypt.compare(password, userData.Item.password))) {
    return {
      statusCode: 401,
      body: JSON.stringify({ message: 'Invalid username or password' }),
    };
  }

  const token = jwt.sign({ username }, 'your-secret-key', { expiresIn: '1h' });

  return {
    statusCode: 200,
    body: JSON.stringify({ token }),
  };
};
Enter fullscreen mode Exit fullscreen mode

Step 4: Use a Database for User Management

Configure DynamoDB:


// serverlessFunctions.js

const AWS = require('aws-sdk');
AWS.config.update({ region: 'your-region', accessKeyId: 'your-access-key', secretAccessKey: 'your-secret-key' });

const dynamoDB = new AWS.DynamoDB.DocumentClient();
const tableName = 'UsersTable';
Enter fullscreen mode Exit fullscreen mode

Step 5: Implement Secure Password Storage

Ensure secure password storage using bcrypt:

Install bcrypt library
npm install bcrypt
Enter fullscreen mode Exit fullscreen mode

Integrate bcrypt into the registration function:


// serverlessFunctions.js

const bcrypt = require('bcrypt');

// ... 

// Function to register a new user
module.exports.registerUser = async (event) => {
  const { username, password } = JSON.parse(event.body);
  const hashedPassword = await bcrypt.hash(password, 10);

  // ... 
};
Enter fullscreen mode Exit fullscreen mode

Step 6: Token-based Authentication with JWT

Implement JWT for token-based authentication:

Install jsonwebtoken library
npm install jsonwebtoken
Enter fullscreen mode Exit fullscreen mode

Integrate JWT into the login function:


// serverlessFunctions.js

const jwt = require('jsonwebtoken');

// ... 

// Function to authenticate and generate a JWT token
module.exports.login = async (event) => {
  const { username, password } = JSON.parse(event.body);

  // ...

  const token = jwt.sign({ username }, 'your-secret-key', { expiresIn: '1h' });

  // ...
};
Enter fullscreen mode Exit fullscreen mode

Step 7: Set Up API Gateway

Configure an API Gateway for communication:

  1. Create an API in the AWS API Gateway.
  2. Set up resource and method for each function (GET for login, POST for registration).
  3. Secure the API using AWS Identity and Access Management (IAM) roles.

Step 8: Implement Secure HTTPS

Ensure secure communication by configuring the API Gateway to use HTTPS:

  1. In the API Gateway console, select your API.
  2. Under "Stages," select the desired stage (e.g., "prod").
  3. In the "Settings" tab, enable "HTTPS Only.”

Step 9: Implement Multi-Factor Authentication (Optional)

Explore the implementation of MFA using services like Amazon Cognito or Auth0.

Multi-Factor Authentication (MFA) adds an extra layer of security by requiring users to verify their identity through multiple methods. This step is optional but highly recommended for applications that prioritize enhanced security. We'll explore the implementation of MFA using Amazon Cognito, a service that simplifies identity management.

1. Setting Up Amazon Cognito:

  • Go to the Amazon Cognito Console.
  • Create a new user pool, specifying settings like MFA and account recovery.
  • Note the User Pool ID and create an app client.
  • Integrate Cognito in Serverless Functions

Update your server-less functions to integrate with Amazon Cognito:


// serverlessFunctions.js

const AWS = require('aws-sdk');
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');

const poolData = {
  UserPoolId: 'your-user-pool-id',
  ClientId: 'your-app-client-id',
};

const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);

// Function to register a new user with MFA
module.exports.registerUserWithMFA = async (event) => {
  // ... 

  const attributeList = [
    new AmazonCognitoIdentity.CognitoUserAttribute({
      Name: 'email',
      Value: 'user@example.com',
    }),
  ];

  userPool.signUp(username, password, attributeList, null, (err, result) => {
    if (err) {
      return {
        statusCode: 500,
        body: JSON.stringify({ message: 'Error registering user', error: err.message }),
      };
    }

    const cognitoUser = result.user;
    console.log('User registered with ID ' + cognitoUser.getUsername());

    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'User registered successfully with MFA' }),
    };
  });
};

// Function to authenticate and generate a JWT token with MFA
module.exports.loginWithMFA = async (event) => {
  // ... 

  const authenticationData = {
    Username: username,
    Password: password,
  };

  const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);

  const userData = {
    Username: username,
    Pool: userPool,
  };

  const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);

  cognitoUser.authenticateUser(authenticationDetails, {
    onSuccess: (session) => {
      const token = session.getIdToken().getJwtToken();
      console.log('Authentication successful, JWT token:', token);

      return {
        statusCode: 200,
        body: JSON.stringify({ token }),
      };
    },
    onFailure: (err) => {
      console.error('Authentication failed:', err);
      return {
        statusCode: 401,
        body: JSON.stringify({ message: 'Invalid username or password' }),
      };
    },
    mfaRequired: (codeDeliveryDetails) => {
      console.log('MFA required, confirmation code sent to', codeDeliveryDetails.Destination);

      // Prompt user for MFA code and call cognitoUser.sendMFACode(code) to confirm MFA
      return {
        statusCode: 200,
        body: JSON.stringify({ message: 'MFA required' }),
      };
    },
  });
};
Enter fullscreen mode Exit fullscreen mode

Step 10: Test and Monitor

Thoroughly test your authentication system using tools like Postman. Implement logging and monitoring for suspicious activities.

Additional Resources

  • Get AWS API Gateway tutorial here
  • Setting Up Amazon Cognito here
  • Token-based Authentication with JWT Look here

Conclusion

In summary, adhering to best practices and staying vigilant about security updates is crucial for maintaining the continuous protection of your web application. It's important to recognize that security is a dynamic field, and regular updates play a pivotal role in sustaining a robust authentication system.

Top comments (1)

Collapse
 
azubuikeduru profile image
Azubuike Duru

This is really nice 👍