Tuesday, April 18, 2017

Adding User Activation Functionality by Using Amazon Simple Queue Service and Amazon Simple Email Service



In this series of posts, I am writing about various AWS services. In my previous posts, I have written about AWS EC2, ELB, Auto Scaling and DynamoDB.

As I said in my last post, this post will be about adding a user activation functionality to our digital card store application.

In that post, I have added User and Card DynamoDB tables to hold user and card information. When a new user is registered, the user was able to use the application immediately.

For this post, I will add an additional step to user registration process to make sure that no fake user is registered in our application. Before activation users will not be allowed to log in.

The User Activation Process

To make the mail sending process is independent from user registration request, I will use a message queue. Amazon Simple Queue Service is used to send and receive messages.

We can use Amazon Simple Email Service to send and receive email messages.

Activation process is shown below. When the user is registered, user information is persisted into DynamoDB and an activation message is put into the user activation queue. The application receives the message from the queue and sends the activation mail to the user.




When the user received the message, clicks the activation link in the message. Application process the activation request and marks the user as active in DynamoDB as shown below.

  



Steps

1. Prepare the queue in SQS
2. Verify the sender email address in SES
3. Change the application.
4. Start EC2 instances with SQS and SES permissions

Let's start.

1. Prepare the queue in SQS

Before sending messages to the queue, we should create the queue. Using the command below, we can create the queue with AWS CLI.

aws sqs create-queue --queue-name MailsToSendForUserActivation --attributes VisibilityTimeout=600
{
    "QueueUrl": "https://eu-central-1.queue.amazonaws.com/XXXX/MailsToSendForUserActivation"
}

Now we are ready to use the queue.
                 
2. Verify the sender email address in SES

Before using Amazon Simple Email Service, you should verify your sender email address. You can verify your email address using the steps below.
  • Sign in to the AWS Console. Under AWS Services choose SES
  • Select Email Addresses from the navigation pane and then click Verify a New Email Address.
  • Enter your email address and click Verify This Email Address.
Amazon Simple Email Service will send a verification email to the address. The verification link will be active 24 hours. When you click the verification link, the email address is verified.

New AWS users are allowed to use Simple Email Service in a limited environment for security reasons. This limited environment is called SES Sandbox. In SES Sandbox, you can only send email to the verified email addresses. To use Simple Email Service for production, you should open a ticket to AWS Support.

After the verification is done, you can send email to the verified email addresses.

3. Change the application

To show how to use SQS and SES, I will use the digital card store application that I have used in my previous post. The code can be found at my GitHub repository.

To use SQS and SES, we should change our applicaton as shown below.

a. Add dependencies

Amazon SQS Java Messaging Library is a Java JMS implementation for accessing Amazon SQS. We will use this library with Spring JMS. Also we will use aws-java-sdk-ses library to access Amazon SES.

             <dependency>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-jms</artifactId>
             </dependency>
             <dependency>
                    <groupId>com.amazonaws</groupId>
                    <artifactId>aws-java-sdk</artifactId>
                    <version>1.9.6</version>
             </dependency>
             <dependency>
                    <groupId>com.amazonaws</groupId>
                    <artifactId>amazon-sqs-java-messaging-lib</artifactId>
                    <version>1.0.0</version>
                    <type>jar</type>
             </dependency>
             <dependency>
                    <groupId>com.amazonaws</groupId>
                    <artifactId>aws-java-sdk-ses</artifactId>
                    <version>1.9.6</version>
             </dependency>

b. Configure SQS

We configure Spring JMS to use SQSConnectionFactory as shown below. You can change your region accordingly.

@Configuration
@EnableJms
public class SQSConfig {
       SQSConnectionFactory connectionFactory = SQSConnectionFactory.builder()
                    .withRegion(Region.getRegion(Regions.EU_CENTRAL_1))
                    .withAWSCredentialsProvider(new DefaultAWSCredentialsProviderChain()).build();

