Skip to content

Commit dc8fea9

Browse files
authored
fix(guard): set correct validation for github token (#111)
1 parent a24ba12 commit dc8fea9

File tree

10 files changed

+348
-100
lines changed

10 files changed

+348
-100
lines changed

guard/lib/guard/api/bitbucket.ex

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule Guard.Api.Bitbucket do
22
require Logger
33
use Tesla
4+
alias Guard.Utils.OAuth
45

56
@api_base_url "https://api.bitbucket.org"
67
@base_url "https://bitbucket.org"
@@ -34,11 +35,11 @@ defmodule Guard.Api.Bitbucket do
3435
Fetch or refresh access token
3536
"""
3637
def user_token(repo_host_account) do
37-
cache_key = token_cache_key(repo_host_account.id)
38+
cache_key = OAuth.token_cache_key(repo_host_account)
3839

3940
case Cachex.get(:token_cache, cache_key) do
4041
{:ok, {token, expires_at}} when not is_nil(token) and token != "" ->
41-
if valid_token?(expires_at) do
42+
if OAuth.valid_token?(expires_at) do
4243
{:ok, {token, expires_at}}
4344
else
4445
fetch_and_cache_token(repo_host_account)
@@ -72,7 +73,7 @@ defmodule Guard.Api.Bitbucket do
7273

7374
case Tesla.post(client, @oauth2_path, body_params) do
7475
{:ok, %Tesla.Env{status: status, body: body}} when status in 200..299 ->
75-
handle_ok_token_response(repo_host_account, body)
76+
OAuth.handle_ok_token_response(repo_host_account, body)
7677

7778
{:ok, %Tesla.Env{status: status}} when status in 400..499 ->
7879
Logger.warn("Failed to refresh token, account might be revoked")
@@ -87,27 +88,6 @@ defmodule Guard.Api.Bitbucket do
8788
end
8889
end
8990

90-
defp handle_ok_token_response(repo_host_account, body) do
91-
body =
92-
if is_binary(body) do
93-
Jason.decode!(body)
94-
else
95-
body
96-
end
97-
98-
token = body["access_token"]
99-
expires_in = body["expires_in"]
100-
101-
current_time = DateTime.utc_now() |> DateTime.to_unix()
102-
expires_at = current_time + expires_in
103-
104-
if valid_token?(expires_at) do
105-
Cachex.put(:token_cache, token_cache_key(repo_host_account.id), {token, expires_at})
106-
end
107-
108-
{:ok, {token, expires_at}}
109-
end
110-
11191
defp build_token_client do
11292
{:ok, {client_id, client_secret}} = Guard.GitProviderCredentials.get(:bitbucket)
11393

@@ -124,12 +104,4 @@ defmodule Guard.Api.Bitbucket do
124104
Tesla.Middleware.JSON
125105
])
126106
end
127-
128-
defp token_cache_key(account_id), do: "bitbucket_token_#{account_id}"
129-
130-
defp valid_token?(expires_at) do
131-
current_time = DateTime.utc_now() |> DateTime.to_unix()
132-
# 5 minutes before expiration
133-
expires_at - 300 > current_time
134-
end
135107
end

guard/lib/guard/api/github.ex

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ defmodule Guard.Api.Github do
33

44
use Tesla
55

6+
alias Guard.Utils.OAuth
7+
8+
@oauth_base_url "https://github.com"
9+
@oauth_path "/login/oauth/access_token"
10+
611
plug(Tesla.Middleware.BaseUrl, "https://api.github.com")
712
plug(Tesla.Middleware.JSON)
813

@@ -29,16 +34,56 @@ defmodule Guard.Api.Github do
2934
end
3035
end
3136

32-
def validate_token(""), do: false
37+
@doc """
38+
Fetch or refresh access token
39+
"""
40+
def user_token(repo_host_account) do
41+
case validate_token(repo_host_account.token) do
42+
{:ok, true} -> {:ok, {repo_host_account.token, nil}}
43+
_ -> handle_fetch_token(repo_host_account)
44+
end
45+
end
3346

34-
def validate_token(token) do
47+
defp handle_fetch_token(repo_host_account) do
3548
{:ok, {client_id, client_secret}} = Guard.GitProviderCredentials.get(:github)
3649

37-
body = %{"access_token" => token}
50+
query_params = [
51+
grant_type: "refresh_token",
52+
refresh_token: repo_host_account.refresh_token,
53+
client_id: client_id,
54+
client_secret: client_secret
55+
]
56+
57+
client = build_token_client()
58+
59+
case Tesla.post(client, @oauth_path, nil, query: query_params) do
60+
{:ok, %Tesla.Env{status: status, body: body}} when status in 200..299 ->
61+
OAuth.handle_ok_token_response(repo_host_account, body, cache: false)
62+
63+
{:ok, %Tesla.Env{status: status}} when status in 400..499 ->
64+
Logger.warning("Failed to refresh github token, account might be revoked")
65+
{:error, :revoked}
66+
67+
{:ok, %Tesla.Env{status: _status}} ->
68+
{:error, :failed}
3869

39-
case post("/applications/#{client_id}/token", body,
40-
headers: authorization_headers(client_id, client_secret)
41-
) do
70+
{:error, error} ->
71+
Logger.error("Error fetching github token: #{inspect(error)}")
72+
{:error, :network_error}
73+
end
74+
end
75+
76+
defp build_token_client do
77+
Tesla.client([
78+
{Tesla.Middleware.BaseUrl, @oauth_base_url},
79+
Tesla.Middleware.JSON
80+
])
81+
end
82+
83+
def validate_token(""), do: false
84+
85+
def validate_token(token) do
86+
case get("", headers: authorization_headers(token)) do
4287
{:ok, res} ->
4388
is_valid = res.status in 200..299
4489

@@ -56,11 +101,9 @@ defmodule Guard.Api.Github do
56101
end
57102
end
58103

59-
defp authorization_headers(client_id, client_secret) do
104+
defp authorization_headers(token) do
60105
[
61-
{"Accept", "application/vnd.github+json"},
62-
{"Authorization", "Basic " <> Base.encode64("#{client_id}:#{client_secret}")},
63-
{"X-GitHub-Api-Version", "2022-11-28"}
106+
{"Authorization", "token #{token}"}
64107
]
65108
end
66109
end

guard/lib/guard/api/gitlab.ex

Lines changed: 6 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Guard.Api.Gitlab do
22
require Logger
33

4-
alias Guard.FrontRepo
4+
alias Guard.Utils.OAuth
55

66
@base_url "https://gitlab.com"
77
@oauth_path "/oauth/token"
@@ -11,11 +11,11 @@ defmodule Guard.Api.Gitlab do
1111
Fetch or refresh access token
1212
"""
1313
def user_token(repo_host_account) do
14-
cache_key = token_cache_key(repo_host_account.id)
14+
cache_key = OAuth.token_cache_key(repo_host_account)
1515

1616
case Cachex.get(:token_cache, cache_key) do
1717
{:ok, {token, expires_at}} when not is_nil(token) and token != "" ->
18-
if valid_token?(expires_at) do
18+
if OAuth.valid_token?(expires_at) do
1919
{:ok, {token, expires_at}}
2020
else
2121
handle_fetch_and_cache_token(repo_host_account)
@@ -31,24 +31,15 @@ defmodule Guard.Api.Gitlab do
3131

3232
case Tesla.get(client, @oauth_token_info_path) do
3333
{:ok, res} ->
34-
expires_at = calc_expires_at(res.body["expires_in"])
35-
{:ok, res.status in 200..299 && valid_token?(expires_at)}
34+
expires_at = OAuth.calc_expires_at(res.body["expires_in"])
35+
{:ok, res.status in 200..299 && OAuth.valid_token?(expires_at)}
3636

3737
{:error, error} ->
3838
Logger.error("Error validating token: #{inspect(error)}")
3939
{:error, :network_error}
4040
end
4141
end
4242

43-
# Case where the token never expires
44-
defp valid_token?(nil), do: true
45-
46-
defp valid_token?(expires_at) do
47-
current_time = DateTime.utc_now() |> DateTime.to_unix()
48-
# 5 minutes before expiration
49-
expires_at - 300 > current_time
50-
end
51-
5243
defp handle_fetch_and_cache_token(repo_host_account) do
5344
body_params = %{
5445
"grant_type" => "refresh_token",
@@ -60,7 +51,7 @@ defmodule Guard.Api.Gitlab do
6051

6152
case Tesla.post(client, @oauth_path, body_params) do
6253
{:ok, %Tesla.Env{status: status, body: body}} when status in 200..299 ->
63-
handle_ok_token_response(repo_host_account, body)
54+
OAuth.handle_ok_token_response(repo_host_account, body)
6455

6556
{:ok, %Tesla.Env{status: status}} when status in 400..499 ->
6657
Logger.warning("Failed to refresh gitlab token, account might be revoked")
@@ -100,39 +91,4 @@ defmodule Guard.Api.Gitlab do
10091

10192
gitlab_config[:default_scope]
10293
end
103-
104-
defp handle_ok_token_response(repo_host_account, body) do
105-
body =
106-
if is_binary(body) do
107-
Jason.decode!(body)
108-
else
109-
body
110-
end
111-
112-
token = body["access_token"]
113-
expires_in = body["expires_in"]
114-
refresh_token = body["refresh_token"]
115-
116-
expires_at = calc_expires_at(expires_in)
117-
118-
if valid_token?(expires_at) do
119-
Cachex.put(:token_cache, token_cache_key(repo_host_account.id), {token, expires_at})
120-
update_refresh_token(repo_host_account, refresh_token)
121-
end
122-
123-
{:ok, {token, expires_at}}
124-
end
125-
126-
defp calc_expires_at(nil), do: nil
127-
128-
defp calc_expires_at(expires_in) do
129-
current_time = DateTime.utc_now() |> DateTime.to_unix()
130-
current_time + expires_in
131-
end
132-
133-
defp token_cache_key(account_id), do: "gitlab_token_#{account_id}"
134-
135-
defp update_refresh_token(repo_host_account, refresh_token) do
136-
FrontRepo.RepoHostAccount.update_refresh_token(repo_host_account, refresh_token)
137-
end
13894
end

guard/lib/guard/front_repo/repo_host_account.ex

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ defmodule Guard.FrontRepo.RepoHostAccount do
128128
end
129129

130130
@spec get_github_token(String.t()) :: {:ok, String.t()} | {:error, :not_found}
131-
def get_github_token(user_id) do
131+
def get_github_token(user_id) when is_binary(user_id) do
132132
token =
133133
Guard.FrontRepo.RepoHostAccount
134134
|> where([rha], rha.user_id == ^user_id)
@@ -142,6 +142,20 @@ defmodule Guard.FrontRepo.RepoHostAccount do
142142
end
143143
end
144144

145+
def get_github_token(%__MODULE__{} = rha) do
146+
case Guard.Api.Github.user_token(rha) do
147+
{:ok, {_token, _expires_at}} = token_tuple ->
148+
token_tuple
149+
150+
{:error, :revoked} ->
151+
update_account(%{revoked: true}, rha)
152+
{:error, {"", nil}}
153+
154+
{:error, _} ->
155+
{:error, {"", nil}}
156+
end
157+
end
158+
145159
def get_bitbucket_token(rha) do
146160
case Guard.Api.Bitbucket.user_token(rha) do
147161
{:ok, {_token, _expires_at}} = token_tuple ->
@@ -170,8 +184,8 @@ defmodule Guard.FrontRepo.RepoHostAccount do
170184
end
171185
end
172186

173-
def update_refresh_token(rha, refresh_token) do
174-
update_account(%{refresh_token: refresh_token}, rha)
187+
def update_token_pair(rha, token, refresh_token) do
188+
update_account(%{token: token, refresh_token: refresh_token}, rha)
175189
end
176190

177191
@spec get_uid_by_login(String.t(), String.t()) :: {:ok, String.t()} | {:error, :not_found}

guard/lib/guard/grpc_servers/user_server.ex

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,11 @@ defmodule Guard.GrpcServers.UserServer do
384384
"Token for #{user.id} is #{if is_valid, do: "valid", else: "invalid"}. Updating revoke status."
385385
)
386386

387+
unless is_valid do
388+
cache_key = Guard.Utils.OAuth.token_cache_key(repo_account)
389+
Cachex.del(:token_cache, cache_key)
390+
end
391+
387392
FrontRepo.RepoHostAccount.update_revoke_status(repo_account, not is_valid)
388393

389394
{:error, _} ->
@@ -745,8 +750,15 @@ defmodule Guard.GrpcServers.UserServer do
745750
end)
746751
end
747752

