Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to set success and error pages for when calling AcquireInt… #490

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4fdaed6
Add option to set success and error pages for when calling AcquireInt…
wayneforrest Jun 12, 2024
1319335
fix: setting errorPage
wayneforrest Jun 12, 2024
fa4a6be
Refactor using different function name WithSystemBrowserOptions to se…
wayneforrest Jun 20, 2024
c110b76
update module name
wayneforrest Jun 20, 2024
587663f
Revert failPage change, to be of type const string
wayneforrest Jun 21, 2024
aa7dde8
Change success and error pages lifetime scope to be per "session", and
wayneforrest Jun 21, 2024
4f65972
Revert "update module name"
wayneforrest Jul 18, 2024
92a81c0
Add tests for optional success and error pages
wayneforrest Aug 6, 2024
9ac831a
make success and error pages immutable, isolate changes from user app…
wayneforrest Aug 6, 2024
552855f
Update apps/internal/local/server_test.go
wayneforrest Aug 10, 2024
fad8abf
Update apps/internal/local/server_test.go
wayneforrest Aug 10, 2024
f28cc47
Update server_test remove redundant setting success and error pages t…
wayneforrest Aug 10, 2024
7b991a0
Add error code and description to errpage template variables
wayneforrest Aug 10, 2024
0f55b06
Update server to hold the error page html template
wayneforrest Aug 10, 2024
037b7e7
Update golang doc for WithSystemBrowserOptions describing the usage o…
wayneforrest Aug 10, 2024
2dc114f
Update failpage to use html templates
wayneforrest Aug 11, 2024
11000f8
Add tests for html template fail page
wayneforrest Aug 11, 2024
ac071ca
Refactor by setting fail and success pages when New HTTP server is cr…
wayneforrest Aug 11, 2024
260faed
Fix typo
wayneforrest Aug 11, 2024
61f5cf1
Add test for default fail error page
wayneforrest Aug 11, 2024
4b6ce29
add comments escaping html entities, protection against XSS
wayneforrest Aug 27, 2024
435203d
add test for escaping html entities, when using the error page template
wayneforrest Aug 27, 2024
3b04492
Update default error page XSS tests, to cover both the .Code and the …
wayneforrest Aug 28, 2024
0cde778
Add option error page XSS tests, to cover both the .Code and the .Err…
wayneforrest Aug 29, 2024
87372be
Remove html template as it uses reflection, unnecessarily increase a…
wayneforrest Sep 27, 2024
16fdca2
Refactor, put variable closer to where it is being used
wayneforrest Sep 27, 2024
4834645
Update use function parameter list syntax
wayneforrest Nov 30, 2024
60f4ed1
Update, use ReplaceAll for when replacing the erro code and err descr…
wayneforrest Nov 30, 2024
5c38a70
Update error template tests, extracting the expected values into the …
wayneforrest Nov 30, 2024
c4e8578
Update error page handling
wayneforrest Dec 9, 2024
f419d6a
Simplify tests by removing conditional logic, making use of the expte…
wayneforrest Dec 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 45 additions & 16 deletions apps/internal/local/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package local

