Seamless website integration of the password-reset-example for simple auth product creators

This applies to product creators…you develop your product and for speed to market and simplicity you opt for simple auth - let Particle handle it! Until you realize when a user needs to reset a password the normal developer process of resetting passwords is no longer (or soon will be no longer) available for product mode users, nor an ideal experience for that matter.

Good news, Particle put some effort into helping us bridge this with the password-reset-example.

I’ll share here 3 key areas: General tips, how to seamlessly integrate it and brand it inside your existing website, and custom domain options. I’m using Windows so some of the links/instructions are specific to Windows.

General tips:

  • How much will Heroku cost? As far as I can tell, free. More on this later.
  • How long will this take? About 2 hours if you’re fairly familiar (but not an expert) with the range of tech skills this is going to require.
  • Can I do this on my own web server instead of Heroku? Only if you have a dedicated server and root access. If you’re on a shared hosting plan, even though you can use PostgreSQL, you won’t be able to deploy all the tools you need.
  • Keep this accessible while following instructions in the read.me for the example. Personally, I found this to be one of the better documented examples from Particle.
  • First off, while you read, get started on installing PostgreSQL on your local machine. It takes a long time to do its thing and if you wait until that step in the process you’ll be waiting and waiting. You don’t need Stack Builder, but you will need to install the server and command line tools
  • First time around I installed it in a directory connected to a cloud drive (Google Drive/Onedrive). Mistake. Even when you’re not hitting the database it seems to constantly create file updates, maybe log files that it is running and your machine will keep syncing these files constantly. Just install it in the default directory.

  • Google it if you’re not familiar with editing your PATH environmental variable.
  • While PostgreSQL installs, follow the read.me starting with setting up Heroku
  • heroku addons:create heroku-postgresql:hobby-dev : On this step, I was worried about hobby-dev plan costs vs free tier. But from what I can tell this won’t cost a thing. While free apps sleep automatically after 30 minutes, they wake up when a web request is received. Just stick with hobby-dev and don’t mess around with this.
  • Create tables step: when you’re done with this step quit it using “/q”. You need to get back to the command line for the next step to set environmental variables.
  • Set environmental variables step: Before you start, if you don’t have it, you’ll need to setup an email account that will send the password reset details. I’ll wait…
    OK, great, you’re back with the details. You have your Particle access token too? No? I would go to your Particle dashboard > product > Authentication. Create a new client to use for this, type = Simple Auth (Web App). The redirect is probably not going to be used, just use your primary domain name. Once you’ve copied your client ID and secret (this is not your access token), click the client to edit it and give the scope “full permissions”. On your command line use: curl https://api.particle.io/oauth/token -u "clientID:clientsecret" -d grant_type=password -d username=developer@example.com -d password=Password -d expires_in=0
    Copy the access token.
  • It may be smooth sailing until you get to “heroku config:set SMTP_PASSWORD=secret” particularly if you actually have a strong password containing symbols. All you need to do is put the password in quotation marks: heroku config:set SMTP_PASSWORD="&se$cret3"
  • For me, the email format instructed didn’t work. I found this works with no problems and displays the email headers correctly:
heroku config:set EMAIL_FROM="Brand Password Reset <password-reset@example.com>"
  • Deploy remotely and test the basic functionality.

How to seamlessly integrate it and brand it inside your existing website

  • If you have a large IoT budget and developer resources you may opt to skip this section and setup your custom domain (see next section) and customize the app on Heroku using pug.
  • You want to control your brand and have the user trust the password reset process. Users will be suspicious being sent to sheltered-spire-62734.herokuapp.com for example. Below is the script I created that enables you to have the password reset functionality hosted within your current website, maintaining your colors, logo, navigation, etc.
  • Create a new page on your website, example /password-reset.html. At the location you want users to enter their email address to request a reset (or create a new password), use the script below. You’ll replace the URL with your own Heroku app address.
<script type="text/javascript">
var paramsString = window.location.search;
var searchParams = new URLSearchParams(paramsString);

