···103103 def validate_jwt(jwt, opts \\ []) do
104104 {expected_aud, expected_lxm} = options(opts)
105105106106- %{
107107- fields:
108108- %{
109109- "aud" => target_aud,
110110- "iat" => iat,
111111- "exp" => exp,
112112- "iss" => issuing_did,
113113- "jti" => nonce
114114- } = fields
115115- } = JOSE.JWT.peek(jwt)
106106+ peek_result =
107107+ try do
108108+ peeked = JOSE.JWT.peek(jwt)
109109+ {:ok, peeked}
110110+ rescue
111111+ _ -> {:error, :invalid_jwt}
112112+ end
113113+114114+ case peek_result do
115115+ {:error, _} = err ->
116116+ err
116117117117- target_lxm = Map.get(fields, "lxm")
118118+ {:ok,
119119+ %{
120120+ fields:
121121+ %{
122122+ "aud" => target_aud,
123123+ "iat" => iat,
124124+ "exp" => exp,
125125+ "iss" => issuing_did,
126126+ "jti" => nonce
127127+ } = fields
128128+ }} ->
129129+ target_lxm = Map.get(fields, "lxm")
118130119119- with :ok <- validate_aud(expected_aud, target_aud),
120120- :ok <- validate_lxm(expected_lxm, target_lxm),
121121- :ok <- validate_token_times(iat, exp),
122122- # Resolve JWT's issuer to: a) make sure it's a real identity, b) get
123123- # the signing key from their DID document to verify the token
124124- {:ok, identity} <- Atex.IdentityResolver.resolve(issuing_did),
125125- user_jwk when not is_nil(user_jwk) <-
126126- Atex.DID.Document.get_atproto_signing_key(identity.document),
127127- {true, %JOSE.JWT{} = jwt_struct, _jws} <- JOSE.JWT.verify(user_jwk, jwt),
128128- # Record the nonce atomically after successful verification. insert_new
129129- # is used under the hood so this returns :seen if the jti was already
130130- # consumed, preventing replay attacks.
131131- :ok <- Atex.ServiceAuth.JTICache.put(nonce, exp) do
132132- {:ok, jwt_struct}
133133- else
134134- :seen -> {:error, :replayed_token}
135135- err -> err
131131+ with :ok <- validate_aud(expected_aud, target_aud),
132132+ :ok <- validate_lxm(expected_lxm, target_lxm),
133133+ :ok <- validate_token_times(iat, exp),
134134+ # Resolve JWT's issuer to: a) make sure it's a real identity, b) get
135135+ # the signing key from their DID document to verify the token
136136+ {:ok, identity} <- Atex.IdentityResolver.resolve(issuing_did),
137137+ user_jwk when not is_nil(user_jwk) <-
138138+ Atex.DID.Document.get_atproto_signing_key(identity.document),
139139+ {true, %JOSE.JWT{} = jwt_struct, _jws} <- JOSE.JWT.verify(user_jwk, jwt),
140140+ # Record the nonce atomically after successful verification. insert_new
141141+ # is used under the hood so this returns :seen if the jti was already
142142+ # consumed, preventing replay attacks.
143143+ :ok <- Atex.ServiceAuth.JTICache.put(nonce, exp) do
144144+ {:ok, jwt_struct}
145145+ else
146146+ :seen -> {:error, :replayed_token}
147147+ err -> err
148148+ end
136149 end
137150 end
138151
+15
test/atex/service_auth_test.exs
···11+defmodule Atex.ServiceAuthTest do
22+ use ExUnit.Case, async: true
33+44+ describe "validate_jwt/2" do
55+ test "returns {:error, :invalid_jwt} for a malformed token string" do
66+ assert {:error, :invalid_jwt} =
77+ Atex.ServiceAuth.validate_jwt("not.a.valid.jwt", aud: "did:web:example.com")
88+ end
99+1010+ test "returns {:error, :invalid_jwt} for an empty string" do
1111+ assert {:error, :invalid_jwt} =
1212+ Atex.ServiceAuth.validate_jwt("", aud: "did:web:example.com")
1313+ end
1414+ end
1515+end