diff --git a/lib/tesla_api/auth.ex b/lib/tesla_api/auth.ex index 459ee20b..9aca57ab 100644 --- a/lib/tesla_api/auth.ex +++ b/lib/tesla_api/auth.ex @@ -1,6 +1,8 @@ defmodule TeslaApi.Auth do use Tesla + alias TeslaApi.Error + @web_client_id "ownerapi" @redirect_uri "https://auth.tesla.com/void/callback" @@ -26,6 +28,54 @@ defmodule TeslaApi.Auth do defdelegate login(email, password), to: __MODULE__.Login defdelegate refresh(auth), to: __MODULE__.Refresh + def issuer_url(%__MODULE__{token: access_token}) do + case derive_issuer_url_from_oat(access_token) do + {:ok, issuer_url} -> + issuer_url + + :error -> + case decode_jwt_payload(access_token) do + {:ok, %{"iss" => iss}} -> URI.parse(iss) + _ -> "https://auth.tesla.com/oauth2/v3" + end + end + end + + def region(%__MODULE__{} = auth) do + tld = + auth + |> issuer_url() + |> URI.parse() + |> Map.fetch!(:host) + |> String.split(".") + |> List.last() + + case tld do + "cn" -> :chinese + "com" -> :global + _other -> :other + end + end + + defp derive_issuer_url_from_oat("qts-" <> _), do: {:ok, "https://auth.tesla.com/oauth2/v3"} + defp derive_issuer_url_from_oat("eu-" <> _), do: {:ok, "https://auth.tesla.com/oauth2/v3"} + defp derive_issuer_url_from_oat("cn-" <> _), do: {:ok, "https://auth.tesla.cn/oauth2/v3"} + defp derive_issuer_url_from_oat(_), do: :error + + defp decode_jwt_payload(jwt) do + with [_algo, payload, _signature] <- String.split(jwt, "."), + {:ok, payload} <- Base.decode64(payload, padding: false), + {:ok, payload} <- Jason.decode(payload) do + {:ok, payload} + else + l when is_list(l) -> + Error.into({:error, :invalid_jwt}, :invalid_access_token) + + error -> + Error.into(error, :invalid_access_token) + end + end + defp log_level(%Tesla.Env{} = env) when env.status >= 400, do: :error defp log_level(%Tesla.Env{}), do: :info end diff --git a/lib/tesla_api/auth/login.ex b/lib/tesla_api/auth/login.ex index dd916c46..a068839a 100644 --- a/lib/tesla_api/auth/login.ex +++ b/lib/tesla_api/auth/login.ex @@ -2,7 +2,7 @@ defmodule TeslaApi.Auth.Login do import TeslaApi.Auth, only: [get: 2, post: 2, post: 3] alias TeslaApi.Error - alias TeslaApi.Auth.{MFA, Util} + alias TeslaApi.Auth.{MFA, OwnerApi, Util} require Logger @@ -57,7 +57,8 @@ defmodule TeslaApi.Auth.Login do with {:ok, %Tesla.Env{} = env} <- submit_form(form, ctx), {:ok, {redirect_uri, code}} <- Util.parse_location_header(env, ctx.state), {:ok, auth} <- - get_web_token(code, ctx.code_verifier, redirect_uri, ctx.state, base: ctx.base_url) do + get_web_token(code, ctx.code_verifier, redirect_uri, ctx.state, base: ctx.base_url), + {:ok, auth} <- maybe_exchange_sso_tokens(auth) do {:ok, auth} end rescue @@ -193,7 +194,8 @@ defmodule TeslaApi.Auth.Login do MFA.verify_passcode(device_id, mfa_passcode, transaction_id, headers), {:ok, {redirect_uri, code}} <- Util.parse_location_header(env, ctx.state), {:ok, auth} <- - get_web_token(code, ctx.code_verifier, redirect_uri, ctx.state) do + get_web_token(code, ctx.code_verifier, redirect_uri, ctx.state), + {:ok, auth} <- maybe_exchange_sso_tokens(auth) do {:ok, auth} end rescue @@ -247,4 +249,14 @@ defmodule TeslaApi.Auth.Login do Error.into(error, :web_token_error) end end + + defp maybe_exchange_sso_tokens(%TeslaApi.Auth{} = auth) do + case TeslaApi.Auth.region(auth) do + :chinese -> + OwnerApi.exchange_sso_token(auth) + + _other -> + {:ok, auth} + end + end end diff --git a/lib/tesla_api/auth/owner_api.ex b/lib/tesla_api/auth/owner_api.ex new file mode 100644 index 00000000..e7ea29eb --- /dev/null +++ b/lib/tesla_api/auth/owner_api.ex @@ -0,0 +1,34 @@ +defmodule TeslaApi.Auth.OwnerApi do + import TeslaApi.Auth, only: [post: 3] + + alias TeslaApi.{Auth, Error} + + @client_id "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384" + @client_secret "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3" + + def exchange_sso_token(%Auth{} = sso_auth) do + data = %{ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + client_id: @client_id, + client_secret: @client_secret + } + + headers = [{"Authorization", "Bearer #{sso_auth.token}"}] + + case post("https://owner-api.teslamotors.com/oauth/token", data, headers: headers) do + {:ok, %Tesla.Env{status: 200, body: body}} -> + auth = %Auth{ + token: body["access_token"], + type: body["token_type"], + expires_in: body["expires_in"], + refresh_token: sso_auth.refresh_token, + created_at: body["created_at"] + } + + {:ok, auth} + + error -> + Error.into(error, :api_token_error) + end + end +end diff --git a/lib/tesla_api/auth/refresh.ex b/lib/tesla_api/auth/refresh.ex index c727c91c..224f18b7 100644 --- a/lib/tesla_api/auth/refresh.ex +++ b/lib/tesla_api/auth/refresh.ex @@ -2,21 +2,12 @@ defmodule TeslaApi.Auth.Refresh do import TeslaApi.Auth, only: [post: 2] alias TeslaApi.{Auth, Error} + alias TeslaApi.Auth.OwnerApi @web_client_id TeslaApi.Auth.web_client_id() def refresh(%Auth{} = auth) do - issuer_url = - case derive_issuer_url_from_oat(auth.token) do - {:ok, issuer_url} -> - issuer_url - - :error -> - case decode_jwt_payload(auth.token) do - {:ok, %{"iss" => iss}} -> URI.parse(iss) - _ -> "https://auth.tesla.com/oauth2/v3" - end - end + issuer_url = Auth.issuer_url(auth) data = %{ grant_type: "refresh_token", @@ -35,29 +26,16 @@ defmodule TeslaApi.Auth.Refresh do created_at: body["created_at"] } - {:ok, auth} + case Auth.region(auth) do + :chinese -> + OwnerApi.exchange_sso_token(auth) + + _other -> + {:ok, auth} + end error -> Error.into(error, :token_refresh) end end - - defp derive_issuer_url_from_oat("qts-" <> _), do: {:ok, "https://auth.tesla.com/oauth2/v3"} - defp derive_issuer_url_from_oat("eu-" <> _), do: {:ok, "https://auth.tesla.com/oauth2/v3"} - defp derive_issuer_url_from_oat("cn-" <> _), do: {:ok, "https://auth.tesla.cn/oauth2/v3"} - defp derive_issuer_url_from_oat(_), do: :error - - defp decode_jwt_payload(jwt) do - with [_algo, payload, _signature] <- String.split(jwt, "."), - {:ok, payload} <- Base.decode64(payload, padding: false), - {:ok, payload} <- Jason.decode(payload) do - {:ok, payload} - else - l when is_list(l) -> - Error.into({:error, :invalid_jwt}, :invalid_access_token) - - error -> - Error.into(error, :invalid_access_token) - end - end end