diff --git a/lib/xrpc/oauth_client.ex b/lib/xrpc/oauth_client.ex index c7700ac..c1d2377 100644 --- a/lib/xrpc/oauth_client.ex +++ b/lib/xrpc/oauth_client.ex @@ -97,8 +97,8 @@ defmodule AshAtproto.XRPC.OAuthClient do {:ok, redirect_uri} <- Config.get_redirect_uri(strategy), {:ok, scopes} <- Config.get_key(strategy) do case OAuth.refresh_token( - resource.refresh_token, - resource.dpop_key, + resource.oauth_tokens.refresh_token, + resource.oauth_tokens.dpop_key, iss, token_endpoint, key: key, @@ -109,11 +109,10 @@ defmodule AshAtproto.XRPC.OAuthClient do {:ok, tokens, nonce} -> oauth_tokens = %{ - sub: tokens.did, access_token: tokens.access_token, refresh_token: tokens.refresh_token, expires_at: tokens.expires_at, - dpop_key: resource.dpop_key, + dpop_key: resource.oauth_tokens.dpop_key, dpop_nonce: nonce } @@ -133,19 +132,22 @@ defmodule AshAtproto.XRPC.OAuthClient do @spec maybe_refresh(t(), integer()) :: {:ok, t()} | {:error, any()} defp maybe_refresh(%__MODULE__{resource: resource} = client, buffer_minutes \\ 5) do - if token_expiring_soon?(resource.expires_at, buffer_minutes) do + if resource.oauth_tokens && + token_expiring_soon?(resource.oauth_tokens.expires_at, buffer_minutes) do do_refresh(client) else {:ok, client} end end - @spec token_expiring_soon?(NaiveDateTime.t(), integer()) :: boolean() + @spec token_expiring_soon?(DateTime.t() | nil, integer()) :: boolean() + defp token_expiring_soon?(nil, _), do: true + defp token_expiring_soon?(expires_at, buffer_minutes) do - now = NaiveDateTime.utc_now() - expiry_threshold = NaiveDateTime.add(now, buffer_minutes * 60, :second) + now = DateTime.utc_now() + expiry_threshold = DateTime.add(now, buffer_minutes, :minute) - NaiveDateTime.compare(expires_at, expiry_threshold) in [:lt, :eq] + DateTime.compare(expires_at, expiry_threshold) in [:lt, :eq] end @doc """ @@ -180,14 +182,17 @@ defmodule AshAtproto.XRPC.OAuthClient do opts |> Keyword.put(:url, url) |> Req.new() - |> Req.Request.put_header("authorization", "DPoP #{user_resource.access_token}"), + |> Req.Request.put_header( + "authorization", + "DPoP #{user_resource.oauth_tokens.access_token}" + ), {:ok, response, nonce} <- OAuth.request_protected_dpop_resource( request, iss, - user_resource.access_token, - user_resource.dpop_key, - user_resource.dpop_nonce + user_resource.oauth_tokens.access_token, + user_resource.oauth_tokens.dpop_key, + user_resource.oauth_tokens.dpop_nonce ) do case {update_resource_nonce(user_resource, nonce), response} do {{:ok, new_resource}, %{status: 200}} -> @@ -210,10 +215,6 @@ defmodule AshAtproto.XRPC.OAuthClient do end) end - # Execute a function with an exclusive lock on the session identified by the - # client's DID. This ensures that concurrent requests for the same user don't - # race during token refresh. - @spec with_session_lock(t(), (-> result)) :: result when result: any() defp with_session_lock(%__MODULE__{resource: %{did: did}}, fun) do Mutex.with_lock(Atex.SessionMutex, did, fun) end @@ -225,9 +226,9 @@ defmodule AshAtproto.XRPC.OAuthClient do case OAuth.request_protected_dpop_resource( request, iss, - resource.access_token, - resource.dpop_key, - resource.dpop_nonce + resource.oauth_tokens.access_token, + resource.oauth_tokens.dpop_key, + resource.oauth_tokens.dpop_nonce ) do {:ok, %{status: 200} = response, nonce} -> case update_resource_nonce(resource, nonce) do @@ -240,8 +241,6 @@ defmodule AshAtproto.XRPC.OAuthClient do {:ok, response, _nonce} -> if auth_error?(response) do - # We tried to refresh the token once but it's still failing - # Clear session and prompt dev to reauth or something delete_tokens(resource) {:error, response, :expired} else @@ -260,7 +259,6 @@ defmodule AshAtproto.XRPC.OAuthClient do end end - @spec auth_error?(Req.Response.t()) :: boolean() defp auth_error?(%{status: 401, headers: %{"www-authenticate" => [www_auth]}}), do: (String.starts_with?(www_auth, "Bearer") or String.starts_with?(www_auth, "DPoP")) and @@ -270,11 +268,10 @@ defmodule AshAtproto.XRPC.OAuthClient do defp update_resource_nonce(resource, nonce) do oauth_tokens = %{ - sub: resource.did, - access_token: resource.access_token, - refresh_token: resource.refresh_token, - expires_at: resource.expires_at, - dpop_key: resource.dpop_key, + access_token: resource.oauth_tokens.access_token, + refresh_token: resource.oauth_tokens.refresh_token, + expires_at: resource.oauth_tokens.expires_at, + dpop_key: resource.oauth_tokens.dpop_key, dpop_nonce: nonce } @@ -309,6 +306,7 @@ defmodule AshAtproto.XRPC.OAuthClient do end defp aud_from_resource(resource) do + # did stays at the top-level case IdentityResolver.resolve(resource.did) do {:ok, identity} -> {:ok, Document.get_pds_endpoint(identity.document)}