@@ -30,30 +30,34 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
3030 { conn , nil } = LivebookTeams . authenticate ( test , conn , [ ] )
3131 assert html_response ( conn , 200 ) =~ "teams_redirect"
3232
33+ session = get_session ( conn )
34+ assert state = session [ "teams_auth_state" ]
35+
3336 redirect_to =
3437 LivebookWeb.Endpoint . url ( )
3538 |> URI . new! ( )
36- |> URI . append_query ( "teams_identity" )
39+ |> URI . append_query ( URI . encode_query ( % { "teams_identity" => "" , "teams_state" => state } ) )
40+ |> URI . to_string ( )
3741
3842 # Step 2: Checks if the given request belongs to a browser
3943 conn =
40- build_conn ( :get , "/" , % { teams_redirect: "" , redirect_to: URI . to_string ( redirect_to ) } )
41- |> init_test_session ( % { } )
44+ build_conn ( :get , "/" , % { teams_redirect: "" , redirect_to: redirect_to } )
45+ |> init_test_session ( session )
4246
4347 { conn , nil } = LivebookTeams . authenticate ( test , conn , [ ] )
4448
4549 # Step 3: Get redirected to Livebook Teams
4650 location = Phoenix.ConnTest . redirected_to ( conn )
4751 uri = URI . parse ( location )
4852 assert uri . path == "/identity/authorize"
49- assert % { "token" => token } = URI . decode_query ( uri . query )
53+ assert % { "token" => token , "redirect_to" => ^ redirect_to } = URI . decode_query ( uri . query )
5054
5155 % { code: code } = TeamsRPC . allow_auth_request ( node , token )
5256
5357 # Step 4: Emulate the redirect back with the code for validation
5458 conn =
55- build_conn ( :get , "/" , % { teams_identity: "" , code: code } )
56- |> init_test_session ( Plug.Conn . get_session ( conn ) )
59+ build_conn ( :get , "/" , % { teams_identity: "" , teams_state: state , code: code } )
60+ |> init_test_session ( session )
5761
5862 assert { conn , % { id: _id , name: _ , email: _ , payload: % { } } = metadata } =
5963 LivebookTeams . authenticate ( test , conn , [ ] )
@@ -68,21 +72,82 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
6872 assert { % { halted: false } , ^ metadata } = LivebookTeams . authenticate ( test , conn , [ ] )
6973 end
7074
75+ test "does not accept a code obtained in another authentication flow" ,
76+ % { conn: conn , node: node , test: test } do
77+ # Someone goes through the authentication flow themselves, up to
78+ # the point where they have a code for their own identity
79+ attacker_conn = init_test_session ( conn , % { } )
80+ { attacker_conn , nil } = LivebookTeams . authenticate ( test , attacker_conn , [ ] )
81+ assert attacker_state = get_session ( attacker_conn , :teams_auth_state )
82+
83+ redirect_to =
84+ LivebookWeb.Endpoint . url ( )
85+ |> URI . new! ( )
86+ |> URI . append_query (
87+ URI . encode_query ( % { "teams_identity" => "" , "teams_state" => attacker_state } )
88+ )
89+
90+ attacker_conn =
91+ build_conn ( :get , "/" , % { teams_redirect: "" , redirect_to: URI . to_string ( redirect_to ) } )
92+ |> init_test_session ( get_session ( attacker_conn ) )
93+
94+ { attacker_conn , nil } = LivebookTeams . authenticate ( test , attacker_conn , [ ] )
95+
96+ uri = attacker_conn |> Phoenix.ConnTest . redirected_to ( ) |> URI . parse ( )
97+ assert % { "token" => token } = URI . decode_query ( uri . query )
98+
99+ % { code: code } = TeamsRPC . allow_auth_request ( node , token )
100+
101+ # Meanwhile the victim visits Livebook and starts their own flow
102+ victim_conn = init_test_session ( conn , % { } )
103+ { victim_conn , nil } = LivebookTeams . authenticate ( test , victim_conn , [ ] )
104+ victim_session = get_session ( victim_conn )
105+
106+ # Making the victim's browser complete the flow with the code has
107+ # no effect, no matter which state it is presented with
108+ for params <- [
109+ % { teams_identity: "" , teams_state: attacker_state , code: code } ,
110+ % { teams_identity: "" , code: code }
111+ ] do
112+ conn =
113+ build_conn ( :get , "/" , params )
114+ |> init_test_session ( victim_session )
115+
116+ assert { conn , nil } = LivebookTeams . authenticate ( test , conn , [ ] )
117+ assert redirected_to ( conn , 302 ) == "/"
118+ refute get_session ( conn , :livebook_teams_access_token )
119+
120+ # Following the redirect starts the authentication flow over
121+ conn =
122+ build_conn ( :get , "/" )
123+ |> init_test_session ( get_session ( conn ) )
124+
125+ assert { conn , nil } = LivebookTeams . authenticate ( test , conn , [ ] )
126+ assert html_response ( conn , 200 ) =~ "teams_redirect"
127+ end
128+ end
129+
71130 test "shows an error when the user does not belong to the org" , % { conn: conn , test: test } do
72- # Step 1: Emulate a request coming from Teams saying the user does belong to the org
131+ # Step 1: Start the authentication flow, which stores the state in the session
73132 conn = init_test_session ( conn , % { } )
133+ { conn , nil } = LivebookTeams . authenticate ( test , conn , [ ] )
134+ assert state = get_session ( conn , :teams_auth_state )
74135
136+ # Step 2: Emulate a request coming from Teams saying the user does not belong to the org
75137 params_from_teams = % {
76138 "teams_identity" => "" ,
139+ "teams_state" => state ,
77140 "failed_reason" => "you do not belong to this org"
78141 }
79142
80- conn = % { conn | params: params_from_teams }
143+ conn =
144+ build_conn ( :get , "/" , params_from_teams )
145+ |> init_test_session ( get_session ( conn ) )
81146
82147 { conn , nil } = LivebookTeams . authenticate ( test , conn , [ ] )
83148 assert conn . status == 302
84149
85- # Step 2 : follow the redirect keeping the session set in previous request
150+ # Step 3 : follow the redirect keeping the session set in previous request
86151 conn =
87152 build_conn ( :get , redirected_to ( conn ) )
88153 |> init_test_session ( get_session ( conn ) )
@@ -93,6 +158,40 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
93158 "Failed to authenticate with Livebook Teams: you do not belong to this org"
94159 end
95160
161+ test "starts over when the callback carries neither code nor failure reason" ,
162+ % { conn: conn , test: test } do
163+ conn = init_test_session ( conn , % { } )
164+ { conn , nil } = LivebookTeams . authenticate ( test , conn , [ ] )
165+ assert state = get_session ( conn , :teams_auth_state )
166+
167+ conn =
168+ build_conn ( :get , "/" , % { teams_identity: "" , teams_state: state } )
169+ |> init_test_session ( get_session ( conn ) )
170+
171+ { conn , nil } = LivebookTeams . authenticate ( test , conn , [ ] )
172+ assert redirected_to ( conn , 302 ) == "/"
173+ end
174+
175+ test "ignores a failure reason from an unknown authentication flow" ,
176+ % { conn: conn , test: test } do
177+ conn = init_test_session ( conn , % { } )
178+ { conn , nil } = LivebookTeams . authenticate ( test , conn , [ ] )
179+
180+ params_from_teams = % {
181+ "teams_identity" => "" ,
182+ "teams_state" => "invalid" ,
183+ "failed_reason" => "you do not belong to this org"
184+ }
185+
186+ conn =
187+ build_conn ( :get , "/" , params_from_teams )
188+ |> init_test_session ( get_session ( conn ) )
189+
190+ { conn , nil } = LivebookTeams . authenticate ( test , conn , [ ] )
191+ assert redirected_to ( conn , 302 ) == "/"
192+ refute get_session ( conn , :teams_failed_reason )
193+ end
194+
96195 test "deletes the cache if access token is invalid" ,
97196 % { test: test , node: node , team: team } do
98197 { conn , code } = authenticate_user_on_teams ( test , node , team )
0 commit comments