AUTH-5682 Org token flow in Access logins should pass CF_AppSession cookie

- Refactor HandleRedirects function and add unit tests
- Move signal test to its own file because of OS specific instructions
This commit is contained in:
James Royal 2023-11-13 11:46:06 -06:00
parent 33baad35b8
commit 652df22831
3 changed files with 161 additions and 56 deletions

54
token/signal_test.go Normal file
View File

@ -0,0 +1,54 @@
//go:build linux || darwin
package token
import (
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSignalHandler(t *testing.T) {
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
handlerRan := false
done := make(chan struct{})
timer := time.NewTimer(time.Second)
sigHandler.register(func() {
handlerRan = true
done <- struct{}{}
})
p, err := os.FindProcess(os.Getpid())
require.Nil(t, err)
p.Signal(syscall.SIGUSR1)
// Blocks for up to one second to make sure the handler callback runs before the assert.
select {
case <-done:
assert.True(t, handlerRan)
case <-timer.C:
t.Fail()
}
sigHandler.deregister()
}
func TestSignalHandlerClose(t *testing.T) {
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
done := make(chan struct{})
timer := time.NewTimer(time.Second)
sigHandler.register(func() { done <- struct{}{} })
sigHandler.deregister()
p, err := os.FindProcess(os.Getpid())
require.Nil(t, err)
p.Signal(syscall.SIGUSR1)
select {
case <-done:
t.Fail()
case <-timer.C:
}
}

View File

@ -23,9 +23,11 @@ import (
const ( const (
keyName = "token" keyName = "token"
tokenCookie = "CF_Authorization" tokenCookie = "CF_Authorization"
appSessionCookie = "CF_AppSession"
appDomainHeader = "CF-Access-Domain" appDomainHeader = "CF-Access-Domain"
appAUDHeader = "CF-Access-Aud" appAUDHeader = "CF-Access-Aud"
AccessLoginWorkerPath = "/cdn-cgi/access/login" AccessLoginWorkerPath = "/cdn-cgi/access/login"
AccessAuthorizedWorkerPath = "/cdn-cgi/access/authorized"
) )
var ( var (
@ -297,20 +299,41 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
return &AppInfo{location.Hostname(), aud, domain}, nil return &AppInfo{location.Hostname(), aud, domain}, nil
} }
func handleRedirects(req *http.Request, via []*http.Request, orgToken string) error {
// attach org token to login request
if strings.Contains(req.URL.Path, AccessLoginWorkerPath) {
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: orgToken})
}
// attach app session cookie to authorized request
if strings.Contains(req.URL.Path, AccessAuthorizedWorkerPath) {
// We need to check and see if the CF_APP_SESSION cookie was set
for _, prevReq := range via {
if prevReq != nil && prevReq.Response != nil {
for _, c := range prevReq.Response.Cookies() {
if c.Name == appSessionCookie {
req.AddCookie(&http.Cookie{Name: appSessionCookie, Value: c.Value})
return nil
}
}
}
}
}
// stop after hitting authorized endpoint since it will contain the app token
if len(via) > 0 && strings.Contains(via[len(via)-1].URL.Path, AccessAuthorizedWorkerPath) {
return http.ErrUseLastResponse
}
return nil
}
// exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO // exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO
// flow to automatically generate and return an app token without the login page. // flow to automatically generate and return an app token without the login page.
func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) { func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) {
client := &http.Client{ client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
// attach org token to login request return handleRedirects(req, via, orgToken)
if strings.Contains(req.URL.Path, AccessLoginWorkerPath) {
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: orgToken})
}
// stop after hitting authorized endpoint since it will contain the app token
if strings.Contains(via[len(via)-1].URL.Path, "cdn-cgi/access/authorized") {
return http.ErrUseLastResponse
}
return nil
}, },
Timeout: time.Second * 7, Timeout: time.Second * 7,
} }

View File

@ -1,54 +1,82 @@
//go:build linux
package token package token
import ( import (
"os" "net/http"
"syscall" "net/url"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestSignalHandler(t *testing.T) { func TestHandleRedirects_AttachOrgToken(t *testing.T) {
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}} req, _ := http.NewRequest("GET", "http://example.com/cdn-cgi/access/login", nil)
handlerRan := false via := []*http.Request{}
done := make(chan struct{}) orgToken := "orgTokenValue"
timer := time.NewTimer(time.Second)
sigHandler.register(func() {
handlerRan = true
done <- struct{}{}
})
p, err := os.FindProcess(os.Getpid()) handleRedirects(req, via, orgToken)
require.Nil(t, err)
p.Signal(syscall.SIGUSR1)
// Blocks for up to one second to make sure the handler callback runs before the assert. // Check if the orgToken cookie is attached
select { cookies := req.Cookies()
case <-done: found := false
assert.True(t, handlerRan) for _, cookie := range cookies {
case <-timer.C: if cookie.Name == tokenCookie && cookie.Value == orgToken {
t.Fail() found = true
break
}
} }
sigHandler.deregister()
}
func TestSignalHandlerClose(t *testing.T) { if !found {
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}} t.Errorf("OrgToken cookie not attached to the request.")
done := make(chan struct{}) }
timer := time.NewTimer(time.Second) }
sigHandler.register(func() { done <- struct{}{} })
sigHandler.deregister() func TestHandleRedirects_AttachAppSessionCookie(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/cdn-cgi/access/authorized", nil)
p, err := os.FindProcess(os.Getpid()) via := []*http.Request{
require.Nil(t, err) {
p.Signal(syscall.SIGUSR1) URL: &url.URL{Path: "/cdn-cgi/access/login"},
select { Response: &http.Response{
case <-done: Header: http.Header{"Set-Cookie": {"CF_AppSession=appSessionValue"}},
t.Fail() },
case <-timer.C: },
}
orgToken := "orgTokenValue"
err := handleRedirects(req, via, orgToken)
// Check if the appSessionCookie is attached to the request
cookies := req.Cookies()
found := false
for _, cookie := range cookies {
if cookie.Name == appSessionCookie && cookie.Value == "appSessionValue" {
found = true
break
}
}
if !found {
t.Errorf("AppSessionCookie not attached to the request.")
}
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
func TestHandleRedirects_StopAtAuthorizedEndpoint(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/cdn-cgi/access/authorized", nil)
via := []*http.Request{
{
URL: &url.URL{Path: "other"},
},
{
URL: &url.URL{Path: AccessAuthorizedWorkerPath},
},
}
orgToken := "orgTokenValue"
err := handleRedirects(req, via, orgToken)
// Check if ErrUseLastResponse is returned
if err != http.ErrUseLastResponse {
t.Errorf("Expected ErrUseLastResponse, got %v", err)
} }
} }