var token = searchParams.getAll("https://sheltered-spire-62734.herokuapp.com/passwordReset?token");

 var form = 'https://sheltered-spire-62734.herokuapp.com/';
 var thisScript = document.scripts[document.scripts.length - 1];
 var iframe = document.createElement('iframe');

 if (token.length > 0){  //if there is a token go to page to set new password, passing along the token
  iframe.setAttribute('src', 'https://sheltered-spire-62734.herokuapp.com/passwordReset?token=' + token);
 }
  else
 {
  iframe.setAttribute('src', 'https://sheltered-spire-62734.herokuapp.com/');  //no token present, go to page to enter email
 }

 iframe.setAttribute('width', '100%');

 thisScript.parentElement.replaceChild(iframe, thisScript);
</script>
  • Now you need to make sure that the reset email that goes out directs users to come back to this page (including passing the validation token).
  • Update the views/emailHtml.pug, for example to include branding and implement the custom link:
body
img.image(src='https://www.example.com/Logo.png')
p
p We received a request that you want to reset your password.
p If that was you, click the link below. Otherwise, you can ignore this message.
p
- var url = 'https://www.example.com/password-reset/?';
a(href=url + link) https://www.example.com/password-reset/?#{link}
  • Do the same for views/emailText.pug:
p We received a request that you want to reset your password.
If that was you, click the link below. Otherwise, you can ignore this message.
p https://www.example.com/password-reset/?#{link}
  • Now deploy the updates: $ git push heroku master and test.

  • Verify account by adding a credit card: https://dashboard.heroku.com/account/billing

  • This will also provide an additional 450 dyno hours (providing a 1,000 per month). For reference, during initial setup and testing my app used less than 4 hours. Since password resets should be an infrequent activity the hours used will likely never exceed the free usage amount.

  • Congrats, you’re done!

==================================
Here is what that experience should look like:

Your own domain and website with page you’ll direct users to reset their password:

Next, they’ll get an email directing them back to the same page, using your domain (with the heroku app address and token passed as a query). You can probably clean this up to even only use the token.

Finally, they click on the link in the email taking them back to your web page and they can enter their new password.


Technically, you could use 2 different pages with this process, or expand the script to control the instructions. Also, you may want two fields to confim the password, etc.

==================================

To use a custom domain/sub domain with your Heroku app:
Note, on the hobby plan you can create a custom domain, HOWEVER, it will be http and not https. For a secured https custom domain you will need to pay for a certificate (Expedited SSL starts at $14/month) and pay Heroku for the SSL Endpoint (Starts at $20/month).

By using my seamless integration method above, the form is https and the user experience is controlled on your existing website and domain so this step is not required.

5 Likes

well done, thank you!

This is perfect. Thanks for sharing!

