https://www.learn2crack.com/2016/04/android-login-registration-php-mysql-reset-password.html



In previous tutorials we have learned about created a basic Login Registration system. But it lacks a feature, reset password by email. We will add some additional code to previous one to add this feature. We need to modify code both in Server and Client.




Selection_024
Some changes when compared to the old one,

-> Use PHPMailer for sending smtp mail from localhost.
-> Added methods for sending mail using PHPMailer or using default php mail() function.
-> Reset password verification code expires in 120 seconds, which can also be changed.

Working

We have created a separate table for password reset request. If the user initiates password reset request, a unique key is generated and stored in the new table with time stamp which is then sent to the client by email. The client sends the verification key along with new password to the server. If the verification code is correct and the time elapsed is less than 120 seconds the password is changed. If the time exceeds 120 seconds the user must re initiate password reset request.

Now let us see what changes to be made in Server and Client.

Prerequisite

This is the third tutorial in the series Android Login Registration System with PHP and MySQL. Take a look at the previous one before proceeding. To view the first tutorial.

Complete Project Files

You can download the complete project as zip or fork from our Github repository.

Server

Creating Password Reset table

Enter the following query to create password reset table with email, encrypted_temp_password, salt, created_at fields which will be similar to users table.

CREATE TABLE password_reset_request (
sno int(11) NOT NULL AUTO_INCREMENT,
email varchar(50) NOT NULL,
encrypted_temp_password varchar(256) NOT NULL,
salt varchar(10) NOT NULL,
created_at datetime DEFAULT NULL,
PRIMARY KEY (sno)
)

We are going to use PHPMailer library for sending mail from localhost. Download PHPMailer from the Github repository. The downloaded PHPMailer should be placed in root of your project.

learn2crack-login-register -> PHPMailer

Lets discuss the additional methods and code defined in each of the three php files.

DBOperations.php

Here we have added two methods passwordResetRequest() and resetPassword(). The passwordResetRequest() method is for the first step of the reset password process. A new random string is generated which is encrypted and stored in database along with time stamp, email. This code is returned which is send to user by email. If the row already exists it us updated with new data.

The resetPassword() method is for final step of the password reset process. Here the unique code is verified with time elapsed. If the time is less than 120 seconds the old password is replaced with new password else the process is terminated.

DBOperations.php

<?php

class DBOperations{

    public function passwordResetRequest($email){

        $random_string = substr(str_shuffle(str_repeat("0123456789abcdefghijklmnopqrstuvwxyz", 6)), 0, 6);
        $hash = $this->getHash($random_string);
        $encrypted_temp_password = $hash["encrypted"];
        $salt = $hash["salt"];

        $sql = 'SELECT COUNT(*) from password_reset_request WHERE email =:email';
        $query = $this -> conn -> prepare($sql);
        $query -> execute(array('email' => $email));

        if($query){

            $row_count = $query -> fetchColumn();

            if ($row_count == 0){

                $insert_sql = 'INSERT INTO password_reset_request SET email =:email,encrypted_temp_password =:encrypted_temp_password,
                salt =:salt,created_at = :created_at';
                $insert_query = $this ->conn ->prepare($insert_sql);
                $insert_query->execute(array(':email' => $email, ':encrypted_temp_password' => $encrypted_temp_password,
                        ':salt' => $salt, ':created_at' => date("Y-m-d H:i:s")));

                if ($insert_query) {

                    $user["email"] = $email;
                    $user["temp_password"] = $random_string;

                    return $user;

                } else {

                    return false;

                }
            } else {

                $update_sql = 'UPDATE password_reset_request SET email =:email,encrypted_temp_password =:encrypted_temp_password,
                salt =:salt,created_at = :created_at';
                $update_query = $this -> conn -> prepare($update_sql);
                $update_query -> execute(array(':email' => $email, ':encrypted_temp_password' => $encrypted_temp_password,
                        ':salt' => $salt, ':created_at' => date("Y-m-d H:i:s")));

                if ($update_query) {

                    $user["email"] = $email;
                    $user["temp_password"] = $random_string;
                    return $user;

                } else {

                    return false;

                }
            }
        } else {

            return false;
        }
    }

