Skip to content

Commit a8a6d1e

Browse files
authored
Merge pull request #228 from lambdaclass/google-sign-in
Add Google sign in
2 parents bec94ba + cdd15d1 commit a8a6d1e

15 files changed

+186
-67
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,14 @@ The release generated for `prod` expects some config values from the environment
5656
**NOTE:** Remember to mount the folder where `SSL_CERTFILE` and `SSL_KEYFILE` are stored into the container.
5757

5858
### Credentials
59-
#### GitHub login configuration
59+
#### GitHub and Google login configuration
6060

6161
For the GitHub login option to work, [OAuth cretentials](https://github.com/settings/applications/new)
6262
need to be generated and set as `GITHUB_CLIENTID` and `GITHUB_SECRET`
6363
environment variables.
6464

65+
Similarly, use the [Google developers console](https://console.developers.google.com/) to generate the Google credentials and set them as `GOOGLE_CLIENTID` and `GOOGLE_SECRET`.
66+
6567
### Email providers
6668
Holiday Pinger supports two different providers for sending emails: Amazon SES and Mailgun. To choose one, set it in the config file (`prod.conf` for the production environment) as
6769
```

priv/ui/src/holiday_ping_ui/auth/events.cljs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,22 @@
8282
:on-failure [:error-message "Registration failed"]}}))
8383

8484
(defmethod events/load-view
85-
:github-callback [_ _]
86-
{:dispatch [:github-code-submit]})
85+
:provider-callback [_ [_ provider]]
86+
{:dispatch [:provider-code-submit provider]})
8787

8888
(re-frame/reg-event-fx
89-
:github-code-submit
89+
:provider-code-submit
9090
[(re-frame/inject-cofx :location)]
91-
(fn [{:keys [location]} _]
91+
(fn [{:keys [location]} [_ provider]]
9292
(let [code (get-in location [:query "code"])]
9393
{:http-xhrio {:method :post
94-
:uri "/api/auth/github/code"
94+
:uri (str "/api/auth/" provider "/code")
9595
:timeout 8000
9696
:format (ajax/json-request-format)
9797
:params {:code code}
9898
:response-format (ajax/json-response-format {:keywords? true})
9999
:on-success [:login-success]
100-
:on-failure [:error-message "GitHub authentication failed"]}})))
100+
:on-failure [:error-message (str provider " authentication failed")]}})))
101101

102102
(defmethod events/load-view
103103
:register-confirm [_ _]

priv/ui/src/holiday_ping_ui/auth/views.cljs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,9 @@
1111
[views/section-size "is-one-third-desktop is-half-tablet"
1212
[:div.card
1313
[:div.card-content
14-
[:div.has-text-centered
15-
[:a.button.is-medium.is-primary.is-fullwidth
16-
{:data-pushy-ignore true ;; don't try to hadle this uri in the frontend
17-
:href "/oauth/github"}
18-
[:span.icon.is-medium [:i.fa.fa-github]]
19-
[:span " Login with GitHub"]]]
14+
(views/provider-login-button "GitHub" [:i.fa.fa-github])
15+
[:br]
16+
(views/provider-login-button "Google" [:i.fa.fa-google])
2017
[:hr]
2118
[views/message-view]
2219
[forms/form-view {:submit-text "Login"

priv/ui/src/holiday_ping_ui/common/views.cljs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@
2626
[:div.columns.is-centered
2727
(apply vector :div.column {:class (name size)} views)]]])
2828

29+
(defn provider-login-button
30+
[provider icon]
31+
[:div.has-text-centered
32+
[:a.button.is-medium.is-primary.is-fullwidth
33+
{:data-pushy-ignore true ;; don't try to handle this uri in the frontend
34+
:href (str "/oauth/" (clojure.string/lower-case provider))}
35+
[:span.icon.is-medium icon]
36+
[:span (str " Login with " provider)]]])
37+
2938
;; APP VIEWS
3039
(defn user-info-view []
3140
(when @(re-frame/subscribe [:access-token])

priv/ui/src/holiday_ping_ui/core.cljs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
:request-password-reset [auth/request-password-reset-view]
3535
:password-reset-sent [auth/password-reset-sent-view]
3636
:submit-password-reset [auth/submit-password-reset-view]
37-
:github-callback [common/loading-view]
37+
:provider-callback [common/loading-view]
3838
:holidays [holidays/holidays-view]
3939
:not-found [common/not-found-view]})
4040

priv/ui/src/holiday_ping_ui/routes.cljs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
["register/confirm" :resend-confirmation]
1616
["password" :request-password-reset]
1717
["password/code" :submit-password-reset]
18-
["oauth/github/callback" :github-callback]
18+
[["oauth/" [#".+" :provider] "/callback"] :provider-callback]
1919
[true :not-found]]])
2020

2121
(defn parse-url [url]
@@ -45,7 +45,7 @@
4545
(loging, register, etc.)."
4646
[view]
4747
(contains?
48-
#{:landing :login :register :github-callback :not-verified
48+
#{:landing :login :register :provider-callback :not-verified
4949
:register-confirm :email-sent :register-confirm-error :resend-confirmation
5050
:request-password-reset :submit-password-reset :password-reset-sent}
5151
view))

src/db/db_user.erl

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
-module(db_user).
22

3-
-export([create_holiday_user/3,
4-
create_github_user/2,
3+
-export([create_user/4,
54
get/1,
65
delete/1,
76
get_with_password/1,
@@ -16,18 +15,18 @@
1615
%% needed so atoms exist.
1716
user_keys () -> [email, password, name].
1817

19-
create_holiday_user(Email, Name, Password) ->
20-
Q = <<"INSERT INTO users(email, name, password, auth_type) "
21-
"VALUES($1, $2, $3, 'holiday') RETURNING email, name ">>,
22-
case db:query(Q, [Email, Name, Password]) of
18+
create_user(Email, Name, null, AuthType) ->
19+
Q = <<"INSERT INTO users(email, name, auth_type, verified) "
20+
"VALUES($1, $2, $3, true) RETURNING email, name ">>,
21+
case db:query(Q, [Email, Name, AuthType]) of
2322
{ok, [Result | []]} -> {ok, Result};
2423
{error, unique_violation} -> {error, user_already_exists}
25-
end.
24+
end;
2625

27-
create_github_user(Email, Name) ->
28-
Q = <<"INSERT INTO users(email, name, auth_type, verified)"
29-
"VALUES($1, $2, 'github', true) RETURNING email, name ">>,
30-
case db:query(Q, [Email, Name]) of
26+
create_user(Email, Name, Password, AuthType) ->
27+
Q = <<"INSERT INTO users(email, name, password, auth_type) "
28+
"VALUES($1, $2, $3, $4) RETURNING email, name ">>,
29+
case db:query(Q, [Email, Name, Password, AuthType]) of
3130
{ok, [Result | []]} -> {ok, Result};
3231
{error, unique_violation} -> {error, user_already_exists}
3332
end.

src/handlers/github_callback_handler.erl

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ from_json(Req, State) ->
3232
Email = get_primary_email(GithubToken),
3333

3434
ok = register_user(Email, Profile),
35-
Token = build_holiday_access_token(Email, Profile),
35+
Name = get_name(Email, Profile),
36+
AvatarUrl = maps:get(avatar_url, Profile),
37+
Token = hp_auth:build_holiday_access_token(Email, Name, AvatarUrl),
3638

3739
Encoded = hp_json:encode(#{access_token => Token}),
3840
Req3 = cowboy_req:set_resp_body(Encoded, Req2),
@@ -71,21 +73,12 @@ register_user(Email, Profile) ->
7173
%% only attempt to create it if it's not already registered
7274
case db_user:get(Email) of
7375
{error, not_found} ->
74-
{ok, _} = db_user:create_github_user(Email, get_name(Email, Profile)),
76+
{ok, _} = db_user:create_user(Email, get_name(Email, Profile), null, "github"),
7577
ok;
7678
{ok, _} ->
7779
ok
7880
end.
7981

80-
build_holiday_access_token(Email, Profile) ->
81-
Data = #{
82-
email => Email,
83-
name => get_name(Email, Profile),
84-
avatar => maps:get(avatar_url, Profile)
85-
},
86-
{ok, Token} = hp_auth:token_encode(Data),
87-
Token.
88-
8982
get_name(Email, #{name := null}) ->
9083
Email;
9184
get_name(_Email, #{name := Name}) ->

src/handlers/github_redirect_handler.erl

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
-module(google_callback_handler).
2+
3+
%%% REST handler that receives the authentication code set by Google in the OAuth callback.
4+
%%% If the user is new, register it with the data from Google
5+
%%% In any case, respond with an access_token
6+
7+
-export([init/3,
8+
allowed_methods/2,
9+
content_types_accepted/2,
10+
content_types_provided/2,
11+
from_json/2]).
12+
13+
init(_Transport, _Req, []) ->
14+
{upgrade, protocol, cowboy_rest}.
15+
16+
allowed_methods(Req, State) ->
17+
{[<<"POST">>], Req, State}.
18+
19+
content_types_accepted(Req, State) ->
20+
{[{<<"application/json">>, from_json}], Req, State}.
21+
22+
content_types_provided(Req, State) ->
23+
{[{<<"application/json">>, to_json}], Req, State}.
24+
25+
from_json(Req, State) ->
26+
{ok, Body, Req2} = cowboy_req:body(Req),
27+
28+
#{code := Code} = hp_json:decode(Body),
29+
30+
GoogleToken = get_google_access_token(Code),
31+
Profile = get_public_profile(GoogleToken),
32+
33+
Email = get_primary_field(Profile, emailAddresses),
34+
Name = get_primary_field(Profile, names),
35+
AvatarUrl = get_primary_field(Profile, photos),
36+
37+
ok = register_user(Email, Name),
38+
Token = hp_auth:build_holiday_access_token(Email, Name, AvatarUrl),
39+
40+
Encoded = hp_json:encode(#{access_token => Token}),
41+
Req3 = cowboy_req:set_resp_body(Encoded, Req2),
42+
{true, Req3, State}.
43+
44+
%% internal
45+
get_google_access_token(Code) ->
46+
ClientId = list_to_binary(os:getenv("GOOGLE_CLIENTID", false)),
47+
ClientSecret = list_to_binary(os:getenv("GOOGLE_SECRET", false)),
48+
GoogleBody = #{
49+
code => Code,
50+
client_id => ClientId,
51+
client_secret => ClientSecret,
52+
grant_type => << "authorization_code" >>,
53+
redirect_uri => << "https://holidaypinger.com/oauth/google/callback" >>
54+
},
55+
56+
{ok, 200, _, GoogleTokenResponse} =
57+
hp_request:post_json(<<"https://oauth2.googleapis.com/token">>,
58+
GoogleBody),
59+
maps:get(access_token, GoogleTokenResponse).
60+
61+
get_public_profile(GoogleToken) ->
62+
{ok, 200, _, Profile} = hp_request:get_json(<<"https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,photos">>,
63+
[{<<"Authorization">>, <<"Bearer ", GoogleToken/binary>>}]),
64+
Profile.
65+
66+
get_primary_field(Profile, Field) ->
67+
Fields = maps:get(Field, Profile),
68+
[PrimaryField | _] = lists:dropwhile(fun(#{metadata := #{primary := IsPrimary}}) -> not IsPrimary end, Fields),
69+
case Field of
70+
emailAddresses ->
71+
maps:get(value, PrimaryField);
72+
names ->
73+
maps:get(displayName, PrimaryField);
74+
photos ->
75+
maps:get(url, PrimaryField)
76+
end.
77+
78+
register_user(Email, Name) ->
79+
%% only attempt to create it if it's not already registered
80+
case db_user:get(Email) of
81+
{error, not_found} ->
82+
{ok, _} = db_user:create_user(Email, Name, null, "google"),
83+
ok;
84+
{ok, _} ->
85+
ok
86+
end.

0 commit comments

Comments
 (0)