ADDED: Changes required for your Android App to use this new infrastructure, in PasswordResetActivity.java you can use the modified code below (replacing with your own heroku URL:

package io.particle.android.sdk.accountsetup;

import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AlertDialog.Builder;
import android.view.View;
import android.widget.EditText;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

import javax.inject.Inject;

import butterknife.BindView;
import butterknife.ButterKnife;
import io.particle.android.sdk.cloud.ParticleCloud;
import io.particle.android.sdk.cloud.exceptions.ParticleCloudException;
import io.particle.android.sdk.devicesetup.ParticleDeviceSetupLibrary;
import io.particle.android.sdk.devicesetup.R;
import io.particle.android.sdk.devicesetup.R2;
import io.particle.android.sdk.di.ApModule;
import io.particle.android.sdk.ui.BaseActivity;
import io.particle.android.sdk.utils.Async;
import io.particle.android.sdk.utils.SEGAnalytics;
import io.particle.android.sdk.utils.TLog;
import io.particle.android.sdk.utils.ui.ParticleUi;
import io.particle.android.sdk.utils.ui.Ui;

import static io.particle.android.sdk.utils.Py.truthy;


public class PasswordResetActivity extends BaseActivity {

    private static final TLog log = TLog.get(PasswordResetActivity.class);

    public static final String EXTRA_EMAIL = "EXTRA_EMAIL";

    @Inject protected ParticleCloud sparkCloud;
    @BindView(R2.id.email) protected EditText emailView;

    public static Intent buildIntent(Context context, String email) {
        Intent i = new Intent(context, PasswordResetActivity.class);
        if (truthy(email)) {
            i.putExtra(EXTRA_EMAIL, email);
        }
        return i;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_password_reset);
        ParticleDeviceSetupLibrary.getInstance().getApplicationComponent().activityComponentBuilder()
                .apModule(new ApModule()).build().inject(this);
        ButterKnife.bind(this);

        SEGAnalytics.screen("Auth: Forgot password screen");
        ParticleUi.enableBrandLogoInverseVisibilityAgainstSoftKeyboard(this);

        Ui.findView(this, R.id.action_cancel).setOnClickListener(view -> finish());
        emailView.setText(getIntent().getStringExtra(EXTRA_EMAIL));
    }

    public void onPasswordResetClicked(View v) {



        SEGAnalytics.track("Auth: Request password reset");
        final String email = emailView.getText().toString();
        if (isEmailValid(email)) {

            // Particles Method to send email
           // performReset();
            // Your pseudo API to send email
         sendEmailToAPI(email);

        } else {
            new Builder(this)
                    .setTitle(getString(R.string.reset_password_dialog_title))
                    .setMessage(getString(R.string.reset_paassword_dialog_please_enter_a_valid_email))
                    .setPositiveButton(R.string.ok, (dialog, which) -> {
                        dialog.dismiss();
                        emailView.requestFocus();
                    })
                    .show();
        }





    }


    private void sendEmailToAPI(String email)
    {
        RequestQueue queue = Volley.newRequestQueue(this);
        String url ="https://sheltered-spire-62734.herokuapp.com/sendEmail?email="+email;

        StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
                new Response.Listener<String>()
                {
                    @Override
                    public void onResponse(String response)
                    {
                        onResetAttemptFinished("Instructions for how to reset your password will be sent " +
                                "to the provided email address.  Please check your email and continue " +
                                "according to instructions.");

                    }
                }, new Response.ErrorListener()
                {
                    @Override
                    public void onErrorResponse(VolleyError error)
                    {
                        onResetAttemptFinished("There is error performing the request. Try again!");
                    }
        });

// Add the request to the RequestQueue.
        queue.add(stringRequest);

    }

    private void performReset() {
        ParticleUi.showParticleButtonProgress(this, R.id.action_reset_password, true);

        Async.executeAsync(sparkCloud, new Async.ApiWork<ParticleCloud, Void>() {
            @Override
            public Void callApi(@NonNull ParticleCloud sparkCloud) throws ParticleCloudException {
                sparkCloud.requestPasswordReset(emailView.getText().toString());
                return null;
            }

            @Override
            public void onTaskFinished() {
                ParticleUi.showParticleButtonProgress(PasswordResetActivity.this, R.id.action_reset_password, false);
            }

            @Override
            public void onSuccess(@NonNull Void result) {
                onResetAttemptFinished("Instructions for how to reset your password will be sent " +
                        "to the provided email address.  Please check your email and continue " +
                        "according to instructions.");
            }

            @Override
            public void onFailure(@NonNull ParticleCloudException error) {
                log.d("onFailed(): " + error.getMessage());
                onResetAttemptFinished("Could not find a user with supplied email address, please " +
                        " check the address supplied or create a new user via the signup screen");
            }
        });
    }

    private void onResetAttemptFinished(String content) {
        new AlertDialog.Builder(this)
                .setMessage(content)
                .setPositiveButton(R.string.ok, (dialog, which) -> {
                    dialog.dismiss();
                    finish();
                })
                .show();
    }

    private boolean isEmailValid(String email) {
        return truthy(email) && email.contains("@");
    }

}


In the devicesetup build.gradle file under dependencies add:

implementation 'com.android.volley:volley:1.1.1'