    public function resetPassword($email,$code,$password){

        $sql = 'SELECT * FROM password_reset_request WHERE email = :email';
        $query = $this -> conn -> prepare($sql);
        $query -> execute(array(':email' => $email));
        $data = $query -> fetchObject();
        $salt = $data -> salt;
        $db_encrypted_temp_password = $data -> encrypted_temp_password;

        if ($this -> verifyHash($code.$salt,$db_encrypted_temp_password) ) {

            $old = new DateTime($data -> created_at);
            $now = new DateTime(date("Y-m-d H:i:s"));
            $diff = $now->getTimestamp() - $old->getTimestamp();

            if($diff < 120) {

                return $this -> changePassword($email, $password);

            } else {

                false;
            }
        } else {

            return false;
        }
    }
}

Functions.php

Two additional methods resetPasswordRequest() and resetPassword() are added for reset password initiation and finishing.

The methods sendEmail() and sendPHPMail() are used for sending emails. Use either one of them. The sendEmail() method is used to send smtp mail. In the code I have defined for Gmail smtp settings. If you run this code on a server you can use sendPHPMail() function. It sends mail using php mail() function.

Functions.php

<?php
 require_once 'DBOperations.php';
require 'PHPMailer/PHPMailerAutoload.php';

class Functions{

    private $db;
    private $mail;

    public function __construct() {

        $this -> db = new DBOperations();
        $this -> mail = new PHPMailer;

    }

    public function resetPasswordRequest($email){

        $db = $this -> db;

        if ($db -> checkUserExist($email)) {

            $result =  $db -> passwordResetRequest($email);

            if(!$result){

                $response["result"] = "failure";
                $response["message"] = "Reset Password Failure";
                return json_encode($response);

            } else {

                $mail_result = $this -> sendEmail($result["email"],$result["temp_password"]);

                if($mail_result){

                    $response["result"] = "success";
                    $response["message"] = "Check your mail for reset password code.";
                    return json_encode($response);

                } else {

                    $response["result"] = "failure";
                    $response["message"] = "Reset Password Failure";
                    return json_encode($response);
                }
            }
        } else {

            $response["result"] = "failure";
            $response["message"] = "Email does not exist";
            return json_encode($response);

        }
    }

    public function resetPassword($email,$code,$password){

        $db = $this -> db;

        if ($db -> checkUserExist($email)) {

            $result =  $db -> resetPassword($email,$code,$password);

            if(!$result){

                $response["result"] = "failure";
                $response["message"] = "Reset Password Failure";
                return json_encode($response);

            } else {

                $response["result"] = "success";
                $response["message"] = "Password Changed Successfully";
                return json_encode($response);

            }
        } else {

            $response["result"] = "failure";
            $response["message"] = "Email does not exist";
            return json_encode($response);

        }
    }

    public function sendEmail($email,$temp_password){

        $mail = $this -> mail;
        $mail->isSMTP();
        $mail->Host = 'smtp.gmail.com';
        $mail->SMTPAuth = true;
        $mail->Username = 'your.email@gmail.com';
        $mail->Password = 'password';
        $mail->SMTPSecure = 'ssl';
        $mail->Port = 465;

        $mail->From = 'your.email@gmail.com';
        $mail->FromName = 'Your Name';
        $mail->addAddress($email, 'Your Name');

        $mail->addReplyTo('your.email@gmail.com', 'Your Name');

        $mail->WordWrap = 50;
        $mail->isHTML(true);

        $mail->Subject = 'Password Reset Request';
        $mail->Body    = 'Hi,<br><br> Your password reset code is <b>'.$temp_password.'</b> . This code expires in 120 seconds. Enter this code within 120 seconds to reset your password.<br><br>Thanks,<br>Learn2Crack.';

        if(!$mail->send()) {

            return $mail->ErrorInfo;

        } else {

            return true;

        }
    }

    public function sendPHPMail($email,$temp_password){

        $subject = 'Password Reset Request';
        $message = 'Hi,nn Your password reset code is '.$temp_password.' . This code expires in 120 seconds. Enter this code within 120 seconds to reset your password.nnThanks,nLearn2Crack.';
        $from = "your.email@example.com";
        $headers = "From:" . $from;

        return mail($email,$subject,$message,$headers);

    }
}

index.php

Two additional operations resPassReq and resPass are added to handle reset password process.

index.php

if ($operation == 'resPassReq') {

if(isset($data -> user) && !empty($data -> user) &&isset($data -> user -> email)){

$user = $data -> user;
$email = $user -> email;

echo $fun -> resetPasswordRequest($email);

} else {

echo $fun -> getMsgInvalidParam();

}
}else if ($operation == 'resPass') {

if(isset($data -> user) && !empty($data -> user) && isset($data -> user -> email) && isset($data -> user -> password)
&& isset($data -> user -> code)){

$user = $data -> user;
$email = $user -> email;
$code = $user -> code;
$password = $user -> password;

echo $fun -> resetPassword($email,$code,$password);

} else {

echo $fun -> getMsgInvalidParam();

}
}

