DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • How to Build a Pokedex React App with a Slash GraphQL Backend
  • Exploring API Headers
  • Allow Users to Track Fitness Status in Your App
  • Turn Your App into a Handy Health Assistant

Trending

  • Unlocking Data with Language: Real-World Applications of Text-to-SQL Interfaces
  • Building an AI/ML Data Lake With Apache Iceberg
  • Dropwizard vs. Micronaut: Unpacking the Best Framework for Microservices
  • IoT and Cybersecurity: Addressing Data Privacy and Security Challenges
  1. DZone
  2. Data Engineering
  3. Data
  4. Creating Your First Elm App: From Authentication to Calling an API (Part 2)

Creating Your First Elm App: From Authentication to Calling an API (Part 2)

In the first part of this tutorial, we introduced the Elm language by building a simple Elm Application that called an API. Now we'll authenticate with JSON Web Tokens to make protected API requests.

By 
Kim Maida user avatar
Kim Maida
·
Sep. 01, 16 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
7.2K Views

Join the DZone community and get the full member experience.

Join For Free

Elm is a reactive language that compiles to JavaScript. Its robust compiler and static typing make it a nice option for developing applications for the browser that are free of runtime errors. In the first part of this Elm tutorial, we built a small web app to introduce the language and its syntax.

Authenticating the Chuck Norris Quoter App

Now we'll continue to build out our Chuck Norris Quoter app to add user registration, login, and make authenticated API requests with JSON Web Tokens. We'll also use JavaScript interop to persist user sessions with local storage.

Registering a User

Last time, we finished up by retrieving Chuck Norris quotes from the API. We also need registration so users can be issued JSON Web Tokens with which to access protected quotes. We'll create a form that submits a POST request to the API to create a new user and return a token.

elm quote

After the user has registered, we'll display a welcome message:

elm quote

Here's the complete Main.elm code for this step:

Main.elm - Registering a User

Starting with new imports:

import Json.Decode as Decode exposing (..)
import Json.Encode as Encode exposing (..)


We'll be sending objects and receiving JSON now instead of just working with a string API response, so we need to be able to translate these from and to Elm records.

{- 
    MODEL
    * Model type 
    * Initialize model with empty values
    * Initialize with a random quote
-}

type alias Model =
    { username : String
    , password : String
    , token : String
    , quote : String
    , errorMsg : String
    }

init : (Model, Cmd Msg)
init =
    ( Model "" "" "" "" "", fetchRandomQuoteCmd )


Our model needs to hold more data than just a quote now. We've added username, password, token, and errorMsg (to display any API errors from authentication).

We'll initialize our application with empty strings for all of the above.

{-
    UPDATE
    * API routes
    * GET and POST
    * Encode request body 
    * Decode responses
    * Messages
    * Update case
-}

-- API request URLs  

...

registerUrl : String
registerUrl =
    api ++ "users"


The API route for POSTing new users is http://localhost:3001/users so we'll create registerUrl to store this.

-- Encode user to construct POST request body (for Register and Log In)

userEncoder : Model -> Encode.Value
userEncoder model = 
    Encode.object 
        [ ("username", Encode.string model.username)
        , ("password", Encode.string model.password) 
        ]          


The API expects the request body for registration and login to be a JavaScript object in string format that looks like this: { username: "string", password: "string" }

An Elm record is not the same as a JavaScript object so we need to encode the applicable properties of our model before we can send them with the HTTP request. The userEncoder function utilizes Json.Encode.object to take the model and return the encoded value.

-- POST register / login request

authUser : Model -> String -> Task Http.Error String
authUser model apiUrl =
    { verb = "POST"
    , headers = [ ("Content-Type", "application/json") ]
    , url = apiUrl
    , body = Http.string <| Encode.encode 0 <| userEncoder model
    }
    |> Http.send Http.defaultSettings
    |> Http.fromJson tokenDecoder 


We'll use a fully specified HTTP request this time as opposed to the simple getString used for the quote in the previous step. The same settings can be used for both register and login with the exception of the API route which we'll pass in an argument.

We'll call this effect function authUser (for "authenticate a user"). The type says "authUser takes model as an argument and a string as an argument and returns a task that fails with an error or succeeds with a string".

Let's take a closer look at these lines:

...
    , body = Http.string <| Encode.encode 0 <| userEncoder model
    }
    |> Http.send Http.defaultSettings
    |> Http.fromJson tokenDecoder


<|
and |> are aliases for function application to reduce parentheses. <| is backward function application.

