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.
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.
<?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;
}
}
}
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.
<?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);
}
}
Two additional operations resPassReq and resPass are added to handle reset password process.
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();
}
}
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"
}
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>
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();
}