Request and Responses

For reset password initiation the request would be similar to,




{
   "operation":"resPassReq",
   "user":{
       "email":"raj.amalw@learn2crack.com"

   }
}

and if the request is sucess the response would be similar to,

{
  "result": "success",
  "message": "Check your mail for reset password code."
}

For finishing password reset process the request would be similar to,

{
   "operation":"resPass",
   "user":{
       "email":"raj.amalw@learn2crack.com",
       "code":"bcfqa3",
       "password":"rajamalw"

   }
}

and if the request is success the response would be similar to,

{
  "result": "success",
  "message": "Password Changed Successfully"
}

Client

Creating Layout

A new layout fragment_password_reset is created for the password reset process. It has three EditText widgets to get email, code and password as input. We also have a TextView to display a countdown timer.

fragment_password_reset.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_marginLeft="40dp"
    android:layout_marginRight="40dp"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:text="Learn2Crack"
        android:textSize="22sp"
        android:textAlignment="center"
        android:layout_marginBottom="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/timer"
        android:textSize="22sp"
        android:textStyle="bold"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <android.support.design.widget.TextInputLayout
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:hint="Email"
            android:id="@+id/et_email"
            android:drawableRight="@drawable/ic_email"
            android:inputType="textEmailAddress"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:hint="Code"
            android:id="@+id/et_code"
            android:drawableRight="@drawable/ic_key"
            android:inputType="textPassword"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:hint="Password"
            android:id="@+id/et_password"
            android:drawableRight="@drawable/ic_key"
            android:inputType="textPassword"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </android.support.design.widget.TextInputLayout>

    <android.support.v7.widget.AppCompatButton
        android:id="@+id/btn_reset"
        android:text="Forgot Password"
        android:background="@color/colorPrimary"
        android:textColor="@android:color/white"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <ProgressBar
        style="@style/Base.Widget.AppCompat.ProgressBar"
        android:id="@+id/progress"
        android:visibility="invisible"
        android:layout_marginTop="10dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:indeterminate="true" />

</LinearLayout>

Creating Fragment

Before creating a new Fragment add these constants to Constants.java file.

public static final String RESET_PASSWORD_INITIATE = "resPassReq";
public static final String RESET_PASSWORD_FINISH = "resPass";

Then modify the User.java model class to add code field.

private String code;

Here we have two methods initiateResetPasswordProcess() and finishResetPasswordProcess(). When success response is received in initiateResetPasswordProcess() method the count down timer is started, hidden code and new password fields are displayed. After getting code and new password as input finishResetPasswordProcess() is called. If the time runs out the user is automatically redirected to the login screen.

ResetPasswordFragment.java

package com.learn2crack.loginregistration;