body = Http.string <| Encode.encode 0 <| userEncoder model takes the results of the last function and passes it as the last argument to the function on its left. Written with parentheses, the equivalent would be: body = Http.string (Encode.encode 0 (userEncoder model))

We'll run the userEncoder function to encode the request body. Then Json.Encode.encode converts the resulting value to a prettified string with no (0) indentation. Finally, we provide the resulting string as the request body with Http.String.

Next, we have forward function application performed with |> to send the HTTP request with default settings and then take the JSON result and decode it with a tokenDecoder function that we'll create in a moment.

We now have our authUser effect so we need to create an authUserCmd command. This should look familiar from fetching quotes earlier. We're also passing the API route as an argument. We'll create the AuthError and GetTokenSuccess messages shortly.

authUserCmd : Model -> String -> Cmd Msg    
authUserCmd model apiUrl = 
    Task.perform AuthError GetTokenSuccess <| authUser model apiUrl


Because authUser takes arguments, we'll use <| to tell the app that the model and apiUrl belong to authUser and not to Task.perform.

We'll also define the tokenDecoder function that ensures we can work with the response from the HTTP request:

-- Decode POST response to get token

tokenDecoder : Decoder String
tokenDecoder =
    "id_token" := Decode.string


When registering or logging in a user, the response from the API is JSON-shaped like this:

{
    "id_token": "someJWTTokenString"
}


Recall that : means "has type" in Elm. We'll take the id_token and extract its contents as a string that will be returned on success.

Now we will do something with the result and set up a way for our UI to interact with the model:

-- Messages

type Msg 
    ...
    | AuthError Http.Error
    | SetUsername String
    | SetPassword String
    | ClickRegisterUser
    | GetTokenSuccess String

-- Update

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        ...  

        AuthError error ->
            ( { model | errorMsg = (toString error) }, Cmd.none )  

        SetUsername username ->
            ( { model | username = username }, Cmd.none )

        SetPassword password ->
            ( { model | password = password }, Cmd.none )

        ClickRegisterUser ->
            ( model, authUserCmd model registerUrl )

        GetTokenSuccess newToken ->
            ( { model | token = newToken, errorMsg = "" } |> Debug.log "got new token", Cmd.none )


We want to display authentication errors to the user. Unlike the HttpError message we implemented earlier, AuthError won't discard its argument. The type of the error argument is Http.Error. As you can see from the docs, this is a union type that could be a few different errors. For the sake of simplicity, we're going to convert the error to a string and update the model's errorMsg with that string. A good exercise later would be to translate the different errors to more user-friendly messaging.

The SetUsername and SetPassword messages are for sending form field values to update the model. ClickRegisterUser is the onClick for our "Register" button. It runs the authUserCmd command we just created and passes the model and the API route for new user creation.

GetTokenSuccess is the success function for the authUser task. Its argument is the token string. We'll update our model with the token so we can use it to request protected quotes later. This is a good place to verify that everything is working as expected, so let's log the updated model to the browser console using the |> forward function application alias and a Debug.log: { model | token = newToken, errorMsg = "" } |> Debug.log "got new token".

{-
    VIEW
    * Hide sections of view depending on authenticaton state of model
    * Get a quote
    * Register
-}

