In the exercise from last week, we added support for storing our API keys in localStorage. However, those API keys expire under various circumstances. We need to detect this and handle it appropriately. Let's get started.

Project

I'm starting from the before_episode_024.3 tag. Right now our API keys last for some period of time. We can see what that is in the elixir backend:

cd time_tracker_backend
vim config/config.exs
config :guardian, Guardian,
  allowed_algos: ["ES512"], # optional
  verify_module: Guardian.JWT,  # optional
  verify_issuer: true, # optional
  issuer: "TimeTrackerBackend",
  ttl: { 30, :days }, # <--- there's the TTL, this is what determines it.
  secret_key: fn ->
    JOSE.JWK.from_pem_file("ec-secp521r1.pem")
  end,
  serializer: TimeTrackerBackend.GuardianSerializer

We'll change this to something unreasonable - I think 15 seconds sounds nice and unreasonable.

  #...
  ttl: { 15, :seconds },
  #...

Now we can restart the backend and it will pick this up - new API keys we're provided will expire very rapidly. This doesn't do anything for existing API keys so we should remove it from localStorage and refresh the page:

localStorage.clear();

If we log in now and revisit the page, we'll actually continue to see a list of users, but if we look at the console it will start returning a 401 unauthorized after 15 seconds. Now if we refresh the page, we think we're logged in but we can't access any of the resources that require authorization. That's no good! Let's look at how we would fix this.

First, let's open up src/Util.elm and add a logging statement to be sure we know what we're getting when we try to fetch the users without a valid apiKey:

-- ...
cmdsForModelRoute : Model -> List (Cmd Msg)
cmdsForModelRoute model =
    case model.route of
        Just Users ->
            [ API.fetchUsers model
                (\x ->
                    let
                        _ =
                            Debug.log "FetchUsers failed" x
                    in
                        NoOp
                )
              <|
                UserMsg'
                    << GotUsers
            ]
        -- ...

Now when we refresh the page, we see we got a BadResponse 401 "Unauthorized". We can pattern match on this and, in the event of a 401, clear the API key. First, let's add a ClearApiKey Msg at the top level:

vim src/Msg.elm
type Msg
    -- ...
    | ClearApiKey
    -- ...

And we'll handle it in the Update by clearing the apiKey from the model and explicitly redirecting to login:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        -- ...
        ClearApiKey ->
            { model | apiKey = Nothing } ! [ Navigation.newUrl <| Route.urlFor Login ]
        -- ...

Then we'll make our API call do that for us explicitly in the event of a BadResponse 401 _:

vim src/Util.elm
module Util exposing (cmdsForModelRoute, MaterialTableHeader)
-- ...
import Http exposing (Error(BadResponse))
cmdsForModelRoute : Model -> List (Cmd Msg)
cmdsForModelRoute model =
    case model.route of
        Just Users ->
            [ API.fetchUsers model
                (\x ->
                    case x of
                        BadResponse 401 _ ->
                            ClearApiKey

                        _ ->
                            NoOp
                )
              <|
                UserMsg'
                    << GotUsers
            ]
        -- ...

That's sufficient to handle this for us. From here, we can either modify the API functions to do this for any requests, or we could make a utility that allowed us to wrap our response tagger in this case statement trivially.

Summary

In today's episode, we saw how to handle HTTP 401 requests to force the user to log back in. I hope you enjoyed it. See you soon!

Resources