       @Bean
       public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
             DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
             factory.setConnectionFactory(this.connectionFactory);
             factory.setDestinationResolver(new DynamicDestinationResolver());
             factory.setConcurrency("3-10");
             factory.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);

             return factory;
       }

       @Bean
       public JmsTemplate defaultJmsTemplate() {
             return new JmsTemplate(this.connectionFactory);
       }
}

After configuration is done, we can create a simple service to send messages to the queue.

@Service
public class SQSService {
       @Autowired
       protected JmsTemplate defaultJmsTemplate;

       public void sendMessage(String queueName, String messageBody) {
             defaultJmsTemplate.convertAndSend(queueName, messageBody);
       }
}

b. Configure SES

We configure SES  as shown below. You can change your region accordingly. Please note that SES is not available in every region, so you might use a different region than you used with other services.

@Configuration
public class SESConfig {
       @Value("${mail.from.address}")
       String mailFromAddress;

       @Bean
       public AmazonSimpleEmailService amazonSimpleEmailService() {
             AmazonSimpleEmailService client = new AmazonSimpleEmailServiceClient();
             client.setRegion(Region.getRegion(Regions.EU_WEST_1));
             return client;
       }

       public String getFromAddress() {
             return mailFromAddress;
       }
}

After configuration we can create a simple service to send email.

@Service
public class SESService {
       @Autowired
       SESConfig sesConfig;

       public void sendMessage(String to, String subject, String body) {
             Destination destination = new Destination().withToAddresses(to);
             Content subj = new Content().withData(subject);
             Content bdy = new Content().withData(body);

             Message message = new Message().withSubject(subj).withBody(new Body().withHtml(bdy));
             SendEmailRequest request = new SendEmailRequest().withSource(sesConfig.getFromAddress())
                           .withDestination(destination).withMessage(message);
             sesConfig.amazonSimpleEmailService().sendEmail(request);
       }
}

c. Change User class

We add activationStatus and activationToken fields to User class as shown below.

@DynamoDBTable(tableName = "User")
public class User {
       public static final String ACTIVATION_STATUS_NONE = "NONE";
       public static final String ACTIVATION_STATUS_MAIL_SENT = "MAIL_SENT";
       public static final String ACTIVATION_STATUS_DONE = "DONE";

       @DynamoDBHashKey
       private String username;

       private String name;
       private String password;
       private String email;
       private double balance;
       private String activationStatus = ACTIVATION_STATUS_NONE;
       private String activationToken;

c. Change UserController class

We add userActivationQueueName, sqsService and sesService fields as shown below.

@Controller
public class UserController {

       public static final String USER_KEY_FOR_SESSION = "USER";

       @Autowired
       UserRepository userRepository;

       @Value("${user.activation.queue.name}")
       String userActivationQueueName;

       @Autowired
       SQSService sqsService;

