Skip to content

Commit 2815d7c

Browse files
authored
Use form_post response mode for interactive authentication (#596)
* Form_post support in system browser auth * Update server.go
1 parent 18520b0 commit 2815d7c

File tree

4 files changed

+92
-32
lines changed

4 files changed

+92
-32
lines changed

apps/internal/base/base.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -302,13 +302,10 @@ func (b Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, s
302302
if authParams.DomainHint != "" {
303303
v.Add("domain_hint", authParams.DomainHint)
304304
}
305-
// There were left over from an implementation that didn't use any of these. We may
306-
// need to add them later, but as of now aren't needed.
307-
/*
308-
if p.ResponseMode != "" {
309-
urlParams.Add("response_mode", p.ResponseMode)
310-
}
311-
*/
305+
// Use form_post response mode for interactive auth to avoid exposing the auth code in the URL
306+
if authParams.AuthorizationType == authority.ATInteractive {
307+
v.Add("response_mode", "form_post")
308+
}
312309
baseURL.RawQuery = v.Encode()
313310
return baseURL.String(), nil
314311
}

apps/internal/local/server.go

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ var okPage = []byte(`
2424
</head>
2525
<body>
2626
<p>Authentication complete. You can return to the application. Feel free to close this browser tab.</p>
27+
<p><strong>For your security:</strong> Do not share the contents of this page, the address bar, or take screenshots.</p>
2728
</body>
2829
</html>
2930
`)
@@ -42,6 +43,20 @@ const failPage = `
4243
</html>
4344
`
4445

46+
const unsupportedResponseModePage = `
47+
<!DOCTYPE html>
48+
<html>
49+
<head>
50+
<meta charset="utf-8" />
51+
<title>Authentication Failed</title>
52+
</head>
53+
<body>
54+
<p>Authentication failed. The response was received via a GET operation, which is not supported.</p>
55+
<p>You can return to the application. Feel free to close this browser tab.</p>
56+
</body>
57+
</html>
58+
`
59+
4560
// Result is the result from the redirect.
4661
type Result struct {
4762
// Code is the code sent by the authority server.
@@ -138,11 +153,24 @@ func (s *Server) putResult(r Result) {
138153
}
139154

140155
func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
141-
q := r.URL.Query()
156+
// Only accept POST requests (form_post response mode)
157+
// GET requests with query parameters are not supported for security reasons
158+
if r.Method != http.MethodPost {
159+
w.WriteHeader(http.StatusMethodNotAllowed)
160+
_, _ = w.Write([]byte(unsupportedResponseModePage))
161+
s.putResult(Result{Err: fmt.Errorf("response was received via a GET operation, which is not supported")})
162+
return
163+
}
164+
165+
// For form_post response mode, parameters come in the POST body
166+
if err := r.ParseForm(); err != nil {
167+
s.error(w, http.StatusBadRequest, "failed to parse form data: %v", err)
168+
return
169+
}
142170

143-
headerErr := q.Get("error")
171+
headerErr := r.PostFormValue("error")
144172
if headerErr != "" {
145-
desc := html.EscapeString(q.Get("error_description"))
173+
desc := html.EscapeString(r.PostFormValue("error_description"))
146174
escapedHeaderErr := html.EscapeString(headerErr)
147175
// Note: It is a little weird we handle some errors by not going to the failPage. If they all should,
148176
// change this to s.error() and make s.error() write the failPage instead of an error code.
@@ -152,7 +180,7 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
152180
return
153181
}
154182

155-
respState := q.Get("state")
183+
respState := r.PostFormValue("state")
156184
switch respState {
157185
case s.reqState:
158186
case "":
@@ -163,9 +191,9 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
163191
return
164192
}
165193

166-
code := q.Get("code")
194+
code := r.PostFormValue("code")
167195
if code == "" {
168-
s.error(w, http.StatusInternalServerError, "authorization code missing in query string")
196+
s.error(w, http.StatusInternalServerError, "authorization code missing in response")
169197
return
170198
}
171199

apps/internal/local/server_test.go

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,44 +23,44 @@ func TestServer(t *testing.T) {
2323
desc string
2424
reqState string
2525
port int
26-
q url.Values
26+
formData url.Values
2727
failPage bool
2828
statusCode int
2929
}{
3030
{
31-
desc: "Error: Query Values has 'error' key",
31+
desc: "Error: Form data has 'error' key",
3232
reqState: "state",
3333
port: 0,
34-
q: url.Values{"state": []string{"state"}, "error": []string{"error"}},
34+
formData: url.Values{"state": []string{"state"}, "error": []string{"error"}},
3535
statusCode: 200,
3636
failPage: true,
3737
},
3838
{
39-
desc: "Error: Query Values missing 'state' key",
39+
desc: "Error: Form data missing 'state' key",
4040
reqState: "state",
4141
port: 0,
42-
q: url.Values{"code": []string{"code"}},
42+
formData: url.Values{"code": []string{"code"}},
4343
statusCode: http.StatusInternalServerError,
4444
},
4545
{
46-
desc: "Error: Query Values missing had 'state' key value that was different that requested",
46+
desc: "Error: Form data had 'state' key value that was different than requested",
4747
reqState: "state",
4848
port: 0,
49-
q: url.Values{"state": []string{"etats"}, "code": []string{"code"}},
49+
formData: url.Values{"state": []string{"etats"}, "code": []string{"code"}},
5050
statusCode: http.StatusInternalServerError,
5151
},
5252
{
53-
desc: "Error: Query Values missing 'code' key",
53+
desc: "Error: Form data missing 'code' key",
5454
reqState: "state",
5555
port: 0,
56-
q: url.Values{"state": []string{"state"}},
56+
formData: url.Values{"state": []string{"state"}},
5757
statusCode: http.StatusInternalServerError,
5858
},
5959
{
6060
desc: "Success",
6161
reqState: "state",
6262
port: 0,
63-
q: url.Values{"state": []string{"state"}, "code": []string{"code"}},
63+
formData: url.Values{"state": []string{"state"}, "code": []string{"code"}},
6464
statusCode: 200,
6565
},
6666
}
@@ -79,14 +79,9 @@ func TestServer(t *testing.T) {
7979
if err != nil {
8080
panic(err)
8181
}
82-
u.RawQuery = test.q.Encode()
8382

84-
resp, err := http.DefaultClient.Do(
85-
&http.Request{
86-
Method: "GET",
87-
URL: u,
88-
},
89-
)
83+
// Send POST request with form data (form_post response mode)
84+
resp, err := http.DefaultClient.PostForm(u.String(), test.formData)
9085

9186
if err != nil {
9287
panic(err)
@@ -139,3 +134,36 @@ func TestServer(t *testing.T) {
139134
}
140135
}
141136
}
137+
138+
func TestServerRejectsGET(t *testing.T) {
139+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
140+
defer cancel()
141+
142+
serv, err := New("state", 0)
143+
if err != nil {
144+
t.Fatal(err)
145+
}
146+
defer serv.Shutdown()
147+
148+
u, err := url.Parse(serv.Addr)
149+
if err != nil {
150+
t.Fatal(err)
151+
}
152+
u.RawQuery = url.Values{"state": []string{"state"}, "code": []string{"code"}}.Encode()
153+
154+
// Send GET request (should be rejected)
155+
resp, err := http.DefaultClient.Get(u.String())
156+
if err != nil {
157+
t.Fatal(err)
158+
}
159+
defer resp.Body.Close()
160+
161+
if resp.StatusCode != http.StatusMethodNotAllowed {
162+
t.Errorf("GET request: got StatusCode %d, want %d", resp.StatusCode, http.StatusMethodNotAllowed)
163+
}
164+
165+
res := serv.Result(ctx)
166+
if res.Err == nil {
167+
t.Error("GET request: Result.Err == nil, want error about unexpected response mode")
168+
}
169+
}

apps/public/public_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,15 @@ func fakeBrowserOpenURL(authURL string) error {
5656
if redirect == "" {
5757
return errors.New("missing redirect param 'redirect_uri'")
5858
}
59-
// now send the info to our local redirect server
60-
resp, err := http.DefaultClient.Get(redirect + fmt.Sprintf("/?state=%s&code=fake_auth_code", state))
59+
// Verify response_mode=form_post is requested
60+
if q.Get("response_mode") != "form_post" {
61+
return errors.New("missing or incorrect response_mode, expected 'form_post'")
62+
}
63+
// Send POST request with form data (simulating form_post response mode)
64+
resp, err := http.DefaultClient.PostForm(redirect, url.Values{
65+
"state": []string{state},
66+
"code": []string{"fake_auth_code"},
67+
})
6168
if err != nil {
6269
return err
6370
}

0 commit comments

Comments
 (0)