import android.app.Fragment;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.AppCompatButton;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.learn2crack.loginregistration.models.ServerRequest;
import com.learn2crack.loginregistration.models.ServerResponse;
import com.learn2crack.loginregistration.models.User;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class ResetPasswordFragment extends Fragment implements View.OnClickListener{

    private AppCompatButton btn_reset;
    private EditText et_email,et_code,et_password;
    private TextView tv_timer;
    private ProgressBar progress;
    private boolean isResetInitiated = false;
    private String email;
    private CountDownTimer countDownTimer;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.fragment_password_reset,container,false);
        initViews(view);
        return view;
    }

    private void initViews(View view){

        btn_reset = (AppCompatButton)view.findViewById(R.id.btn_reset);
        tv_timer = (TextView)view.findViewById(R.id.timer);
        et_code = (EditText)view.findViewById(R.id.et_code);
        et_email = (EditText)view.findViewById(R.id.et_email);
        et_password = (EditText)view.findViewById(R.id.et_password);
        et_password.setVisibility(View.GONE);
        et_code.setVisibility(View.GONE);
        tv_timer.setVisibility(View.GONE);
        btn_reset.setOnClickListener(this);
        progress = (ProgressBar)view.findViewById(R.id.progress);

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){

            case R.id.btn_reset:

                if(!isResetInitiated) {

                    email = et_email.getText().toString();
                    if (!email.isEmpty()) {
                        progress.setVisibility(View.VISIBLE);
                        initiateResetPasswordProcess(email);
                    } else {

                        Snackbar.make(getView(), "Fields are empty !", Snackbar.LENGTH_LONG).show();
                    }
                } else {

                    String code = et_code.getText().toString();
                    String password = et_password.getText().toString();

                    if(!code.isEmpty() && !password.isEmpty()){

                        finishResetPasswordProcess(email,code,password);
                    } else {

                        Snackbar.make(getView(), "Fields are empty !", Snackbar.LENGTH_LONG).show();
                    }

                }

                break;
        }
    }

    private void initiateResetPasswordProcess(String email){

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(Constants.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        RequestInterface requestInterface = retrofit.create(RequestInterface.class);

        User user = new User();
        user.setEmail(email);
        ServerRequest request = new ServerRequest();
        request.setOperation(Constants.RESET_PASSWORD_INITIATE);
        request.setUser(user);
        Call<ServerResponse> response = requestInterface.operation(request);

        response.enqueue(new Callback<ServerResponse>() {
            @Override
            public void onResponse(Call<ServerResponse> call, retrofit2.Response<ServerResponse> response) {

                ServerResponse resp = response.body();
                Snackbar.make(getView(), resp.getMessage(), Snackbar.LENGTH_LONG).show();

                if(resp.getResult().equals(Constants.SUCCESS)){

                    Snackbar.make(getView(), resp.getMessage(), Snackbar.LENGTH_LONG).show();
                    et_email.setVisibility(View.GONE);
                    et_code.setVisibility(View.VISIBLE);
                    et_password.setVisibility(View.VISIBLE);
                    tv_timer.setVisibility(View.VISIBLE);
                    btn_reset.setText("Change Password");
                    isResetInitiated = true;
                    startCountdownTimer();

                } else {

                    Snackbar.make(getView(), resp.getMessage(), Snackbar.LENGTH_LONG).show();

                }
                progress.setVisibility(View.INVISIBLE);
            }

            @Override
            public void onFailure(Call<ServerResponse> call, Throwable t) {

                progress.setVisibility(View.INVISIBLE);
                Log.d(Constants.TAG,"failed");
                Snackbar.make(getView(), t.getLocalizedMessage(), Snackbar.LENGTH_LONG).show();

            }
        });
    }

    private void finishResetPasswordProcess(String email,String code, String password){

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(Constants.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        RequestInterface requestInterface = retrofit.create(RequestInterface.class);

        User user = new User();
        user.setEmail(email);
        user.setCode(code);
        user.setPassword(password);
        ServerRequest request = new ServerRequest();
        request.setOperation(Constants.RESET_PASSWORD_FINISH);
        request.setUser(user);
        Call<ServerResponse> response = requestInterface.operation(request);

        response.enqueue(new Callback<ServerResponse>() {
            @Override
            public void onResponse(Call<ServerResponse> call, retrofit2.Response<ServerResponse> response) {

                ServerResponse resp = response.body();
                Snackbar.make(getView(), resp.getMessage(), Snackbar.LENGTH_LONG).show();

                if(resp.getResult().equals(Constants.SUCCESS)){

                    Snackbar.make(getView(), resp.getMessage(), Snackbar.LENGTH_LONG).show();
                    countDownTimer.cancel();
                    isResetInitiated = false;
                    goToLogin();

                } else {

                    Snackbar.make(getView(), resp.getMessage(), Snackbar.LENGTH_LONG).show();

                }
                progress.setVisibility(View.INVISIBLE);
            }

            @Override
            public void onFailure(Call<ServerResponse> call, Throwable t) {

                progress.setVisibility(View.INVISIBLE);
                Log.d(Constants.TAG,"failed");
                Snackbar.make(getView(), t.getLocalizedMessage(), Snackbar.LENGTH_LONG).show();

            }
        });
    }

    private void startCountdownTimer(){
        countDownTimer = new CountDownTimer(120000, 1000) {

            public void onTick(long millisUntilFinished) {
                tv_timer.setText("Time remaining : " + millisUntilFinished / 1000);
            }

            public void onFinish() {
                Snackbar.make(getView(), "Time Out ! Request again to reset password.", Snackbar.LENGTH_LONG).show();
                goToLogin();
            }
        }.start();
    }

    private void goToLogin(){

        Fragment login = new LoginFragment();
        FragmentTransaction ft = getFragmentManager().beginTransaction();
        ft.replace(R.id.fragment_frame,login);
        ft.commit();
    }
}

Finally add a button to Login Screen to view the PasswordResetFragment. Add when it is pressed call the following method.

private void goToResetPassword(){

    Fragment reset = new ResetPasswordFragment();
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    ft.replace(R.id.fragment_frame,reset);
    ft.commit();
}

Screenshots