import (
"bytes"
"context"
"fmt"
"html"
Expand All @@ -28,7 +29,7 @@ var okPage = []byte(`
</html>
`)

const failPage = `
var failPage = []byte(`
<!DOCTYPE html>
<html>
<head>
Expand All @@ -37,10 +38,19 @@ const failPage = `
</head>
<body>
<p>Authentication failed. You can return to the application. Feel free to close this browser tab.</p>
<p>Error details: error %s error_description: %s</p>
<p>Error details: error {{.Code}}, error description: {{.Err}}</p>
</body>
</html>
`
`)

var (
// code is the html template variable name,
// which matches the Result Code variable
code = []byte("{{.Code}}")
// err is the html template variable name
// which matches the Result Err variable
err = []byte("{{.Err}}")
)

// Result is the result from the redirect.
type Result struct {
Expand All @@ -53,14 +63,16 @@ type Result struct {
// Server is an HTTP server.
type Server struct {
// Addr is the address the server is listening on.
Addr string
resultCh chan Result
s *http.Server
reqState string
Addr string
resultCh chan Result
s *http.Server
reqState string
successPage []byte
errorPage []byte
}

// New creates a local HTTP server and starts it.
func New(reqState string, port int) (*Server, error) {
func New(reqState string, port int, successPage []byte, errorPage []byte) (*Server, error) {
var l net.Listener
var err error
var portStr string
Expand All @@ -84,11 +96,21 @@ func New(reqState string, port int) (*Server, error) {
return nil, err
}

if len(successPage) == 0 {
successPage = okPage
}

if len(errorPage) == 0 {
errorPage = failPage
}

serv := &Server{
Addr: fmt.Sprintf("http://localhost:%s", portStr),
s: &http.Server{Addr: "localhost:0", ReadHeaderTimeout: time.Second},
reqState: reqState,
resultCh: make(chan Result, 1),
Addr: fmt.Sprintf("http://localhost:%s", portStr),
s: &http.Server{Addr: "localhost:0", ReadHeaderTimeout: time.Second},
reqState: reqState,
resultCh: make(chan Result, 1),
successPage: successPage,
errorPage: errorPage,
}
serv.s.Handler = http.HandlerFunc(serv.handler)

Expand Down Expand Up @@ -142,11 +164,18 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {

headerErr := q.Get("error")
if headerErr != "" {
desc := html.EscapeString(q.Get("error_description"))
// Note: It is a little weird we handle some errors by not going to the failPage. If they all should,
// change this to s.error() and make s.error() write the failPage instead of an error code.
_, _ = w.Write([]byte(fmt.Sprintf(failPage, headerErr, desc)))
s.putResult(Result{Err: fmt.Errorf(desc)})

errDesc := q.Get("error_description")

errorPage := bytes.ReplaceAll(s.errorPage, code, []byte(html.EscapeString(headerErr))) // provides XSS protection
errorPage = bytes.ReplaceAll(errorPage, err, []byte(html.EscapeString(errDesc))) // provides XSS protection

_, _ = w.Write(errorPage)

errorDesc := fmt.Errorf(errDesc)
s.putResult(Result{Err: errorDesc})
return
}

Expand All @@ -167,7 +196,7 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
return
}

_, _ = w.Write(okPage)
_, _ = w.Write(s.successPage)
s.putResult(Result{Code: code})
}

Expand Down
124 changes: 117 additions & 7 deletions apps/internal/local/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package local

import (
"bytes"
"context"
"io"
"net/http"
Expand All @@ -20,12 +21,18 @@ func TestServer(t *testing.T) {
defer cancel()

tests := []struct {
desc string
reqState string
port int
q url.Values
failPage bool
statusCode int
desc string
reqState string
port int
q url.Values
failPage bool
statusCode int
successPage []byte
errorPage []byte
testTemplate bool
testErrCodeXSS bool
testErrDescriptionXSS bool
expected string
}{
{
desc: "Error: Query Values has 'error' key",
Expand Down Expand Up @@ -63,10 +70,99 @@ func TestServer(t *testing.T) {
q: url.Values{"state": []string{"state"}, "code": []string{"code"}},
statusCode: 200,
},
{
desc: "Error: Query Values missing 'state' key, and optional error page, with template having code and error",
reqState: "state",
port: 0,
q: url.Values{"error": []string{"error_code"}, "error_description": []string{"error_description"}},
statusCode: 200,
errorPage: []byte("test option error page {{.Code}} {{.Err}}"),
testTemplate: true,
expected: "test option error page error_code error_description",
},
{
desc: "Error: Query Values missing 'state' key, and optional error page, with template having only code",
reqState: "state",
port: 0,
q: url.Values{"error": []string{"error_code"}, "error_description": []string{"error_description"}},
statusCode: 200,
errorPage: []byte("test option error page {{.Code}}"),
testTemplate: true,
expected: "test option error page error_code",
},
{
desc: "Error: Query Values missing 'state' key, and optional error page, with template having only error",
reqState: "state",
port: 0,
q: url.Values{"error": []string{"error_code"}, "error_description": []string{"error_description"}},
statusCode: 200,
errorPage: []byte("test option error page {{.Err}}"),
testTemplate: true,
expected: "test option error page error_description",
},
{
desc: "Error: Query Values missing 'state' key, and optional error page, having no code or error",
reqState: "state",
port: 0,
q: url.Values{"error": []string{"error_code"}, "error_description": []string{"error_description"}},
statusCode: 200,
errorPage: []byte("test option error page"),
testTemplate: true,
expected: "test option error page",
},
{
desc: "Error: Query Values missing 'state' key, using default fail error page",
reqState: "state",
port: 0,
q: url.Values{"error": []string{"error_code"}, "error_description": []string{"error_description"}},
statusCode: 200,
testTemplate: true,
expected: "<p>Error details: error error_code, error description: error_description</p>",
},
{
desc: "Error: Query Values missing 'state' key, using default fail error page - Error Code XSS test",
reqState: "state",
port: 0,
q: url.Values{"error": []string{"<script>alert('this code snippet was executed')</script>"}, "error_description": []string{"error_description"}},
statusCode: 200,
testTemplate: true,
testErrCodeXSS: true,
},
{
desc: "Error: Query Values missing 'state' key, using default fail error page - Error Description XSS test",
reqState: "state",
port: 0,
q: url.Values{"error": []string{"error_code"}, "error_description": []string{"<script>alert('this code snippet was executed')</script>"}},
statusCode: 200,
testTemplate: true,
testErrDescriptionXSS: true,
},
{
desc: "Error: Query Values missing 'state' key, using optional fail error page - Error Code XSS test",
reqState: "state",
port: 0,
q: url.Values{"error": []string{"<script>alert('this code snippet was executed')</script>"}, "error_description": []string{"error_description"}},
statusCode: 200,
errorPage: []byte("error: {{.Code}} error_description: {{.Err}}"),
testTemplate: true,
testErrCodeXSS: true,
expected: "&lt;script&gt;alert(&#39;this code snippet was executed&#39;)&lt;/script&gt;",
},
{
desc: "Error: Query Values missing 'state' key, using optional fail error page - Error Description XSS test",
reqState: "state",
port: 0,
q: url.Values{"error": []string{"error_code"}, "error_description": []string{"<script>alert('this code snippet was executed')</script>"}},
statusCode: 200,
errorPage: []byte("error: {{.Code}} error_description: {{.Err}}"),
testTemplate: true,
testErrDescriptionXSS: true,
expected: "&lt;script&gt;alert(&#39;this code snippet was executed&#39;)&lt;/script&gt;",
},
}

for _, test := range tests {
serv, err := New(test.reqState, test.port)
serv, err := New(test.reqState, test.port, test.successPage, test.errorPage)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -129,6 +225,20 @@ func TestServer(t *testing.T) {
continue
}

if len(test.successPage) > 0 {
if !bytes.Equal(content, test.successPage) {
t.Errorf("TestServer(%s): -want/+got:\ntest option error page", test.desc)
}
continue
}

if test.testTemplate {
if !strings.Contains(string(content), test.expected) {
t.Errorf("TestServer(%s): -want:%s got:%s ", test.desc, test.expected, string(content))
}
continue
}

if !strings.Contains(string(content), "Authentication Complete") {
t.Errorf("TestServer(%s): got failed page, okay page", test.desc)
}
Expand Down
37 changes: 34 additions & 3 deletions apps/public/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,13 +522,44 @@ type interactiveAuthOptions struct {
claims, domainHint, loginHint, redirectURI, tenantID string
openURL func(url string) error
authnScheme AuthenticationScheme
successPage []byte
errorPage []byte
}

// AcquireInteractiveOption is implemented by options for AcquireTokenInteractive
type AcquireInteractiveOption interface {
acquireInteractiveOption()
}

// WithSystemBrowserOptions sets the optional success and error pages.
// The error page supports two optional html template variables {{.Code}} and {{.Err}},
// which will be replaced with the corresponding error code, and descriptions.
func WithSystemBrowserOptions(successPage, errorPage []byte) interface {
AcquireInteractiveOption
options.CallOption
} {
return struct {
AcquireInteractiveOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *interactiveAuthOptions:
t.successPage = make([]byte, len(successPage))
copy(t.successPage, successPage)

t.errorPage = make([]byte, len(errorPage))
copy(t.errorPage, errorPage)
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}

// WithLoginHint pre-populates the login prompt with a username.
func WithLoginHint(username string) interface {
AcquireInteractiveOption
Expand Down Expand Up @@ -671,7 +702,7 @@ func (pca Client) AcquireTokenInteractive(ctx context.Context, scopes []string,
if o.authnScheme != nil {
authParams.AuthnScheme = o.authnScheme
}
res, err := pca.browserLogin(ctx, redirectURL, authParams, o.openURL)
res, err := pca.browserLogin(ctx, redirectURL, authParams, o.openURL, o.successPage, o.errorPage)
if err != nil {
return AuthResult{}, err
}
Expand Down Expand Up @@ -709,13 +740,13 @@ func parsePort(u *url.URL) (int, error) {
}

// browserLogin calls openURL and waits for a user to log in
func (pca Client) browserLogin(ctx context.Context, redirectURI *url.URL, params authority.AuthParams, openURL func(string) error) (interactiveAuthResult, error) {
func (pca Client) browserLogin(ctx context.Context, redirectURI *url.URL, params authority.AuthParams, openURL func(string) error, successPage []byte, errorPage []byte) (interactiveAuthResult, error) {
// start local redirect server so login can call us back
port, err := parsePort(redirectURI)
if err != nil {
return interactiveAuthResult{}, err
}
srv, err := local.New(params.State, port)
srv, err := local.New(params.State, port, successPage, errorPage)
if err != nil {
return interactiveAuthResult{}, err
}
Expand Down