view : Model -> Html Msg
view model =
    let 
        -- Is the user logged in?
        loggedIn : Bool
        loggedIn =
            if String.length model.token > 0 then True else False 

        -- If the user is logged in, show a greeting; if logged out, show the login/register form
        authBoxView =
            let
                -- If there is an error on authentication, show the error alert
                showError : String
                showError = 
                    if String.isEmpty model.errorMsg then "hidden" else ""  

                -- Greet a logged in user by username
                greeting : String
                greeting =
                    "Hello, " ++ model.username ++ "!"

            in        
                if loggedIn then
                    div [id "greeting" ][
                        h3 [ class "text-center" ] [ text greeting ]
                        , p [ class "text-center" ] [ text "You have super-secret access to protected quotes." ]  
                    ] 
                else
                    div [ id "form" ] [
                        h2 [ class "text-center" ] [ text "Log In or Register" ]
                        , p [ class "help-block" ] [ text "If you already have an account, please Log In. Otherwise, enter your desired username and password and Register." ]
                        , div [ class showError ] [
                            div [ class "alert alert-danger" ] [ text model.errorMsg ]
                        ]
                        , div [ class "form-group row" ] [
                            div [ class "col-md-offset-2 col-md-8" ] [
                                label [ for "username" ] [ text "Username:" ]
                                , input [ id "username", type' "text", class "form-control", Html.Attributes.value model.username, onInput SetUsername ] []
                            ]    
                        ]
                        , div [ class "form-group row" ] [
                            div [ class "col-md-offset-2 col-md-8" ] [
                                label [ for "password" ] [ text "Password:" ]
                                , input [ id "password", type' "password", class "form-control", Html.Attributes.value model.password, onInput SetPassword ] []
                            ]    
                        ]
                        , div [ class "text-center" ] [
                            button [ class "btn btn-link", onClick ClickRegisterUser ] [ text "Register" ]
                        ] 
                    ]

    in
        div [ class "container" ] [
            h2 [ class "text-center" ] [ text "Chuck Norris Quotes" ]
            , p [ class "text-center" ] [
                button [ class "btn btn-success", onClick GetQuote ] [ text "Grab a quote!" ]
            ]
            -- Blockquote with quote
            , blockquote [] [ 
                p [] [text model.quote] 
            ]
            , div [ class "jumbotron text-left" ] [
                -- Login/Register form or user greeting
                authBoxView
            ]
        ]


There's a lot of new stuff in the view but it's mostly from HTML. We'll start with some logic to hide the form once the user is authenticated; we want to show a greeting in this case.

Remember that view is a function. This means we can do things like create scoped variables with let expressions to conditionally render parts of the view.

For the sake of simplicity, we'll check if the model's token string has a length to determine if the user is logged in. In a real-world application, token verification might be performed in a route callback to ensure proper UI access. For our Chuck Norris Quoter, the token is needed to get protected quotes so all the loggedIn variable does is show the form vs. a simple greeting.

We'll then create the authBoxView. This contains the form and greeting and executes either depending on the value of loggedIn. We'll also display the authentication error if there is one.

If the user is logged in, we'll greet them by their username and inform them that they have super-secret access to protected quotes.

If the user is not logged in, we'll display the Log In / Register form. We can use the same form to do both because they share the same request body. Right now though, we only have the functionality for Register prepared.

After the heading, instructional copy, and conditional error alert, we need the username and password form fields. We can supply various attributes:

input [ id "username", type' "text", class "form-control", Html.Attributes.value model.username, onInput SetUsername ] []
...
input [ id "password", type' "password", class "form-control", Html.Attributes.value model.password, onInput SetPassword ] []


There are a few things that may stand out here: type' has a single quote after it because type is a reserved keyword. Appending the quote has origins in the usage of the prime symbol in mathematics#Use_in_mathematics.2C_statistics.2C_and_science). Html.Attributes.value is fully qualified because value alone is ambiguous in context because the compiler could confuse it with Json.Decode.value. onInput is a custom event handler that gets values from triggered events. When these events are fired we want to update the username or password in the model.

After our form fields, we'll include a "Register" button with onClick ClickRegisterUser. We'll use Bootstrap's CSS to style this button like a link since it will live next to a Log In button later.

Finally, we'll use our authBoxView variable in the main view. We'll place it below our Chuck Norris quote in a jumbotron.

Now we can register new users in our app. When successfully registered, the user will receive a token and be authenticated. The view will then update to show the greeting message. Try it out in the browser. You should also try to trigger an error message!

Logging In and Logging Out

Now that users can register, they need to be able to log in with existing accounts.

elm quote

We also need the ability to log out.

elm quote

The full Main.elm code with login and logout implemented will look like this:

Main.elm - Logging In and Logging Out

Login works like Register (and uses the same request body), so creating its functionality should be straightforward.

-- API request URLs

... 

loginUrl : String
loginUrl =
    api ++ "sessions/create" 


We'll add the login API route, which is http://localhost:3001/sessions/create.

We already have the authUser effect and authUserCmd command, so all we need to do is create a way for login to interact with the UI. We'll also create a logout.

-- Messages

type Msg 
    ...
    | ClickLogIn
     ...
    | LogOut

-- Update

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
         ...

        ClickLogIn ->
            ( model, authUserCmd model loginUrl ) 

        ...

        LogOut ->
            ( { model | username = "", password = "", protectedQuote = "", token = "", errorMsg = "" }, Cmd.none )


ClickLogIn
runs the authUserCmd command with the appropriate arguments. LogOut resets authentication-related data in the model record to empty strings.

...

if loggedIn then
    div [id "greeting" ][
        h3 [ class "text-center" ] [ text greeting ]
        , p [ class "text-center" ] [ text "You have super-secret access to protected quotes." ]
        , p [ class "text-center" ] [
            button [ class "btn btn-danger", onClick LogOut ] [ text "Log Out" ]
        ]   
    ]

...                    

, div [ class "text-center" ] [
    button [ class "btn btn-primary", onClick ClickLogIn ] [ text "Log In" ]
    , button [ class "btn btn-link", onClick ClickRegisterUser ] [ text "Register" ]
]

...


There are minimal updates to the view. We'll add a logout button in the greeting message in authBoxView. Then in the form, we'll insert the login button before the register button.

Registered users can now log in and log out. Our application is really coming together!

Note: A nice enhancement might be to show different forms for logging in and registering. Maybe the user should be asked to confirm their password when registering?

Getting Protected Quotes

It's time to make authorized requests to the API to get protected quotes for authenticated users. Our logged-out state will look like this:

elm quote

If a user is logged in, they'll be able to click a button to make API requests to get protected quotes:

elm quote

Here's the completed Main.elm code for this step:

Main.elm - Getting Protected Quotes

We're going to need a new package:

import Http.Decorators


We'll go into more detail regarding why this is needed when we make the GET request for the protected quotes.

{- 
    MODEL
    * Model type 
    * Initialize model with empty values
    * Initialize with a random quote
-}

type alias Model =
    { username : String
    , password : String
    , token : String
    , quote : String
    , protectedQuote : String 
    , errorMsg : String
    }

init : (Model, Cmd Msg)
init =
    ( Model "" "" "" "" "" "", fetchRandomQuoteCmd )


We're adding a protectedQuote property to the Model type alias. This will be a string. We'll add another pair of double quotes "" to the init tuple to initialize our app with an empty string for the protected quote.

-- API request URLs

...  

protectedQuoteUrl : String
protectedQuoteUrl = 
    api ++ "api/protected/random-quote"


Add the API route for the protectedQuoteUrl: http://localhost:3001/api/protected/random-quote.

-- GET request for random protected quote (authenticated)

fetchProtectedQuote : Model -> Task Http.Error String
fetchProtectedQuote model = 
    { verb = "GET"
    , headers = [ ("Authorization", "Bearer " ++ model.token) ]
    , url = protectedQuoteUrl
    , body = Http.empty
    }
    |> Http.send Http.defaultSettings  
    |> Http.Decorators.interpretStatus -- decorates Http.send result so error type is Http.Error instead of RawError
    |> Task.map responseText


We'll create the HTTP request to GET the protected quote. The type for this request is "fetchProtectedQuote takes model as an argument and returns a task that fails with an error or succeeds with a string". This time, we need to define an Authorization header. The value of this header is Bearer plus the user's token string. We then Http.send the request with default settings like we did in authUser.

We're going to step through a typing challenge now. If we try to compile before adding |> Http.Decorators.interpretStatus we'll receive a type mismatch error. This API route returns a string instead of JSON like our authUserPOST request. Elm infers that the type should be Model -> Task Http.RawError String but we've written Http.Error instead. We didn't have this problem getting the unprotected quote because we used Http.getString. We can't use getString here because we need to pass a custom header. And because the response is not JSON, we can't use Http.fromJson (which takes a RawError and returns an Error). We want the error type to be Http.Error because we want to use our pre-existing HttpError function in our command and HttpError expects Error, not RawError.

We can resolve this by using Http.Decorators.interpretStatus. This decorates the Http.send result so the error type is Error instead of RawError. Now all our types match again!

Now we need to handle the response. It's a string and not JSON so we won't be decoding it the way we did with authUser. We'll map the response to transform it with a responseText function that we'll define in a moment.

Before that, we'll define the command:

fetchProtectedQuoteCmd : Model -> Cmd Msg
fetchProtectedQuoteCmd model = 
    Task.perform HttpError FetchProtectedQuoteSuccess <| fetchProtectedQuote model 


We're quite familiar with these commands now and there are no surprises here. We'll use the same HttpError that we used for fetching the unprotected quote and we'll create the FetchProtectedQuoteSuccess message shortly.

-- Extract GET plain text response to get protected quote    

responseText : Http.Response -> String
responseText response = 
    case response.value of 
        Http.Text t ->
            t 
        _ ->
            ""


Since we're not using getString we need to extract the plain text from the result of our HTTP request. Type annotation says, "responseText takes a response and returns a string" which is our new protected quote. The type of the response is a record that contains, among other things, a value. If the response value is a text string, we'll return the text. In any other case, we'll return an empty string.

-- Messages

type Msg 
    ...
    | GetProtectedQuote
    | FetchProtectedQuoteSuccess String
    ...

-- Update

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        ...

        GetProtectedQuote ->
            ( model, fetchProtectedQuoteCmd model )

        FetchProtectedQuoteSuccess newPQuote ->
            ( { model | protectedQuote = newPQuote }, Cmd.none )  

        ...


There are no new concepts in the implementation of the messages here. GetProtectedQuote returns the command and FetchProtectedQuoteSuccess updates the model.

-- If user is logged in, show button and quote; if logged out, show a message instructing them to log in
protectedQuoteView = 
    let
        -- If no protected quote, apply a class of "hidden"
        hideIfNoProtectedQuote : String
        hideIfNoProtectedQuote = 
            if String.isEmpty model.protectedQuote then "hidden" else ""

    in        
        if loggedIn then
            div [] [
                p [ class "text-center" ] [
                    button [ class "btn btn-info", onClick GetProtectedQuote ] [ text "Grab a protected quote!" ]
                ]
                -- Blockquote with protected quote: only show if a protectedQuote is present in model
                , blockquote [ class hideIfNoProtectedQuote ] [ 
                    p [] [text model.protectedQuote] 
                ]
            ]    
        else
            p [ class "text-center" ] [ text "Please log in or register to see protected quotes." ]

...

, div [ class "jumbotron text-left" ] [
    -- Login/Register form or user greeting
    authBoxView 
], div [] [
    h2 [ class "text-center" ] [ text "Protected Chuck Norris Quotes" ]
    -- Protected quotes
    , protectedQuoteView
]


In the let, we'll add a protectedQuoteView under the authBoxView variable. We'll use a variable called hideIfNoProtectedQuote with an expression to output a hidden class to the blockquote. This will prevent the element from being shown if there is no quote.

We'll represent logged in and logged out states using the loggedIn variable we declared earlier. When logged in we'll show a button to GetProtectedQuote and the quote. When logged out we'll show a paragraph with copy telling the user to log in or register.

At the bottom of our view function, we'll add a div with a heading and our protectedQuoteView.

Check it out in the browser--our app is almost finished!

Persisting Logins With Local Storage

We have the primary functionality done now. Our app gets quotes, allows registration, login, and gets authorized quotes. The last thing we'll do is persist logins.

We don't want our logged-in users to lose their data if they refresh their browser or leave and come back. To do this we'll implement localStorage with Elm using JavaScript interop. This is a way to take advantage of features of JS in Elm code. After all, Elm compiles to JavaScript so it only makes sense that we would be able to do this.

When we're done, our completed Main.elm will look like this:

Main.elm - Persisting Logins with Local Storage

The first things you may notice are changes to our Main module and program:

port module Main exposing (..)

...

main : Program (Maybe Model)
main = 
    Html.programWithFlags
        { init = init 
        , update = update
        , subscriptions = \_ -> Sub.none
        , view = view
        }


We need to switch from program to programWithFlags. The type therefore changes from Program Never to Program (Maybe Model). This means we might have a model provided at initialization. If the model is already in local storage it will be available. If we don't have anything stored when we arrive we'll initialize without it.

So where does this initial model come from? We need to write a little bit of JavaScript in our index.html:

...    
    var storedState = localStorage.getItem('model');
    var startingState = storedState ? JSON.parse(storedState) : null;
    var elmApp = Elm.Main.fullscreen(startingState);

    elmApp.ports.setStorage.subscribe(function(state) {
        localStorage.setItem('model', JSON.stringify(state));
    });

    elmApp.ports.removeStorage.subscribe(function() {
        localStorage.removeItem('model');
    });
...


There is no Elm here. We will use JavaScript to check local storage for previously saved model data. Then we'll establish the startingState in a ternary that checks storedState for model data. If data is found we'll JSON.parse it and pass it to our Elm app. If there is no model yet, we'll pass null.

Then we need to set up ports so we can use features of localStorage in our Elm code. We'll call one port setStorage and subscribe to it so we can do something with messages that come through the port. When state data is sent we'll use the setItem method to set a model and save the stringified data to localStorage. The removeStorage port will remove the model item from localStorage. We'll use this when logging out.

Now we'll go back to Main.elm:

-- Helper to update model and set localStorage with the updated model

setStorageHelper : Model -> ( Model, Cmd Msg )
setStorageHelper model = 
    ( model, setStorage model )


We need a helper function of a specific type to save the model to local storage in multiple places in our update. Because the update type always expects a tuple with a model and command message returned, we need our helper to take the model as an argument and return the same type of tuple. We'll understand how this fits in a little more in a moment.

-- Messages

...

-- Ports

port setStorage : Model -> Cmd msg  
port removeStorage : Model -> Cmd msg

-- Update

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        ...

        GetTokenSuccess newToken ->
            setStorageHelper { model | token = newToken, password = "", errorMsg = "" }

        ...

        FetchProtectedQuoteSuccess newPQuote ->
            setStorageHelper { model | protectedQuote = newPQuote }

        LogOut ->
            ( { model | username = "", password = "", protectedQuote = "", token = "", errorMsg = "" }, removeStorage model )    


We need to define the type annotation for our setStorage and removeStorage ports. They'll take a model and return a command. The lowercase msg is significant because this is an effect manager and its type is actually Cmd a. It does not send messages back to the program. Keep in mind that using Cmd Msg here will result in a compiler error.

Finally, we're going to replace some of our update returns with the setStorageHelper and use the removeStorage command for logging out. This helper will return the tuple that our update function expects from all branches so we won't have to worry about type mismatches.

We will call setStorageHelper and pass the model updates that we want to propagate to the app and save to local storage. We're saving the model to storage when the user is successfully granted a token and when they get a protected quote. GetTokenSuccess will now also clear the password and error message; there is no reason to save these to storage. On logout, we'll remove the localStoragemodel item.

Now when we authenticate, local storage will keep our data so when we refresh or come back later, we won't lose our login state.

If everything compiles and works as expected, we're done with our basic Chuck Norris Quoter application!

Aside: Using Auth0 With Elm

Auth0 provides registration, login, and authentication with JSON Web Tokens. We can add Auth0 authentication to an Elm app by setting up a couple of additional Elm modules and importing them for use in an app's Main.elm file. Marcus Griep has a great Elm Workshop with 0.17 available on GitHub that includes an Auth0.elm module and an Authentication.elm module.

These modules are great resources to help you implement Auth0 with Elm. We will introduce and build on them in a new app to demonstrate how to use Auth0's lock widget with JSON Web Tokens. The modules will be imported into our Main.elm file and we'll use JS interop to interface with Auth0's lock component.

elm with Auth0

Auth0 Lock Interop and Local Storage

In our index.html file, we'll use JavaScript to implement ports that instantiate the Auth0 lock and log out:

<!-- index.html -->

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
        <title>Elm with Auth0</title>
        <script src="Main.js"></script>
        <script src="Auth0.js"></script>
        <script src="Authentication.js"></script>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
    </head>

    <body>
    </body>

    <script src="//cdn.auth0.com/js/lock-9.1.min.js"></script>
    <script>
        var lock = new Auth0Lock(<YOUR_CLIENT_ID>, <YOUR_CLIENT_DOMAIN>);
        var storedProfile = localStorage.getItem('profile');
        var storedToken = localStorage.getItem('token');
        var authData = storedProfile && storedToken ? { profile: JSON.parse(storedProfile), token: storedToken } : null;
        var elmApp = Elm.Main.fullscreen(authData);

        elmApp.ports.auth0showLock.subscribe(function(opts) {
            opts.connections = ['Username-Password-Authentication'];

            lock.show(opts, function(err, profile, token) {
                var result = { err: null, ok: null };

                if (!err) {
                    result.ok = { profile: profile, token: token };
                    localStorage.setItem('profile', JSON.stringify(profile));
                    localStorage.setItem('token', token);
                } else {
                    result.err = err.details;

                    // Ensure that optional fields are on the object
                    result.err.name = result.err.name ? result.err.name : null;
                    result.err.code = result.err.code ? result.err.code : null;
                    result.err.statusCode = result.err.statusCode ? result.err.statusCode : null;
                }
                elmApp.ports.auth0authResult.send(result);
            });
        });

        elmApp.ports.auth0logout.subscribe(function(opts) {
            localStorage.removeItem('profile');
            localStorage.removeItem('token');
        });
    </script>
</html>


First, we need to add our compiled Elm files as well as the Auth0 lock widget JavaScript file:

<script src="Main.js"></script>
<script src="Auth0.js"></script>
<script src="Authentication.js"></script>

...

<script src="//cdn.auth0.com/js/lock-9.1.min.js"></script>


Then we'll instantiate a lock instance:

var lock = new Auth0Lock(<YOUR_CLIENT_ID>, <YOUR_CLIENT_DOMAIN>);


Next, we'll set up the JS to instantiate the Elm application with flags and ports to interoperate with the lock widget and localStorage. We'll request a stored profile and token and if available, we'll recreate an object that matches the record we'll use in the Auth0.elm module for a LoggedInUser. Then we'll create ports to show the lock widget and perform logout, adding and removing items from local storage accordingly.

Auth0 Module

Next, we'll build our Auth0.elm module:

-- Auth0.elm

module Auth0
    exposing
        ( AuthenticationState(..)
        , AuthenticationError
        , AuthenticationResult
        , RawAuthenticationResult
        , Options
        , defaultOpts
        , LoggedInUser
        , UserProfile
        , Token
        , mapResult
        )


type alias LoggedInUser =
    { profile : UserProfile
    , token : Token
    }


type AuthenticationState
    = LoggedOut
    | LoggedIn LoggedInUser


type alias Options =
    {}


type alias UserProfile =
    { email : String
    , email_verified : Bool
    , name : String
    , nickname : String
    , picture : String
    , user_id : String
    }


type alias Token =
    String


type alias AuthenticationError =
    { name : Maybe String
    , code : Maybe String
    , description : String
    , statusCode : Maybe Int
    }


type alias AuthenticationResult =
    Result AuthenticationError LoggedInUser


type alias RawAuthenticationResult =
    { err : Maybe AuthenticationError
    , ok : Maybe LoggedInUser
    }


mapResult : RawAuthenticationResult -> AuthenticationResult
mapResult result =
    case ( result.err, result.ok ) of
        ( Just msg, _ ) ->
            Err msg

        ( Nothing, Nothing ) ->
            Err { name = Nothing, code = Nothing, statusCode = Nothing, description = "No information was received from the authentication provider" }

        ( Nothing, Just user ) ->
            Ok user


defaultOpts : Options
defaultOpts =
    {}


This module handles responses from Auth0 authentication requests. It defines authentication state and types for the user's profile, JWT, authentication errors, and results.

Authentication Module

Now we need a way for the Auth0 module to interface with the authentication logic for our Elm app. We'll set this up in an Authentication.elm file:

-- Authentication.elm

module Authentication
    exposing
        ( Msg(..)
        , Model
        , init
        , update
        , handleAuthResult
        , tryGetUserProfile
        , isLoggedIn
        )

import Auth0


type alias Model =
    { state : Auth0.AuthenticationState
    , lastError : Maybe Auth0.AuthenticationError
    , showLock : Auth0.Options -> Cmd Msg
    , logOut : () -> Cmd Msg
    }


init : (Auth0.Options -> Cmd Msg) -> (() -> Cmd Msg) -> Maybe Auth0.LoggedInUser -> Model
init showLock logOut initialData =
    { state =
        case initialData of
            Just user ->
                Auth0.LoggedIn user

            Nothing ->
                Auth0.LoggedOut
    , lastError = Nothing
    , showLock = showLock
    , logOut = logOut
    }


type Msg
    = AuthenticationResult Auth0.AuthenticationResult
    | ShowLogIn
    | LogOut


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        AuthenticationResult result ->
            let
                ( newState, error ) =
                    case result of
                        Ok user ->
                            ( Auth0.LoggedIn user, Nothing )

                        Err err ->
                            ( model.state, Just err )
            in
                ( { model | state = newState, lastError = error }, Cmd.none )

        ShowLogIn ->
            ( model, model.showLock Auth0.defaultOpts )

        LogOut ->
            ( { model | state = Auth0.LoggedOut }, model.logOut () )


handleAuthResult : Auth0.RawAuthenticationResult -> Msg
handleAuthResult =
    Auth0.mapResult >> AuthenticationResult


tryGetUserProfile : Model -> Maybe Auth0.UserProfile
tryGetUserProfile model =
    case model.state of
        Auth0.LoggedIn user ->
            Just user.profile

        Auth0.LoggedOut ->
            Nothing


isLoggedIn : Model -> Bool
isLoggedIn model =
    case model.state of
        Auth0.LoggedIn _ ->
            True

        Auth0.LoggedOut ->
            False


We need to import the Auth0 module so we can reference it. The model provides a way for our Main Elm module to send data into the Authentication module. We'll do this by passing arguments to Authentication's init function from the Main module. We'll establish a Msg union type and then in our update function, we can handle authentication results, show the lock widget, and log out.

Implementing Auth0 in Main.elm

Our modules are ready to use. We'll import them in our Main.elm program file and create our model, update, and view:

-- Main.elm

port module Main exposing (..)

import Html exposing (..)
import Html.App as Html
import Html.Events exposing (..)
import Html.Attributes exposing (..)
import Auth0
import Authentication


main : Program (Maybe Auth0.LoggedInUser)
main =
    Html.programWithFlags
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }


type alias Model =
    { authModel : Authentication.Model
    }


-- Init

init : Maybe Auth0.LoggedInUser -> ( Model, Cmd Msg )
init initialUser =
    ( Model (Authentication.init auth0showLock auth0logout initialUser), Cmd.none )


-- Messages

type Msg
    = AuthenticationMsg Authentication.Msg


-- Ports

port auth0showLock : Auth0.Options -> Cmd msg
port auth0authResult : (Auth0.RawAuthenticationResult -> msg) -> Sub msg
port auth0logout : () -> Cmd msg


-- Update

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        AuthenticationMsg authMsg ->
            let
                ( authModel, cmd ) =
                    Authentication.update authMsg model.authModel
            in
                ( { model | authModel = authModel }, Cmd.map AuthenticationMsg cmd )



-- Subscriptions

subscriptions : a -> Sub Msg
subscriptions model =
    auth0authResult (Authentication.handleAuthResult >> AuthenticationMsg)


-- View

view : Model -> Html Msg
view model =
    div [ class "container" ]
        [ div [ class "jumbotron text-center" ]
            [ div []
                (case Authentication.tryGetUserProfile model.authModel of
                    Nothing ->
                        [ p [] [ text "Please log in" ] ]

                    Just user ->
                        [ p [] [ img [ src user.picture ] [] ]
                        , p [] [ text ("Hello, " ++ user.name ++ "!") ]
                        ]
                )
            , p []
                [ button
                    [ class "btn btn-primary"
                    , onClick
                        (AuthenticationMsg
                            (if Authentication.isLoggedIn model.authModel then
                                Authentication.LogOut
                                else
                                Authentication.ShowLogIn
                            )
                        )
                    ]
                    [ text
                        (if Authentication.isLoggedIn model.authModel then
                            "Logout"
                            else
                            "Login"
                        )
                    ]
                ]
            ]
        ]


We'll use programWithFlags because we want to check for an existing user and token in local storage upon initialization. The model contains the Authentication model record we set up earlier.

In the init function, we will initAuthentication and pass in arguments for the ports that show the lock and log out. We'll also pass the initial user from local storage if available (recall that we set this up in index.html to mirror the LoggedInUser type from the Auth0 module).

We need to subscribe to the auth0authResult port to listen for external input from Auth0 lock logins.

Aside: >> represents function chaining.

Finally, the view displays a message and button to open the lock widget if there is no authentication data in storage, and a greeting with the user's avatar along with a logout button if there is.

Elm: Now and Future

We made a simple app but covered a lot of ground with Elm's architecture, syntax, and implementation of features you'll likely come across in web application development. Authenticating with JWT was straightforward and packages and JS interop offer a lot of extensibility.

Elm began in 2012 as Evan Czaplicki's Harvard senior thesis and it's still a newcomer in the landscape of front-end languages. That isn't stopping production use though: NoRedInk has been compiling Elm to production for almost a year (Introduction to Elm - Richard Feldman) with no runtime exceptions and Evan Czaplicki is deploying Elm to production at Prezi. Elm's compiler offers a lot of test coverage "free of charge" by thoroughly checking all logic and branches. In addition, the Elm Architecture of model-view-update inspired Redux.

Elm also has an active community. I particularly found the elmlang Slack to be a great place to learn about Elm and chat with knowledgeable developers who are happy to help with any questions.

There are a lot of exciting things about Elm and I'm looking forward to seeing how it continues to evolve. Static typing, functional, reactive programming, and friendly documentation and compiler messaging make it a clean and speedy coding experience. There's also a peace of mind that Elm provides--the fear of production runtime errors is a thing of the past. Once Elm compiles, it just works, and that is something that no other JavaScript SPA frameworks can offer.

API Elm (email client) app authentication Requests Strings Web application Data Types Threading Data (computing)

Published at DZone with permission of Kim Maida, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How to Build a Pokedex React App with a Slash GraphQL Backend
  • Exploring API Headers
  • Allow Users to Track Fitness Status in Your App
  • Turn Your App into a Handy Health Assistant

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!