LiveBook OAuth Token Management

Hello!
I am working on a LiveBook project to wrangle some data.
I have a token management module adapted from StackOverflow:

# TOKEN MANAGER
defmodule TokenHolder do
  
  def start_link() do
    Agent.start_link(fn ->
      {token, tok_time} = get_token() 
      {token, tok_time}
    end, name: __MODULE__)
  end

  # refresh the token if older that one hour
  @max_age 60*60*1000000

  def token do
    Agent.get_and_update(__MODULE__, fn {token,retrieved} ->
      now = :os.timestamp
      if(:timer.now_diff(now, retrieved) < @max_age) do
        # return old token and old state
        {token,retrieved}
      else
        # retrieve new token, return it and return changed state
        get_token()
      end
    end)
  end

  defp get_token() do
    token_url = "https://login.solutionsbytext.com/connect/token"
    client_id = "ABC123"
    client_secret = System.get_env("LB_CLIENT_SECRET")
    token_data = [
      client_id: client_id,
      client_secret: client_secret,
      grant_type: "client_credentials"
    ]

    resp = Req.post!(token_url, form: token_data)
    # IO.inspect(resp, label: "TOKEN RESP")

    token = resp.body["access_token"]
    
    {token,:os.timestamp}
  end
end

I run this:

I can run token = TokenHolder.token in a cell and get a token.

I have another module:

defmodule GetSubscriberStatus do
  
  def new(phone) do
      brand_id = "n4t4333"
      token = TokenHolder.token
...

It fails with:

Evaluation process terminated - an exception was raised:
** (FunctionClauseError) no function clause matching in anonymous fn/1 in TokenHolder.token/0

I’m confused about why I can get a token outside this module but not within it–and maybe just confused in general. I obviously don’t understand the “matching function clause” error. The {token, retrieved} bit should be there after {:ok,_} = TokenHolder.start_link(), shouldn’t it?

I’m picturing it as each time I call token = TokenHolder.token in GetSubscriberStatus, TokenHolder checks that my token is valid. If there is no token, or the existing token is expired, it fetches a new one and returns that. In this way, I should be able to gracefully handle expired tokens when needed.

I tried before to run token = TokenHolder.token in its own cell, then passing token when I call GetSubscriberStatus, like this:

|> Flow.map(&GetSubscriberStatus.new(&1, token))

but it failed with an expired token.

Please help me understand.
If there is a better approach in LiveBook, I’d happily adopt it.
Thank you in advance!

The callback given to Agent.get_and_update needs to return {value_to_return, new_state}, so you rather want something like this:

Agent.get_and_update(__MODULE__, fn {token,retrieved} ->
  now = :os.timestamp

  {token, retrieved} =
    if(:timer.now_diff(now, retrieved) < @max_age) do
      # return old token and old state
      {token,retrieved}
    else
      # retrieve new token, return it and return changed state
      get_token()
    end

  {token, {token, retrieved}}
end)

(or {{token, retrieved}, {token, retrieved}}, if you actually want to return both)

4 Likes

Thank you so much, @jonatanklosko!
I apologize for asking an “Elixir” question in the Livebook Forum!
I realized after your response.
Again, I sincerely appreciate your guidance.
This got things working as expected.
I apologize for not being more diligent in the first place.
All the best,
Jarvis

2 Likes

@jarvism definitely no worries : )

1 Like