Joken returning :ok for expired Token validation? (solved)

I am trying to figure out how to use Joken from the limited documentation on Hex. The only other references I can find seem very out of date.

I am able to create JWT’s that are read correctly by other systems like online JWT inspectors.

However, my understanding is running Joken.verify_and_validate() should return :error or something of that nature on expired Tokens.

I can assess the outputted claims map manually myself, but isn’t this the point of the function?

(1) Create claims map & token:

    def create_token(user_name, duration_hrs) do 

        # start with empty map
        token_config_map = %{} 

            # userID:
            |> Joken.Config.add_claim("user_name", fn -> user_name end, &(&1 == user_name))

            # issuer:
            |> Joken.Config.add_claim("iss", fn -> "The Issuer" end, &(&1 == "The Issuer"))

            # expiry: 
            |> Joken.Config.add_claim("exp", fn -> Joken.CurrentTime.OS.current_time() + (duration_hrs * 60 * 60) end, &(&1 < Joken.CurrentTime.OS.current_time() ) ) 

        # generate claims
        token_claims = Joken.generate_claims(token_config_map)

        # return the unwrapped claims or nil if fails
        token_claims = case token_claims do
            {:ok, claims}-> claims
            _ -> nil
        end

        #create signer or get if stored
        signer = Joken.Signer.create("RS256", %{"pem" => My.Token.private_key}) 

        # make the token
        result = Joken.encode_and_sign(token_claims, signer)

        case (result) do
            {:ok, token, claims} -> token
            _-> nil
        end

This seems to all work okay. Though I don’t really understand what the point of the verification claims functions are. For example, we have add_claim("iss", fn -> "The Issuer" end, &(&1 == "The Issuer")).

Is this just to have the Joken.encode_and_sign check this is still valid at the time it runs? Why would this be necessary? How would this be tampered with at this stage? Seems pointless. The verification function logic &(&1 == "The Issuer") is not somehow encoded into the JWT, so what is the point of this at all?

In any case this seems to work and the token can be created and read correctly.

(2) Verify & Validate Token

I am not sure what the intended method of this is.

As far as I can tell, Joken.verify_and_validate just checks the encryption to make sure the key is correct and presumably that nothing is tampered with from payload vs. signature. Is that all that is intended? Or is it also supposed to be able to test the claims of the token like expiry? Or should we manually do that?

For example if you do:

def verify_token(token) do
            #create or get signer to check
            signer = Joken.Signer.create("RS256", %{"pem" => My.Token.public_key}) 
            
            # Define your token configuration (WHAT IS THE POINT? DOES NOTHING)
            config = %{
                alg: "HS256", # wrong algorithm, should be RS256
                claims: %{
                "exp" => %{required: true}, # I have no idea if this is right
                "iss" => "NOT THE ISSUER" # just to prove a point
                }
            }

            verify_result = Joken.verify_and_validate(config, token, signer) 

            verify_result = case (verify_result) do
                {:ok, claims_map}-> claims_map
                _-> nil
            end

            IO.puts("CHECK TOKEN CLAIMS: " <> inspect(verify_result)) 

            verify_result #returns claims map if succeeds or nil if not
end

This successfully gives you the claims map back, even for expired tokens or for empty config %{}, or even when we have “NOT THE ISSUER” for issuer or wrong “alg” and required for exp.

Is this correct? Or is it supposed to evaluate this things? Are we supposed to manually evaluate the claims map to make sure it has what we want and the exp is not past? What is the point or meaning of the “config” here?

eg. We can manually go through the claims map that is recovered after:

exp_date = Map.get(claims_map, "exp")
if exp_date < Joken.CurrentTime.OS.current_time() do
    #EXPIRED TOKEN
end

Is that what we’re supposed to do here? Feels like that should not be the case.

I also tried in the verification code (2) like this:

    token_config_map = %{} #empty

                # issuer:
                |> Joken.Config.add_claim("iss", fn -> "The Issuer" end, &(&1 == "The Issuer"))

                # expiry: 
                |> Joken.Config.add_claim("exp", fn -> Joken.CurrentTime.OS.current_time() end, &(&1 < Joken.CurrentTime.OS.current_time() ) ) 

            verify_result = Joken.verify_and_validate(token_config_map, token, signer) 

But that also does not fail even when the Token is expired. My theory is the “config map” is to supply two sides of data - one side is the claims (to be used on creation) and the other side is the verification functions. But either way it is not verifying that I can tell.

Thanks for any clarification or help.

They call out that very issue here

Since this is cumbersome and error prone, you can use this module with a more fluent API, see:

And also I’d recommend to use Joken.Config

Something like:

defmodule UserToken do
  use Joken.Config

  add_hook(Joken.Hooks.RequiredClaims, ~w[iss user_name ...]a)
  add_hook(...)

  def token_config do
    default_claims()
    |> add_claim("iss", ...) 
    |> ...
  end
end

And the you could use UserToken.verify_and_validate/1 that would validate exp… as it was the part of default_claims()

{:ok, claims} = UserToken.verify_and_validate(token)

Thanks, but there is a reason I am doing it as described, and it should still be able to work.

I saw their documentation suggesting a default_claims system and the Joken.Config system, but neither are a good idea in my opinion.