748-
defp get_token(%{token: token, repo_host: "github"}, _opts) do
749-
{token, nil}
753+
defp get_token(%{repo_host: "github"} = repo_host_account, user_id: user_id) do
754+
case FrontRepo.RepoHostAccount.get_github_token(repo_host_account) do
755+
{:error, _} ->
756+
Logger.error("Token for User: '#{user_id}' and 'GITHUB' not found.")
757+
grpc_error!(:not_found, "Token for not found.")
758+
759+
{:ok, {token, expires_at}} ->
760+
{token, expires_at}
761+
end
750762
end
751763

752764
defp get_token(%{repo_host: "bitbucket"} = repo_host_account, user_id: user_id) do

guard/lib/guard/utils.ex

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,55 @@ defmodule Guard.Utils do
4444
def timestamp_to_datetime(_, default), do: {:ok, default}
4545
end
4646

47+
defmodule Guard.Utils.OAuth do
48+
def handle_ok_token_response(repo_host_account, body, opts \\ [cache: true]) do
49+
body =
50+
if is_binary(body) do
51+
Jason.decode!(body)
52+
else
53+
body
54+
end
55+
56+
token = body["access_token"]
57+
expires_in = body["expires_in"]
58+
refresh_token = body["refresh_token"]
59+
60+
expires_at = calc_expires_at(expires_in)
61+
62+
if valid_token?(expires_at) do
63+
update_token_pair(repo_host_account, token, refresh_token)
64+
end
65+
66+
if opts[:cache] do
67+
Cachex.put(:token_cache, token_cache_key(repo_host_account), {token, expires_at})
68+
end
69+
70+
{:ok, {token, expires_at}}
71+
end
72+
73+
defp update_token_pair(repo_host_account, token, refresh_token) do
74+
Guard.FrontRepo.RepoHostAccount.update_token_pair(repo_host_account, token, refresh_token)
75+
end
76+
77+
def calc_expires_at(nil), do: nil
78+
79+
def calc_expires_at(expires_in) do
80+
current_time = DateTime.utc_now() |> DateTime.to_unix()
81+
current_time + expires_in
82+
end
83+
84+
# Case where the token never expires
85+
def valid_token?(nil), do: true
86+
87+
def valid_token?(expires_at) do
88+
current_time = DateTime.utc_now() |> DateTime.to_unix()
89+
# 5 minutes before expiration
90+
expires_at - 300 > current_time
91+
end
92+
93+
def token_cache_key(%{repo_host: repo_host, id: id}), do: "#{repo_host}_token_#{id}"
94+
end
95+
4796
defmodule Guard.Utils.Http do
4897
require Logger
4998

0 commit comments

Comments
 (0)