JWT Auth with an Elixir on Phoenix 1.4 API and React Native, Part I: Phoenix JWT API
Update: This guide has been updated to Phoenix 1.4 from 1.3. The only changes: run --no-webpack
rather than --no-brunch
through mix phx.new
and use Jason instead of Poison for JSON encoding.
The JSON Web Token (JWT) standard is a popular, battle-tested method for secure data exchange between APIs and clients.
In this multi-part tutorial, we will walk through how to create an Elixir on Phoenix 1.4 JWT REST API and a React Native mobile app with which we can register, authenticate, and view protected user resources using JWTs.
The Phoenix JWT API we will write here in Part I will be usable with any sort of REST client, including web apps, mobile apps, and/or desktop software.
In Part II & Part III, we’ll write our client in React Native, a brilliant cross-platform, fully-native mobile app development framework (note: ReactJS is a library, React Native is a framework).
Let’s begin!
Part I: Elixir on Phoenix JWT API
Phoenix App Generation and Configuration
If you haven’t already, install Elixir, PostgreSQL, and the Phoenix framework before getting started with this section.
Optional: download the Postman API development tool and a PostgreSQL client such as Postico (macOS) for easier development. We will test our API with Postman during this tutorial.
Generate an API-only Phoenix app with the following command:
Note: If you are using Phoenix 1.3, you should pass --no-brunch
rather than --no-webpack
into the above generator
The above command generates a new Phoenix app called MyApi, without HTML or webpack. Webpack is the Phoenix’s default JS bundling tool.
Set a valid PostgreSQL user for your machine in config/dev.exs:
By default, Phoenix is configured to connect to your dev PostgreSQL database with username: “postgres”, password: “postgres”
.
If you do not want to have to alter config/dev.exs every time you create a new Phoenix project, initiate the PostgreSQL command line (psql) and run the following command:
Back on your regular command line, cd
into your app directory and create your app’s PostgreSQL database with mix ecto.create
.
Start your development server with the mix phx.server
command and your app should start running from localhost:4000.
Installing Phoenix Dependencies
We will be using the Guardian Elixir package for JWT authentication and Comeonin + bcrypt_elixir* for password encryption.
Add these dependencies to mix.exs in your Phoenix app’s root directory:
*If you are using a version of Erlang < 20.0, use {:bcrypt_elixir, “~> 0.12”} rather than 1.0.
Run mix deps.get
to install these added dependencies.
User Generation: Controller, Context, Schema, and JSON View
Using another Phoenix command line generator for resources, we are going to create our User controller, context for database logic, schema, database migration, and JSON API view.
Run the following command on your CLI to generate our user resource code:
mix phx.gen.json Accounts User users email:unique password_hash:string
The phx.gen.json command can be broken down as:
phx.gen.json [Context] [Schema Name] [plural schema name = db table name] [database field:data type]
Passing :unique in as a field’s data type defaults that data type to string while making that field a unique index in its respective table. We do not need to add a user_id field, as it will be added to our PostgreSQL users table by default.
(Side note: If you want to create a unique field with a different data type, such as an integer, pass the data type in last, as in field:unique:integer)
Contexts are where the database business logic (CRUD, get, etc) for your resources live.
You can (and should) use a context for more than one resource, e.g. if we wanted our users to have profiles, we could generate a profile in the Accounts context with phx.gen.json Accounts Profile profiles name:string
…etc.
Before migrating our user, we have to add the UserController to our router — but before we do that, let’s take care of…
Password Encryption and Field Validation
We will now add the logic for hashing passwords and validating field data.
Open lib/myApi/accounts/user.ex . The default generated file should look like:
Add the virtual fields :password and :password_confirmation to the schema:
The virtual:true fields are never saved on our server, they only accept the password and password_confirmation logic that our Phoenix app encrypts into our user’s password_hash.
Now edit our changeset:
In the above code, we:
- Remove :password_hash from cast() and validate_required() in our user changeset and add :password & :password_confirmation
- Add email validation with
validate_format(:email, ~r/@/)
- Validate that password input is at least 8 characters long with
validate_length(:password, min: 8)
- Validate that password input is the same as the password confirmation with
validate_confirmation(:password)
Now we will create put_password_hash(changeset)
as a private function and add it to the end of our changeset pipeline:
The put_password_hash function takes in our changeset, takes our :password from the changeset as pass, then encrypts pass with our hashing libraries through the function Comeonin.Bcrypt.hashpwsalt(pass)
within the put_change function.
The encrypted hash from our hashpwsalt(pass) function is then matched to :password_hash and added to our changeset, hence put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(pass))
.
After writing our put_password_hash function, add the function to the end of our changeset pipeline with |> put_password_hash
.
To clean up our code a bit, let’s import hashpwsalt/1 from Comeonin.Bcrypt at the top of user.ex and trim our put_change function:
Our user.ex file is now complete:
User API Endpoint Routing
Let’s set our Phoenix router to accept user creation POST requests.
- Open lib/myApi_web/router.ex
- Change
scope “/api"
toscope "/api/v1"
Using API versioning (e.g. /v1) in our routes makes it easy to improve and change our APIs without disabling apps written for older versions of our API.
3. Within our /api/v1 scope, add resources “/users", UserController, only: [:create, :show]
.
This resources line creates the /api/v1/users route, and gives it access to the create and show functions in our UserController.
By adding only: [:create, :show] to our public API route, we are only allowing unauthenticated users to access our controller’s create and show functions; unauthenticated users can not access delete, update, or any other UserController methods through this public endpoint.
Our router should now look like this:
With our UserController set in our router, run mix ecto.migrate
to take care of the migration we generated earlier.
Now let’s test this endpoint with Postman. Start your Phoenix server with mix phx.server
and do the following:
- Open Postman
- Set your request type to POST
- Set your request URL to http://localhost:4000/api/v1/users
4. Under Body, enter raw JSON
5. Enter user JSON data in the body of our POST request:
6. Hit Send!
If our request is successful, you should receive a JSON response that looks something like this:
How this works:
When our POST request successfully posts our JSON data to the /api/v1/users endpoint, our JSON post data is run through the create function of our UserController:
The content of our User: {} JSON object is matched to user_params in the second argument of our create function.
Our user_params are then run through our Accounts.create_user function, which refers to the create_user Repo function in our Accounts Context file (lib/myApi/accounts/accounts.ex).
If create_user returns :ok, our user has been successfully created, and the user database record is run through the conn pipeline within the with statement.
With |> put_resp_header("location", user_path(conn, :show, user))
and |> render("show.json", user:user)
, the response to our successful post request returns the user resource we created to us via :show.
In a future step, we will remove this rather useless “show” logic and replace it with a function serving JSON Web Tokens.
But before we can do that, we have to be able to generate and encode tokens. For this, we will use Guardian.
Generating JSON Web Tokens
Guardian Configuration
First, let’s add the following Guardian configuration to config/config.exs:
Generate a secret key with mix guardian.gen.secret
and replace the string after secret_key:
with it. Our secret key is used to generate and validate our JWTs, while and issuer: “myApi” applies “myApi” to the issuer (“iss”)field of our JWts.
Now, in order to use Guardian JWTs, we have to create a Guardian module for our project that adds a resource subject to our Guardian-generated token’s claims.
- Create a file called guardian.ex in your /lib directory.
- Add the following Guardian module code to guardian.ex:
Our subject_for_token(resource, _claims) function takes in a resource (user in this case), finds a property/field by which the token will be able to identify that resource (herein user_id, accessed by user.id), and adds it to the token’s claims as “sub”(subject).
Our resource_from_claims(claims) function gets the “sub” from a token as id, then tries to retrieve the user with that id through MyApi.Accounts.get_user!(id).
If a resource is returned from get_user!(id), resource_from_claims returns :ok and the retrieved resource.
For more on Guardian configuration and customization, check out the Guardian docs on Github.
Serving a Token on Registration
With Guardian wired up, we are ready to serve tokens!
Open lib/myApi_web/views/user_view.ex. Our user_view.ex file is responsible for rendering our API’s JSON data.
Let’s write a method in this view that renders our JWT:
This render method takes in a location name (“jwt.json”), a JWT (%{jwt: jwt}), and renders that JWT at the location specified.
Now, let’s open lib/myApi_web/controllers/user_controller.ex and add alias MyApi.Guardian
near the beginning of our controller.
Then, edit our create() function to serve a JWT upon successful user creation by adding a Guardian.encode_and_sign(user)
statement to our existing accounts.create_user() with statement:
Now, replace the old conn pipeline with a single-line conn pipe, conn |> render("jwt.json", jwt: token)
.
Our create() function should now look like this:
Since we no longer use show in our user creation function, let’s replace our “/users” resources route in router.ex to a create-only POST request route at “/sign_up”:
Go back to Postman and send a new POST request to /api/v1/sign_up, this time with a different email address:
We’re now getting some sweet JWT action!
… but now our users can only receive JWTs upon registration. Let’s change that with…
Token-serving Sign In
Context Authentication Functions
For user authentication, a user will input an email and password and receive a JWT if their credentials match credentials in our database.
Since authenticating existing users involves interacting directly with our database, let’s put our sign in logic inside of our Accounts context.
Open lib/myApi/accounts/accounts.ex.
Alias MyApi.Guardian and import Comeonin.Bcrypt’s checkpw + dummy_checkpw functions near the top of our context module:
checkpw/2 takes in a virtual password and checks it against our password hash (checkpw(password, password_hash)
).
dummy_checkpw/0 makes it seem to a client like a process is running to check a password, and we will use it when someone attempts to log in with an unregistered email so that user enumeration by bad actors will be more difficult.
Now, let’s create a get_by_email(email)
function to retrieve a user by email:
If our Repo.get_by(User, email: email)
function does not find a user with a given email input, it will run dummy_checkpw/0. If a user is found, a user is returned.
Now, let’s verify password input:
Here we use checkpw/2 to check our user input password against our encrypted hash.
Let’s put our get_by_email and verify_password functions together in a private email_password_auth(email, password) function:
Lastly, let’s use Guardian.encode_and_sign(user)
to encode and sign a token for our authenticated user:
Sign In Route and Controller Function
Let’s add a function to our user_controller.ex that renders a JWT when our user successfully passes our Accounts.token_sign_in/2 function, and an :unauthorized error when sign in fails:
In our FallbackController, let’s add a call function to handle any :unauthorized errors:
Let’s add a sign in POST route to our public API scope in router.ex:
post "sign_in", UserController, :sign_in
reveals our UserController’s sign_in function to POST requests sent to our “api/v1/sign_in” route.
Let’s start up our server and test this route in Postman, by posting our registered email and password to http:localhost:4000/api/v1/sign_in:
Success!
But these JWTs are pretty useless if we don’t have any authenticated routes…
Authenticated Routes
In order to take advantage of all of the pipeline goodness that comes with Guardian, we have to create an AuthPipeline for our app’s Guardian module.
Create auth_pipeline.ex in lib/myApi_web/ and add the following plug code to it:
VerifyHeader verifies the token in our request header while LoadResource loads the resource present in our claims subject, in this case a user record. EnsureAuthenticated is self-explanatory.
Create the AuthErrorHandler module referenced above, as auth_error_handler.ex in lib/myApi_web/ and add the following code:
Our auth_error function with serve an error response (send_resp) with an error encoded by Jason in the event of a user’s failure to successfully pass through the authentication pipeline.
Note: If you are using Phoenix 1.3, you would likely use Poison above rather than Jason, as Poison is 1.3’s default JSON encoder.
Go back to our router.ex. Let’s alias our Guardian module, then create a :jwt_authenticated pipeline and add our Guardian.AuthPipeline plug to it:
Now let’s create a new JWT authenticated scope for “/api/v1” under our existing unauthenticated scope, and add a GET route for the show function in our UserController:
Here we are piping any routes listed in our second scope through both our :api and our :jwt_authenticated scopes.
As it is written now, our “/my_user” route will throw an error because our existing show function requires a resource id argument.
Let’s go back to user_controller.ex and replace our old show function with a new one:
This new show function gets the current resource (a user) claimed in our conn’s JWT via Guardian.Plug.current_resource(conn) and matches it to user. Our conn pipeline then renders the returned resource for our client through our “user.json” view.
If the client trying to access show does not have a proper JWT, our show function will render an :unauthenticated error, because it is scoped through the :jwt_authenticated pipeline in our router.
Even if this function were not scoped through :jwt_authenticated, it would not work without a valid JWT, as this show function finds our resource on our client’s JWT rather than via a direct database query with an id param.
JWT-Authenticated GET Request in Postman
Let’s test a JWT-authenticated request.
In order for a request to be JWT-authenticated , our client has to include the header "Authorization": "Bearer [some valid JWT here]"
.
Open Postman, set your request to GET, and enter the request URL http://localhost:4000/api/v1/my_user .
Under Authorization, select TYPE: Bearer Token and paste your JWT into the Token field.
(If you need a JWT, either POST a user to api/v1/sign_up or sign in an existing user with a POST to api/v1/sign_in as we did earlier in this tutorial. Either POST should respond with a JWT.)
Now, with our Phoenix app running, hit Send:
We are now using our JWT to return an authenticated resource from our Elixir on Phoenix REST API!
Inspecting our JWT
Let’s copy and paste one of our JWTs into the JWT Debugger:
Decoded, we see our token’s payload contains “iss”: “myApi” and “sub”: “9” — our claim to [resource subject]: 9, or, in the context of our app, as user_id: 9.
If the client retrieving this JWT then makes authenticated requests containing it, an app should then have access to resources tied to the JWT’s subject.
Note: You can check to see if your JWT is valid by entering your Guardian secret under “Verify Signature”, which is cool when you are testing JWT functions with a development secret that is not used in production. However, NEVER PUT YOUR PRODUCTION SECRETS INTO THIRD-PARTY DEBUGGERS OF ANY KIND.
A Note on HTTP/HTTPS
The API we wrote in this section is configured to accept API requests over HTTP rather than HTTPS.
In production, configure your app to use a secure protocol such with TLS/SSL and only transfer data and JWTs through HTTPS, otherwise your client traffic will be unencrypted and easily sniffed.
Additionally, if you do not use HTTPS endpoints in your React Native apps, you will be unable to publish your apps in the Apple App Store or the Google Play Store.
Here is a great guide on securing Phoenix with SSL on Ubuntu, by Zek Interactive: Securing Your Phoenix App with Free SSL
Section Epilogue
We have successfully created an Elixir on Phoenix REST API for JSON Web Token Authentication!
Check out the full code here on Github.
Click here to continue on to Part II, Building a React Native JWT Client!
If you have any questions, comments, suggestions, or if you find any errors/bugs/typos, please comment below or join me in the Hoptok discord!
Special Thanks
Special thanks to the UeberAuth team for providing Guardian and other awesome open-source Elixir tools, and to David Whitlock, aka riverrun, for bcrypt_elixir, Comeonin, and all of his other amazing open-source Elixir code.
Also, many thanks to Voger and the ElixirForum community for the feedback!
🍹Tips Appreciated! 😉
My Bitcoin address: 1QJuBzHpis4jqQXnSuYxKzGS4Yu3GHhNtX