Re: default_claims I would like to be explicit about exactly what I am setting into every token I generate in that function. This is the safest way to be sure and not make mistakes. I do not want any default_claims. I do not want the claims being created in multiple places for one token. This is poor coding safety in my opinion.

I also do not like the Joken.Config concept with overriding their functions, because I would rather keep as distant from a system’s API as possible. I would rather call only the exact few functions I want from their API as needed and otherwise know I am not touching it. This also creates predictability and fewer likely breaking changes later if they change.

I am still following recommended functions. Their documentation says for example you can create a claim map like this:

  @impl true
  def token_config do
    %{} # empty claim map
    |> add_claim("name", fn -> "John Doe" end, &(&1 == "John Doe"))
    |> add_claim("test", fn -> true end, &(&1 == true))
    |> add_claim("age", fn -> 666 end, &(&1 > 18))
    |> add_claim("simple time test", fn -> 1 end, &(Joken.current_time() > &1))
  end

They also state here for a time expiration example, the following format (which they don’t recommend, but shows the method of a time expiration function) is the equivalent of using add_claim as above:

%{"exp" => %Joken.Claim{
  generate: fn -> Joken.Config.current_time() + (2 * 60 * 60) end,
  validate: fn val, _claims, _context -> val < Joken.Config.current_time() end
}}

This is the convention I followed (or I believe it was). I followed their add_claims method. Thus there should be some way to make this system work.

My code only uses 5 isolated Joken functions (which as I said is ideal in my preference, the less and more distinctly I use of it the better):

Joken.Signer.create # works as expected
Joken.Config.add_claim # seems to work
Joken.generate_claims # seems to work
Joken.encode_and_sign # seems to work
Joken.verify_and_validate # DOESN'T SEEM TO WORK

I seem to be getting everything except a correct verification.

I suspect that the way this system works is that the “claims maps” contain both a generate and validate function because they are meant to be used as one unit for both generating and validating. Though I am not sure of the utility of this design choice.

A “claims map” does not actually need any validate functions to create a JWT. And it does not need any generate functions to validate a JWT. So I suspect in theory we should be able to make a claims map with only generate functions and empty validate functions for creation of the JWT, and a claims map with empty generate functions that has just a few validate functions for validation. This makes more sense to me.

I don’t think any of this is well explained in the documentation but by deduction I presume that must be what is happening. Do you think that is correct?

Either way, I am still not sure what is happening with the verify_and_validate because everything else seems fine, as proven by the fact that I’m getting out valid tokens with the right contents.

Thanks for any further thoughts.

Well I just proved the point about the “generate” and “validate” components of the claims I think. You don’t need any “validate” functions in the create map function. But you do need an empty function taking one argument. Kind of funny but okay. I can’t complain too much it’s a free system someone likely worked very hard on. :slightly_smiling_face:

This is becoming clearer and more logical. You don’t need validation functions during generating a token. I seem to be getting some expiration validations happening now as well, though I still don’t understand what I am doing right or wrong with that.

Okay I have solved what needs to go where. You do not need “generate” functions in validating. And you don’t need “validate” functions in generating. You need nil functions as placeholders though.

I also think the &( &1) notation of Elixir is likely generally dangerous/foolish and obscures meaning for the sake of saving one or two characters each time. I think I had a > or < error in my original code as well but this was instructive.

Thus the corresponding improved code is:

(1) Generate Token

    def create_token(user_name, duration_hrs) do 

        # start with empty map
        token_config_map = %{} 

            # userID:
            |> Joken.Config.add_claim("user_name", fn -> user_name end, fn x-> nil end)

            # issuer:
            |> Joken.Config.add_claim("iss", fn -> "The Issuer" end, fn x-> nil end)

            # expiry: 
            |> Joken.Config.add_claim("exp", fn -> Joken.CurrentTime.OS.current_time() + (duration_hrs * 60 * 60) end, fn x-> nil end ) 

        # generate claims
        token_claims = Joken.generate_claims(token_config_map)

        # return the unwrapped claims or nil if fails
        token_claims = case token_claims do
            {:ok, claims}-> claims
            _ -> nil
        end

        #create signer or get if stored
        signer = Joken.Signer.create("RS256", %{"pem" => My.Token.private_key}) 

        # make the token
        result = Joken.encode_and_sign(token_claims, signer)

        case (result) do
            {:ok, token, claims} -> token
            _-> nil
        end
    end

(2) Verify Token

def verify_token(token) do
            #create or get signer to check
            signer = Joken.Signer.create("RS256", %{"pem" => My.Token.public_key}) 
            
            # Define your token configuration
            token_config_map = %{} #empty

                # issuer:
                |> Joken.Config.add_claim("iss", fn -> nil end, fn x-> x == "The Issuer" end)

                # expiry: 
                |> Joken.Config.add_claim("exp", fn -> nil end, fn x -> x > Joken.CurrentTime.OS.current_time() end ) 

            verify_result = Joken.verify_and_validate(token_config_map, token, signer) 

            verify_result = case (verify_result) do
                {:ok, claims_map}-> claims_map
                _-> nil
            end

            IO.puts("CHECK TOKEN CLAIMS: " <> inspect(verify_result)) 

            verify_result #returns claims map if succeeds or nil if not
end

All this now seems to work and it is more logical in my opinion than any of the methods described in the documentation.

1 Like