       @Autowired
       SESService sesService;

We change registerUser to call prepareForActivation as shown below.

@RequestMapping(value = "/users", method = RequestMethod.POST)
@ResponseBody
public boolean registerUser(@RequestBody User user, HttpServletRequest request) {

       User previous = userRepository.findOne(user.getUsername());

       if (previous == null) {
             prepareForActivation(user, makeActivationUrlFromRequest(request, "/users"));

             user.setBalance(100);
             userRepository.save(user);
       }

       return previous == null;
}

We add prepareForActivation to generate a user activation token and send activation message to the queue as shown below.

private String makeActivationUrlFromRequest(HttpServletRequest request, String suffixToReplace) {
       return request.getRequestURL().toString().replace(suffixToReplace, "/activate");
}

private void prepareForActivation(User user, String url) {
       user.setActivationToken(String.valueOf(100000 * Math.random()));

       sqsService.sendMessage(userActivationQueueName,
                           "{\"username\": \"" + user.getUsername() + "\", \"activationUrl\":\"" + url + "\"}");
}

We add handleUserActivationMailMessage to listen queue messages and send activation emails as shown below.

@JmsListener(destination = "${user.activation.queue.name}")
public void handleUserActivationMailMessage(String json) {
       try {
             ObjectMapper mapper = new ObjectMapper();

             Map<String, String> data = mapper.readValue(json, Map.class);

             String username = data.get("username");
             String activationUrl = data.get("activationUrl");

             User existing = userRepository.findOne(username);

             if (existing != null && !existing.getActivationStatus().equals(User.ACTIVATION_STATUS_DONE)) {
                    System.out.println("Sending activation mail for user " + username);

                    sendActivationMailForUser(existing, activationUrl);

                    existing.setActivationStatus(User.ACTIVATION_STATUS_MAIL_SENT);
                    userRepository.save(existing);
             }
       } catch (Exception ex) {
             throw new RuntimeException("Encountered error while processing user activation message.", ex);
       }
}

private void sendActivationMailForUser(User user, String activationUrlBase) {

       String activationUrl = activationUrlBase + "?username=" + user.getUsername() + "&token="
                           + user.getActivationToken();

       String to = user.getEmail();
       String subject = "Activate your Digital Card Store account";
       String body = "<html><body><br/>" + "Dear " + user.getName() + "<br/>" + "<a href=\"" + activationUrl
                    + "\">Please click to activate your user account " + user.getUsername() + "</a><br/>"
                    + "</body></html>";

       sesService.sendMessage(to, subject, body);
}

We add activateUser as shown below. This method will be called when a user is clicked the activation link in the activation email.

@RequestMapping("/activate")
public String activateUser(Map<String, Object> model, @RequestParam("username") String username,
             @RequestParam("token") String token) {

       User user = userRepository.findOne(username);

       if (user == null)
             model.put("result", "User not found: " + username);
       else if (user.getActivationStatus().equals(User.ACTIVATION_STATUS_DONE))
             model.put("result", "User " + username + " already activated.");
       else if (!user.getActivationToken().equals(token))
             model.put("result", "Activation token for user " + username + " is not correct.");
       else {
             user.setActivationStatus(User.ACTIVATION_STATUS_DONE);
             userRepository.save(user);
             model.put("result", "User " + username + " activated successfully.");
       }
       return "activationResult";
}

4. Start EC2 instances with SQS and SES permissions

When we deploy our application to the EC2, we should start EC2 instances with an IAM role that have AmazonSQSFullAccess and  AmazonSESFullAccess permissions.

Also we should give mail.from.address and user.activation.queue.name parameters accordingly. We can use EC2 instance tags and init scripts to set these parameters.

After we deploy our application, the activation process will be enabled. After an user is registered, registration emails are sent to the user. Users will be allowed to login only after activation. Activation can be done by clicking activation link in the mail. You can use the application like the screenshots below.







Summary

In this post, I have added user activation process to prevent fake users. I have used Amazon SQS queues to send and receive messages. To send emails Amazon SES is used. The code can be found at my GitHub repository.

In my next posts, I will continue to use various AWS services to add functionality to my digital card store application.


7 comments:

  1. Needed to compose you a very little word to thank you yet again regarding the nice suggestions you’ve contributed here.

    AWS Training in Bangalore|

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Email Marketing is a great way of promoting your business.

    email verification service

    ReplyDelete
  4. This article are supper help full if you want to now more aboutsolepaycard.com activatethen please click here:

    ReplyDelete
  5. Nice Article, Thank you for sharing a wonderful blog post.
    DevOps Training
    DevOps Online Training

    ReplyDelete
  6. Some may stag in Interviews!!! OOPS!! More than 50% of students do this in their career. Instead, do Hadoop Training in Chennai at Infycle. Those students can easily clear this Interview session because more than 5 times at INFYCLE practicing mock-interview sessions, Hence students are Getting out of their interview fear.

    ReplyDelete