Use SSO Access Tokens

This commit is contained in:
Adrian Kumpf
2021-09-30 13:26:37 +02:00
parent ed4d738dbc
commit 00ce5c39ce
4 changed files with 63 additions and 82 deletions
+14 -16
View File
@@ -2,7 +2,7 @@ defmodule TeslaApi.Auth.Login do
import TeslaApi.Auth, only: [get: 2, post: 2, post: 3] import TeslaApi.Auth, only: [get: 2, post: 2, post: 3]
alias TeslaApi.Error alias TeslaApi.Error
alias TeslaApi.Auth.{MFA, OwnerApi, Util} alias TeslaApi.Auth.{MFA, Util}
require Logger require Logger
@@ -56,9 +56,8 @@ defmodule TeslaApi.Auth.Login do
with {:ok, %Tesla.Env{} = env} <- submit_form(form, ctx), with {:ok, %Tesla.Env{} = env} <- submit_form(form, ctx),
{:ok, {redirect_uri, code}} <- Util.parse_location_header(env, ctx.state), {:ok, {redirect_uri, code}} <- Util.parse_location_header(env, ctx.state),
{:ok, tokens} <- {:ok, auth} <-
get_web_token(code, ctx.code_verifier, redirect_uri, ctx.state, base: ctx.base_url), get_web_token(code, ctx.code_verifier, redirect_uri, ctx.state, base: ctx.base_url) do
{:ok, auth} <- OwnerApi.get_api_tokens(tokens) do
{:ok, auth} {:ok, auth}
end end
rescue rescue
@@ -193,9 +192,8 @@ defmodule TeslaApi.Auth.Login do
with {:ok, env} <- with {:ok, env} <-
MFA.verify_passcode(device_id, mfa_passcode, transaction_id, headers), MFA.verify_passcode(device_id, mfa_passcode, transaction_id, headers),
{:ok, {redirect_uri, code}} <- Util.parse_location_header(env, ctx.state), {:ok, {redirect_uri, code}} <- Util.parse_location_header(env, ctx.state),
{:ok, tokens} <- {:ok, auth} <-
get_web_token(code, ctx.code_verifier, redirect_uri, ctx.state), get_web_token(code, ctx.code_verifier, redirect_uri, ctx.state) do
{:ok, auth} <- OwnerApi.get_api_tokens(tokens) do
{:ok, auth} {:ok, auth}
end end
rescue rescue
@@ -234,16 +232,16 @@ defmodule TeslaApi.Auth.Login do
} }
case post("#{opts[:base]}/oauth2/v3/token", data) do case post("#{opts[:base]}/oauth2/v3/token", data) do
{:ok, {:ok, %Tesla.Env{status: 200, body: %{"state" => ^state} = body}} ->
%Tesla.Env{ auth = %TeslaApi.Auth{
status: 200, token: body["access_token"],
body: %{ type: body["token_type"],
"access_token" => access_token, expires_in: body["expires_in"],
"refresh_token" => refresh_token, refresh_token: body["refresh_token"],
"state" => ^state created_at: body["created_at"]
} }
}} ->
{:ok, %{access_token: access_token, refresh_token: refresh_token}} {:ok, auth}
error -> error ->
Error.into(error, :web_token_error) Error.into(error, :web_token_error)
-34
View File
@@ -1,34 +0,0 @@
defmodule TeslaApi.Auth.OwnerApi do
import TeslaApi.Auth, only: [post: 3]
alias TeslaApi.{Auth, Error}
@client_id "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384"
@client_secret "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3"
def get_api_tokens(%{access_token: access_token, refresh_token: refresh_token}) do
data = %{
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
client_id: @client_id,
client_secret: @client_secret
}
headers = [{"Authorization", "Bearer #{access_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: refresh_token,
created_at: body["created_at"]
}
{:ok, auth}
error ->
Error.into(error, :api_token_error)
end
end
end
+43 -26
View File
@@ -2,45 +2,62 @@ defmodule TeslaApi.Auth.Refresh do
import TeslaApi.Auth, only: [post: 2] import TeslaApi.Auth, only: [post: 2]
alias TeslaApi.{Auth, Error} alias TeslaApi.{Auth, Error}
alias TeslaApi.Auth.OwnerApi
@web_client_id TeslaApi.Auth.web_client_id() @web_client_id TeslaApi.Auth.web_client_id()
def refresh(%Auth{} = auth) do def refresh(%Auth{} = auth) do
with {:ok, %{access_token: _} = tokens} <- issuer_url =
refresh_oauth_access_token(auth.token, auth.refresh_token), case derive_issuer_url_from_oat(auth.token) do
{:ok, auth} <- OwnerApi.get_api_tokens(tokens) 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
data = %{
grant_type: "refresh_token",
scope: "openid email offline_access",
client_id: @web_client_id,
refresh_token: auth.refresh_token
}
case post("#{issuer_url}/token", data) 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: body["refresh_token"],
created_at: body["created_at"]
}
{:ok, auth} {:ok, auth}
else
error -> error ->
Error.into(error, :token_refresh) Error.into(error, :token_refresh)
end end
end end
defp refresh_oauth_access_token(access_token, refresh_token) do defp derive_issuer_url_from_oat("qts-" <> _), do: {:ok, "https://auth.tesla.com/oauth2/v3"}
data = %{ defp derive_issuer_url_from_oat("eu-" <> _), do: {:ok, "https://auth.tesla.com/oauth2/v3"}
grant_type: "refresh_token", defp derive_issuer_url_from_oat("cn-" <> _), do: {:ok, "https://auth.tesla.cn/oauth2/v3"}
scope: "openid email offline_access", defp derive_issuer_url_from_oat(_), do: :error
client_id: @web_client_id,
refresh_token: refresh_token
}
base_url = defp decode_jwt_payload(jwt) do
case access_token do with [_algo, payload, _signature] <- String.split(jwt, "."),
"cn-" <> _ -> "https://auth.tesla.cn" {:ok, payload} <- Base.decode64(payload, padding: false),
_qts -> nil {:ok, payload} <- Jason.decode(payload) do
end {:ok, payload}
else
case post("#{base_url}/oauth2/v3/token", data) do l when is_list(l) ->
{:ok, Error.into({:error, :invalid_jwt}, :invalid_access_token)
%Tesla.Env{
status: 200,
body: %{"access_token" => access_token, "refresh_token" => refresh_token}
}} ->
{:ok, %{access_token: access_token, refresh_token: refresh_token}}
error -> error ->
error Error.into(error, :invalid_access_token)
end end
end end
end end
+4 -4
View File
@@ -101,7 +101,7 @@ defmodule TeslaMate.Api do
with %Tokens{access: at, refresh: rt} when is_binary(at) and is_binary(rt) <- with %Tokens{access: at, refresh: rt} when is_binary(at) and is_binary(rt) <-
call(deps.auth, :get_tokens) do call(deps.auth, :get_tokens) do
restored_tokens = %Auth{token: at, refresh_token: rt, expires_in: 1.12 * 60 * 60} restored_tokens = %Auth{token: at, refresh_token: rt, expires_in: 20 * 60}
case refresh_tokens(restored_tokens) do case refresh_tokens(restored_tokens) do
{:ok, refreshed_tokens} -> {:ok, refreshed_tokens} ->
@@ -167,8 +167,8 @@ defmodule TeslaMate.Api do
{:error, reason} -> {:error, reason} ->
Logger.warning("Token refresh failed: #{inspect(reason, pretty: true)}") Logger.warning("Token refresh failed: #{inspect(reason, pretty: true)}")
Logger.warning("Retrying in 1 hour...") Logger.warning("Retrying in 5 minutes...")
Process.send_after(self(), :refresh_auth, :timer.hours(1)) Process.send_after(self(), :refresh_auth, :timer.minutes(5))
end end
{:error, reason} -> {:error, reason} ->
@@ -202,7 +202,7 @@ defmodule TeslaMate.Api do
defp schedule_refresh(%Auth{} = auth) do defp schedule_refresh(%Auth{} = auth) do
ms = ms =
auth.expires_in auth.expires_in
|> Kernel.*(0.9) |> Kernel.*(0.5)
|> round() |> round()
|> :timer.seconds() |> :timer.seconds()