mirror of
https://github.com/nais/wonderwall.git
synced 2026-08-23 21:16:14 +00:00
refactor(handler): move handler tests to separate files
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -22,718 +20,6 @@ import (
|
||||
"github.com/nais/wonderwall/pkg/session"
|
||||
)
|
||||
|
||||
func TestHandler_Login(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
resp := localLogin(t, rpClient, idp)
|
||||
loginURL := resp.Location
|
||||
|
||||
req := idp.GetRequest(idp.RelyingPartyServer.URL + "/oauth2/login")
|
||||
|
||||
expectedCallbackURL, err := urlpkg.LoginCallbackURL(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, idp.ProviderServer.URL, fmt.Sprintf("%s://%s", loginURL.Scheme, loginURL.Host))
|
||||
assert.Equal(t, "/authorize", loginURL.Path)
|
||||
assert.Equal(t, idp.OpenIDConfig.Client().ACRValues(), loginURL.Query().Get("acr_values"))
|
||||
assert.Equal(t, idp.OpenIDConfig.Client().UILocales(), loginURL.Query().Get("ui_locales"))
|
||||
assert.Equal(t, idp.OpenIDConfig.Client().ClientID(), loginURL.Query().Get("client_id"))
|
||||
assert.Equal(t, expectedCallbackURL, loginURL.Query().Get("redirect_uri"))
|
||||
assert.Equal(t, "S256", loginURL.Query().Get("code_challenge_method"))
|
||||
assert.ElementsMatch(t, idp.OpenIDConfig.Client().Scopes(), strings.Split(loginURL.Query().Get("scope"), " "))
|
||||
assert.NotEmpty(t, loginURL.Query().Get("state"))
|
||||
assert.NotEmpty(t, loginURL.Query().Get("nonce"))
|
||||
assert.NotEmpty(t, loginURL.Query().Get("code_challenge"))
|
||||
|
||||
resp = get(t, rpClient, loginURL.String())
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
callbackURL := resp.Location
|
||||
assert.Equal(t, loginURL.Query().Get("state"), callbackURL.Query().Get("state"))
|
||||
assert.NotEmpty(t, callbackURL.Query().Get("code"))
|
||||
}
|
||||
|
||||
func TestHandler_Callback(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
}
|
||||
|
||||
func TestHandler_Logout(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := selfInitiatedLogout(t, rpClient, idp)
|
||||
|
||||
// Get endsession endpoint after local logout
|
||||
endsessionURL := resp.Location
|
||||
|
||||
idpserverURL, err := url.Parse(idp.ProviderServer.URL)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := idp.GetRequest(idp.RelyingPartyServer.URL + "/oauth2/logout/callback")
|
||||
expectedLogoutCallbackURL, err := urlpkg.LogoutCallbackURL(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
endsessionParams := endsessionURL.Query()
|
||||
assert.Equal(t, idpserverURL.Host, endsessionURL.Host)
|
||||
assert.Equal(t, "/endsession", endsessionURL.Path)
|
||||
assert.Equal(t, []string{expectedLogoutCallbackURL}, endsessionParams["post_logout_redirect_uri"])
|
||||
assert.NotEmpty(t, endsessionParams["id_token_hint"])
|
||||
}
|
||||
|
||||
func TestHandler_LogoutCallback(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
logout(t, rpClient, idp)
|
||||
}
|
||||
|
||||
func TestHandler_FrontChannelLogout(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.OpenIDConfig.TestProvider.WithFrontChannelLogoutSupport()
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
sessionCookie := login(t, rpClient, idp)
|
||||
|
||||
// Trigger front-channel logout
|
||||
sid := func(r *http.Request) string {
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(sessionCookie.Value)
|
||||
assert.NoError(t, err)
|
||||
|
||||
sessionKey, err := idp.RelyingPartyHandler.GetCrypter().Decrypt(ciphertext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
data, err := idp.RelyingPartyHandler.GetSessions().GetForKey(r, string(sessionKey))
|
||||
assert.NoError(t, err)
|
||||
|
||||
return data.ExternalSessionID
|
||||
}
|
||||
|
||||
frontchannelLogoutURL, err := url.Parse(idp.RelyingPartyServer.URL + "/oauth2/logout/frontchannel")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := idp.GetRequest(frontchannelLogoutURL.String())
|
||||
|
||||
values := url.Values{}
|
||||
values.Add("sid", sid(req))
|
||||
values.Add("iss", idp.OpenIDConfig.Provider().Issuer())
|
||||
frontchannelLogoutURL.RawQuery = values.Encode()
|
||||
|
||||
resp := get(t, rpClient, frontchannelLogoutURL.String())
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestHandler_LogoutLocal(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
localLogout(t, rpClient, idp)
|
||||
}
|
||||
|
||||
func TestHandler_SessionStateRequired(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.OpenIDConfig.TestProvider.WithCheckSessionIFrameSupport(idp.ProviderServer.URL + "/checksession")
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
resp := authorize(t, rpClient, idp)
|
||||
|
||||
// Get callback URL after successful auth
|
||||
params := resp.Location.Query()
|
||||
sessionState := params.Get("session_state")
|
||||
assert.NotEmpty(t, sessionState)
|
||||
}
|
||||
|
||||
func TestHandler_SessionInfo(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Minute
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var data session.MetadataVerbose
|
||||
err := json.Unmarshal([]byte(resp.Body), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
allowedSkew := 5 * time.Second
|
||||
assert.WithinDuration(t, time.Now(), data.Session.CreatedAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now().Add(cfg.Session.MaxLifetime), data.Session.EndsAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now().Add(idp.ProviderHandler.TokenDuration), data.Tokens.ExpireAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now(), data.Tokens.RefreshedAt, allowedSkew)
|
||||
|
||||
sessionEndDuration := time.Duration(data.Session.EndsInSeconds) * time.Second
|
||||
// 1 second < time until session ends <= configured max session lifetime
|
||||
assert.LessOrEqual(t, sessionEndDuration, cfg.Session.MaxLifetime)
|
||||
assert.Greater(t, sessionEndDuration, time.Second)
|
||||
|
||||
tokenExpiryDuration := time.Duration(data.Tokens.ExpireInSeconds) * time.Second
|
||||
// 1 second < time until token expires <= max duration for tokens from IDP
|
||||
assert.LessOrEqual(t, tokenExpiryDuration, idp.ProviderHandler.TokenDuration)
|
||||
assert.Greater(t, tokenExpiryDuration, time.Second)
|
||||
|
||||
assert.True(t, data.Session.Active)
|
||||
assert.True(t, data.Session.TimeoutAt.IsZero())
|
||||
assert.Equal(t, int64(-1), data.Session.TimeoutInSeconds)
|
||||
}
|
||||
|
||||
func TestHandler_SessionInfo_WithInactivity(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
cfg.Session.Inactivity = true
|
||||
cfg.Session.InactivityTimeout = 10 * time.Minute
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var data session.MetadataVerbose
|
||||
err := json.Unmarshal([]byte(resp.Body), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
maxDelta := 5 * time.Second
|
||||
|
||||
assert.True(t, data.Session.Active)
|
||||
assert.False(t, data.Session.TimeoutAt.IsZero())
|
||||
|
||||
expectedTimeoutAt := time.Now().Add(cfg.Session.InactivityTimeout)
|
||||
assert.WithinDuration(t, expectedTimeoutAt, data.Session.TimeoutAt, maxDelta)
|
||||
|
||||
actualTimeoutDuration := time.Duration(data.Session.TimeoutInSeconds) * time.Second
|
||||
assert.WithinDuration(t, expectedTimeoutAt, time.Now().Add(actualTimeoutDuration), maxDelta)
|
||||
}
|
||||
|
||||
func TestHandler_SessionInfo_WithRefresh(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Minute
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var data session.MetadataVerboseWithRefresh
|
||||
err := json.Unmarshal([]byte(resp.Body), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
allowedSkew := 5 * time.Second
|
||||
assert.WithinDuration(t, time.Now(), data.Session.CreatedAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now().Add(cfg.Session.MaxLifetime), data.Session.EndsAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now().Add(idp.ProviderHandler.TokenDuration), data.Tokens.ExpireAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now(), data.Tokens.RefreshedAt, allowedSkew)
|
||||
|
||||
sessionEndDuration := time.Duration(data.Session.EndsInSeconds) * time.Second
|
||||
// 1 second < time until session ends <= configured max session lifetime
|
||||
assert.LessOrEqual(t, sessionEndDuration, cfg.Session.MaxLifetime)
|
||||
assert.Greater(t, sessionEndDuration, time.Second)
|
||||
|
||||
tokenExpiryDuration := time.Duration(data.Tokens.ExpireInSeconds) * time.Second
|
||||
// 1 second < time until token expires <= max duration for tokens from IDP
|
||||
assert.LessOrEqual(t, tokenExpiryDuration, idp.ProviderHandler.TokenDuration)
|
||||
assert.Greater(t, tokenExpiryDuration, time.Second)
|
||||
|
||||
// 1 second < next token refresh <= seconds until token expires
|
||||
assert.LessOrEqual(t, data.Tokens.NextAutoRefreshInSeconds, data.Tokens.ExpireInSeconds)
|
||||
assert.Greater(t, data.Tokens.NextAutoRefreshInSeconds, int64(1))
|
||||
|
||||
assert.True(t, data.Tokens.RefreshCooldown)
|
||||
// 1 second < refresh cooldown <= minimum refresh interval
|
||||
assert.LessOrEqual(t, data.Tokens.RefreshCooldownSeconds, session.RefreshMinInterval)
|
||||
assert.Greater(t, data.Tokens.RefreshCooldownSeconds, int64(1))
|
||||
|
||||
assert.True(t, data.Session.Active)
|
||||
assert.True(t, data.Session.TimeoutAt.IsZero())
|
||||
assert.Equal(t, int64(-1), data.Session.TimeoutInSeconds)
|
||||
}
|
||||
|
||||
func TestHandler_SessionRefresh(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Second
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
// get initial session info
|
||||
resp := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var data session.MetadataVerboseWithRefresh
|
||||
err := json.Unmarshal([]byte(resp.Body), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// wait until refresh cooldown has reached zero before refresh
|
||||
waitForRefreshCooldownTimer(t, idp, rpClient)
|
||||
|
||||
resp = sessionRefresh(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var refreshedData session.MetadataVerboseWithRefresh
|
||||
err = json.Unmarshal([]byte(resp.Body), &refreshedData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// session create and end times should be unchanged
|
||||
assert.WithinDuration(t, data.Session.CreatedAt, refreshedData.Session.CreatedAt, 0)
|
||||
assert.WithinDuration(t, data.Session.EndsAt, refreshedData.Session.EndsAt, 0)
|
||||
|
||||
// token expiration and refresh times should be later than before
|
||||
assert.True(t, refreshedData.Tokens.ExpireAt.After(data.Tokens.ExpireAt))
|
||||
assert.True(t, refreshedData.Tokens.RefreshedAt.After(data.Tokens.RefreshedAt))
|
||||
|
||||
allowedSkew := 5 * time.Second
|
||||
assert.WithinDuration(t, time.Now().Add(idp.ProviderHandler.TokenDuration), refreshedData.Tokens.ExpireAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now(), refreshedData.Tokens.RefreshedAt, allowedSkew)
|
||||
|
||||
sessionEndDuration := time.Duration(refreshedData.Session.EndsInSeconds) * time.Second
|
||||
// 1 second < time until session ends <= configured max session lifetime
|
||||
assert.LessOrEqual(t, sessionEndDuration, cfg.Session.MaxLifetime)
|
||||
assert.Greater(t, sessionEndDuration, time.Second)
|
||||
|
||||
tokenExpiryDuration := time.Duration(refreshedData.Tokens.ExpireInSeconds) * time.Second
|
||||
// 1 second < time until token expires <= max duration for tokens from IDP
|
||||
assert.LessOrEqual(t, tokenExpiryDuration, idp.ProviderHandler.TokenDuration)
|
||||
assert.Greater(t, tokenExpiryDuration, time.Second)
|
||||
|
||||
// 1 second < next token refresh <= seconds until token expires
|
||||
assert.LessOrEqual(t, refreshedData.Tokens.NextAutoRefreshInSeconds, refreshedData.Tokens.ExpireInSeconds)
|
||||
assert.Greater(t, refreshedData.Tokens.NextAutoRefreshInSeconds, int64(1))
|
||||
|
||||
assert.True(t, refreshedData.Tokens.RefreshCooldown)
|
||||
// 1 second < refresh cooldown <= minimum refresh interval
|
||||
assert.LessOrEqual(t, refreshedData.Tokens.RefreshCooldownSeconds, session.RefreshMinInterval)
|
||||
assert.Greater(t, refreshedData.Tokens.RefreshCooldownSeconds, int64(1))
|
||||
|
||||
assert.True(t, data.Session.Active)
|
||||
assert.True(t, refreshedData.Session.Active)
|
||||
|
||||
assert.True(t, data.Session.TimeoutAt.IsZero())
|
||||
assert.True(t, refreshedData.Session.TimeoutAt.IsZero())
|
||||
|
||||
assert.Equal(t, int64(-1), data.Session.TimeoutInSeconds)
|
||||
assert.Equal(t, int64(-1), refreshedData.Session.TimeoutInSeconds)
|
||||
}
|
||||
|
||||
func TestHandler_SessionRefresh_Disabled(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = false
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Second
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := sessionRefresh(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestHandler_SessionRefresh_WithInactivity(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
cfg.Session.Inactivity = true
|
||||
cfg.Session.InactivityTimeout = 10 * time.Minute
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Second
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
// get initial session info
|
||||
resp := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var data session.MetadataVerboseWithRefresh
|
||||
err := json.Unmarshal([]byte(resp.Body), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// wait until refresh cooldown has reached zero before refresh
|
||||
waitForRefreshCooldownTimer(t, idp, rpClient)
|
||||
|
||||
resp = sessionRefresh(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var refreshedData session.MetadataVerboseWithRefresh
|
||||
err = json.Unmarshal([]byte(resp.Body), &refreshedData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
maxDelta := 5 * time.Second
|
||||
|
||||
assert.True(t, data.Session.Active)
|
||||
assert.True(t, refreshedData.Session.Active)
|
||||
|
||||
assert.False(t, data.Session.TimeoutAt.IsZero())
|
||||
assert.False(t, refreshedData.Session.TimeoutAt.IsZero())
|
||||
|
||||
expectedTimeoutAt := time.Now().Add(cfg.Session.InactivityTimeout)
|
||||
assert.WithinDuration(t, expectedTimeoutAt, data.Session.TimeoutAt, maxDelta)
|
||||
assert.WithinDuration(t, expectedTimeoutAt, refreshedData.Session.TimeoutAt, maxDelta)
|
||||
|
||||
assert.True(t, refreshedData.Session.TimeoutAt.After(data.Session.TimeoutAt))
|
||||
|
||||
previousTimeoutDuration := time.Duration(data.Session.TimeoutInSeconds) * time.Second
|
||||
assert.WithinDuration(t, expectedTimeoutAt, time.Now().Add(previousTimeoutDuration), maxDelta)
|
||||
|
||||
refreshedTimeoutDuration := time.Duration(refreshedData.Session.TimeoutInSeconds) * time.Second
|
||||
assert.WithinDuration(t, expectedTimeoutAt, time.Now().Add(refreshedTimeoutDuration), maxDelta)
|
||||
}
|
||||
|
||||
func TestHandler_Default(t *testing.T) {
|
||||
up := newUpstream(t)
|
||||
defer up.Server.Close()
|
||||
|
||||
t.Run("without auto-login", func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
// initial request without session
|
||||
resp := get(t, rpClient, idp.RelyingPartyServer.URL)
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
assert.Equal(t, "not ok", resp.Body)
|
||||
|
||||
// acquire session
|
||||
login(t, rpClient, idp)
|
||||
|
||||
// retry request with session
|
||||
resp = get(t, rpClient, idp.RelyingPartyServer.URL)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, "ok", resp.Body)
|
||||
})
|
||||
|
||||
t.Run("with auto-login", func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.AutoLogin = true
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
// initial request without session
|
||||
target := idp.RelyingPartyServer.URL + "/"
|
||||
|
||||
resp := get(t, rpClient, target)
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
// redirect should point to local login endpoint
|
||||
loginLocation := resp.Location
|
||||
assert.Equal(t, idp.RelyingPartyServer.URL+"/oauth2/login?redirect-encoded="+urlpkg.RedirectEncoded("/"), loginLocation.String())
|
||||
|
||||
// follow redirect to local login endpoint
|
||||
resp = get(t, rpClient, loginLocation.String())
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
// redirect should point to identity provider
|
||||
authorizeLocation := resp.Location
|
||||
|
||||
authorizeEndpoint := *authorizeLocation
|
||||
authorizeEndpoint.RawQuery = ""
|
||||
assert.Equal(t, idp.OpenIDConfig.Provider().AuthorizationEndpoint(), authorizeEndpoint.String())
|
||||
|
||||
// follow redirect to identity provider for login
|
||||
resp = get(t, rpClient, authorizeLocation.String())
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
// redirect should point back to relying party
|
||||
callbackLocation := resp.Location
|
||||
|
||||
callbackEndpoint := *callbackLocation
|
||||
callbackEndpoint.RawQuery = ""
|
||||
|
||||
req := idp.GetRequest(callbackLocation.String())
|
||||
expectedCallbackURL, err := urlpkg.LoginCallbackURL(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedCallbackURL, callbackEndpoint.String())
|
||||
|
||||
// follow redirect back to relying party
|
||||
resp = get(t, rpClient, callbackLocation.String())
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
// finally, follow redirect back to original target, now with a session
|
||||
targetLocation := resp.Location
|
||||
assert.Equal(t, target, targetLocation.String())
|
||||
|
||||
resp = get(t, rpClient, targetLocation.String())
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, "ok", resp.Body)
|
||||
})
|
||||
|
||||
t.Run("with auto-login for non-GET requests", func(t *testing.T) {
|
||||
for _, method := range []string{
|
||||
http.MethodConnect,
|
||||
http.MethodDelete,
|
||||
http.MethodHead,
|
||||
http.MethodOptions,
|
||||
http.MethodPatch,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodTrace,
|
||||
} {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.AutoLogin = true
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
req, err := http.NewRequest(method, idp.RelyingPartyServer.URL, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resp, err := rpClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with auto-login and ignored paths", func(t *testing.T) {
|
||||
for pattern, tt := range map[string]struct {
|
||||
match []string
|
||||
nonMatch []string
|
||||
}{
|
||||
"/": {
|
||||
match: []string{
|
||||
"/",
|
||||
"",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/a",
|
||||
"/a/b",
|
||||
},
|
||||
},
|
||||
"/exact/match": {
|
||||
match: []string{
|
||||
"/exact/match",
|
||||
"/exact/match/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/exact/match/huh",
|
||||
},
|
||||
},
|
||||
"/allowed": {
|
||||
match: []string{
|
||||
"/allowed",
|
||||
"/allowed/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/allowe",
|
||||
"/allowed/no",
|
||||
"/not-allowed",
|
||||
"/not-allowed/allowed",
|
||||
},
|
||||
},
|
||||
"/wildcard/*": {
|
||||
match: []string{
|
||||
"/wildcard/very",
|
||||
"/wildcard/very/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/wildcard",
|
||||
"/wildcard/",
|
||||
"/wildcard/yup/nope",
|
||||
},
|
||||
},
|
||||
"/deeper/*/*": {
|
||||
match: []string{
|
||||
"/deeper/1/2",
|
||||
"/deeper/1/2/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/deeper",
|
||||
"/deeper/",
|
||||
"/deeper/1",
|
||||
"/deeper/1/",
|
||||
"/deeper/1/2/3",
|
||||
},
|
||||
},
|
||||
"/any*": {
|
||||
match: []string{
|
||||
"/any",
|
||||
"/any/",
|
||||
"/anything",
|
||||
"/anything/",
|
||||
"/anywho",
|
||||
"/anywho/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/any/thing",
|
||||
"/any/thing/",
|
||||
"/anywho/mst/ve",
|
||||
},
|
||||
},
|
||||
"/trailing/": {
|
||||
match: []string{
|
||||
"/trailing",
|
||||
"/trailing/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/trailing/path",
|
||||
"/trailing/path/",
|
||||
},
|
||||
},
|
||||
"/nested/**": {
|
||||
match: []string{
|
||||
"/nested",
|
||||
"/nested/",
|
||||
"/nested/very",
|
||||
"/nested/very/deep",
|
||||
"/nested/very/deep/deeper",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/not/nested",
|
||||
"/not/nested/very",
|
||||
},
|
||||
},
|
||||
"/static/**/*.js": {
|
||||
match: []string{
|
||||
"/static/bundle.js",
|
||||
"/static/min/bundle.js",
|
||||
"/static/vendor/min/bundle.js",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/static",
|
||||
"/static/",
|
||||
"/static/some.css",
|
||||
"/static/min",
|
||||
"/static/min/",
|
||||
"/static/min/some.css",
|
||||
"/static/vendor/min/some.css",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(pattern, func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
cfg.AutoLogin = true
|
||||
cfg.AutoLoginIgnorePaths = []string{pattern}
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
t.Run("match", func(t *testing.T) {
|
||||
for _, path := range tt.match {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
target := idp.RelyingPartyServer.URL + path
|
||||
resp := get(t, rpClient, target)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
assert.Equal(t, "not ok", resp.Body)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-match", func(t *testing.T) {
|
||||
for _, path := range tt.nonMatch {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
target := idp.RelyingPartyServer.URL + path
|
||||
resp := get(t, rpClient, target)
|
||||
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("request with authorization header set", func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
t.Run("should be preserved if no session found", func(t *testing.T) {
|
||||
up.requestCallback = func(r *http.Request) {
|
||||
authorization := r.Header.Get("Authorization")
|
||||
assert.Equal(t, "Bearer some-authorization", authorization)
|
||||
}
|
||||
|
||||
resp := getWithHeaders(t, rpClient, idp.RelyingPartyServer.URL, map[string]string{
|
||||
"Authorization": "Bearer some-authorization",
|
||||
})
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
assert.Equal(t, "not ok", resp.Body)
|
||||
})
|
||||
|
||||
t.Run("should be overwritten if session found", func(t *testing.T) {
|
||||
// acquire session
|
||||
login(t, rpClient, idp)
|
||||
|
||||
up.requestCallback = func(r *http.Request) {
|
||||
authorization := r.Header.Get("Authorization")
|
||||
assert.NotEqual(t, "Bearer some-authorization", authorization)
|
||||
}
|
||||
|
||||
resp := getWithHeaders(t, rpClient, idp.RelyingPartyServer.URL, map[string]string{
|
||||
"Authorization": "Bearer some-authorization",
|
||||
})
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, "ok", resp.Body)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func localLogin(t *testing.T, rpClient *http.Client, idp *mock.IdentityProvider) response {
|
||||
// First, run /oauth2/login to set cookies
|
||||
loginURL, err := url.Parse(idp.RelyingPartyServer.URL + "/oauth2/login")
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/nais/wonderwall/pkg/mock"
|
||||
)
|
||||
|
||||
func TestCallback(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
}
|
||||
|
||||
func TestCallback_SessionStateRequired(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.OpenIDConfig.TestProvider.WithCheckSessionIFrameSupport(idp.ProviderServer.URL + "/checksession")
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
resp := authorize(t, rpClient, idp)
|
||||
|
||||
// Get callback URL after successful auth
|
||||
params := resp.Location.Query()
|
||||
sessionState := params.Get("session_state")
|
||||
assert.NotEmpty(t, sessionState)
|
||||
|
||||
callback(t, rpClient, resp)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
urlpkg "github.com/nais/wonderwall/pkg/handler/url"
|
||||
"github.com/nais/wonderwall/pkg/mock"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
resp := localLogin(t, rpClient, idp)
|
||||
loginURL := resp.Location
|
||||
|
||||
req := idp.GetRequest(idp.RelyingPartyServer.URL + "/oauth2/login")
|
||||
|
||||
expectedCallbackURL, err := urlpkg.LoginCallbackURL(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, idp.ProviderServer.URL, fmt.Sprintf("%s://%s", loginURL.Scheme, loginURL.Host))
|
||||
assert.Equal(t, "/authorize", loginURL.Path)
|
||||
assert.Equal(t, idp.OpenIDConfig.Client().ACRValues(), loginURL.Query().Get("acr_values"))
|
||||
assert.Equal(t, idp.OpenIDConfig.Client().UILocales(), loginURL.Query().Get("ui_locales"))
|
||||
assert.Equal(t, idp.OpenIDConfig.Client().ClientID(), loginURL.Query().Get("client_id"))
|
||||
assert.Equal(t, expectedCallbackURL, loginURL.Query().Get("redirect_uri"))
|
||||
assert.Equal(t, "S256", loginURL.Query().Get("code_challenge_method"))
|
||||
assert.ElementsMatch(t, idp.OpenIDConfig.Client().Scopes(), strings.Split(loginURL.Query().Get("scope"), " "))
|
||||
assert.NotEmpty(t, loginURL.Query().Get("state"))
|
||||
assert.NotEmpty(t, loginURL.Query().Get("nonce"))
|
||||
assert.NotEmpty(t, loginURL.Query().Get("code_challenge"))
|
||||
|
||||
resp = get(t, rpClient, loginURL.String())
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
callbackURL := resp.Location
|
||||
assert.Equal(t, loginURL.Query().Get("state"), callbackURL.Query().Get("state"))
|
||||
assert.NotEmpty(t, callbackURL.Query().Get("code"))
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nais/wonderwall/pkg/mock"
|
||||
)
|
||||
|
||||
func TestLogoutCallback(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
logout(t, rpClient, idp)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/nais/wonderwall/pkg/mock"
|
||||
)
|
||||
|
||||
func TestFrontChannelLogout(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.OpenIDConfig.TestProvider.WithFrontChannelLogoutSupport()
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
sessionCookie := login(t, rpClient, idp)
|
||||
|
||||
// Trigger front-channel logout
|
||||
sid := func(r *http.Request) string {
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(sessionCookie.Value)
|
||||
assert.NoError(t, err)
|
||||
|
||||
sessionKey, err := idp.RelyingPartyHandler.GetCrypter().Decrypt(ciphertext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
data, err := idp.RelyingPartyHandler.GetSessions().GetForKey(r, string(sessionKey))
|
||||
assert.NoError(t, err)
|
||||
|
||||
return data.ExternalSessionID
|
||||
}
|
||||
|
||||
frontchannelLogoutURL, err := url.Parse(idp.RelyingPartyServer.URL + "/oauth2/logout/frontchannel")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := idp.GetRequest(frontchannelLogoutURL.String())
|
||||
|
||||
values := url.Values{}
|
||||
values.Add("sid", sid(req))
|
||||
values.Add("iss", idp.OpenIDConfig.Provider().Issuer())
|
||||
frontchannelLogoutURL.RawQuery = values.Encode()
|
||||
|
||||
resp := get(t, rpClient, frontchannelLogoutURL.String())
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
urlpkg "github.com/nais/wonderwall/pkg/handler/url"
|
||||
"github.com/nais/wonderwall/pkg/mock"
|
||||
)
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := selfInitiatedLogout(t, rpClient, idp)
|
||||
|
||||
// Get endsession endpoint after local logout
|
||||
endsessionURL := resp.Location
|
||||
|
||||
idpserverURL, err := url.Parse(idp.ProviderServer.URL)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := idp.GetRequest(idp.RelyingPartyServer.URL + "/oauth2/logout/callback")
|
||||
expectedLogoutCallbackURL, err := urlpkg.LogoutCallbackURL(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
endsessionParams := endsessionURL.Query()
|
||||
assert.Equal(t, idpserverURL.Host, endsessionURL.Host)
|
||||
assert.Equal(t, "/endsession", endsessionURL.Path)
|
||||
assert.Equal(t, []string{expectedLogoutCallbackURL}, endsessionParams["post_logout_redirect_uri"])
|
||||
assert.NotEmpty(t, endsessionParams["id_token_hint"])
|
||||
}
|
||||
|
||||
func TestLogoutLocal(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
localLogout(t, rpClient, idp)
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
urlpkg "github.com/nais/wonderwall/pkg/handler/url"
|
||||
"github.com/nais/wonderwall/pkg/mock"
|
||||
)
|
||||
|
||||
func TestReverseProxy(t *testing.T) {
|
||||
up := newUpstream(t)
|
||||
defer up.Server.Close()
|
||||
|
||||
t.Run("without auto-login", func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
// initial request without session
|
||||
resp := get(t, rpClient, idp.RelyingPartyServer.URL)
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
assert.Equal(t, "not ok", resp.Body)
|
||||
|
||||
// acquire session
|
||||
login(t, rpClient, idp)
|
||||
|
||||
// retry request with session
|
||||
resp = get(t, rpClient, idp.RelyingPartyServer.URL)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, "ok", resp.Body)
|
||||
})
|
||||
|
||||
t.Run("with auto-login", func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.AutoLogin = true
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
// initial request without session
|
||||
target := idp.RelyingPartyServer.URL + "/"
|
||||
|
||||
resp := get(t, rpClient, target)
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
// redirect should point to local login endpoint
|
||||
loginLocation := resp.Location
|
||||
assert.Equal(t, idp.RelyingPartyServer.URL+"/oauth2/login?redirect-encoded="+urlpkg.RedirectEncoded("/"), loginLocation.String())
|
||||
|
||||
// follow redirect to local login endpoint
|
||||
resp = get(t, rpClient, loginLocation.String())
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
// redirect should point to identity provider
|
||||
authorizeLocation := resp.Location
|
||||
|
||||
authorizeEndpoint := *authorizeLocation
|
||||
authorizeEndpoint.RawQuery = ""
|
||||
assert.Equal(t, idp.OpenIDConfig.Provider().AuthorizationEndpoint(), authorizeEndpoint.String())
|
||||
|
||||
// follow redirect to identity provider for login
|
||||
resp = get(t, rpClient, authorizeLocation.String())
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
// redirect should point back to relying party
|
||||
callbackLocation := resp.Location
|
||||
|
||||
callbackEndpoint := *callbackLocation
|
||||
callbackEndpoint.RawQuery = ""
|
||||
|
||||
req := idp.GetRequest(callbackLocation.String())
|
||||
expectedCallbackURL, err := urlpkg.LoginCallbackURL(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedCallbackURL, callbackEndpoint.String())
|
||||
|
||||
// follow redirect back to relying party
|
||||
resp = get(t, rpClient, callbackLocation.String())
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
// finally, follow redirect back to original target, now with a session
|
||||
targetLocation := resp.Location
|
||||
assert.Equal(t, target, targetLocation.String())
|
||||
|
||||
resp = get(t, rpClient, targetLocation.String())
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, "ok", resp.Body)
|
||||
})
|
||||
|
||||
t.Run("with auto-login for non-GET requests", func(t *testing.T) {
|
||||
for _, method := range []string{
|
||||
http.MethodConnect,
|
||||
http.MethodDelete,
|
||||
http.MethodHead,
|
||||
http.MethodOptions,
|
||||
http.MethodPatch,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodTrace,
|
||||
} {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.AutoLogin = true
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
req, err := http.NewRequest(method, idp.RelyingPartyServer.URL, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resp, err := rpClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with auto-login and ignored paths", func(t *testing.T) {
|
||||
for pattern, tt := range map[string]struct {
|
||||
match []string
|
||||
nonMatch []string
|
||||
}{
|
||||
"/": {
|
||||
match: []string{
|
||||
"/",
|
||||
"",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/a",
|
||||
"/a/b",
|
||||
},
|
||||
},
|
||||
"/exact/match": {
|
||||
match: []string{
|
||||
"/exact/match",
|
||||
"/exact/match/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/exact/match/huh",
|
||||
},
|
||||
},
|
||||
"/allowed": {
|
||||
match: []string{
|
||||
"/allowed",
|
||||
"/allowed/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/allowe",
|
||||
"/allowed/no",
|
||||
"/not-allowed",
|
||||
"/not-allowed/allowed",
|
||||
},
|
||||
},
|
||||
"/wildcard/*": {
|
||||
match: []string{
|
||||
"/wildcard/very",
|
||||
"/wildcard/very/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/wildcard",
|
||||
"/wildcard/",
|
||||
"/wildcard/yup/nope",
|
||||
},
|
||||
},
|
||||
"/deeper/*/*": {
|
||||
match: []string{
|
||||
"/deeper/1/2",
|
||||
"/deeper/1/2/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/deeper",
|
||||
"/deeper/",
|
||||
"/deeper/1",
|
||||
"/deeper/1/",
|
||||
"/deeper/1/2/3",
|
||||
},
|
||||
},
|
||||
"/any*": {
|
||||
match: []string{
|
||||
"/any",
|
||||
"/any/",
|
||||
"/anything",
|
||||
"/anything/",
|
||||
"/anywho",
|
||||
"/anywho/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/any/thing",
|
||||
"/any/thing/",
|
||||
"/anywho/mst/ve",
|
||||
},
|
||||
},
|
||||
"/trailing/": {
|
||||
match: []string{
|
||||
"/trailing",
|
||||
"/trailing/",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/trailing/path",
|
||||
"/trailing/path/",
|
||||
},
|
||||
},
|
||||
"/nested/**": {
|
||||
match: []string{
|
||||
"/nested",
|
||||
"/nested/",
|
||||
"/nested/very",
|
||||
"/nested/very/deep",
|
||||
"/nested/very/deep/deeper",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/not/nested",
|
||||
"/not/nested/very",
|
||||
},
|
||||
},
|
||||
"/static/**/*.js": {
|
||||
match: []string{
|
||||
"/static/bundle.js",
|
||||
"/static/min/bundle.js",
|
||||
"/static/vendor/min/bundle.js",
|
||||
},
|
||||
nonMatch: []string{
|
||||
"/static",
|
||||
"/static/",
|
||||
"/static/some.css",
|
||||
"/static/min",
|
||||
"/static/min/",
|
||||
"/static/min/some.css",
|
||||
"/static/vendor/min/some.css",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(pattern, func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
cfg.AutoLogin = true
|
||||
cfg.AutoLoginIgnorePaths = []string{pattern}
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
t.Run("match", func(t *testing.T) {
|
||||
for _, path := range tt.match {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
target := idp.RelyingPartyServer.URL + path
|
||||
resp := get(t, rpClient, target)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
assert.Equal(t, "not ok", resp.Body)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-match", func(t *testing.T) {
|
||||
for _, path := range tt.nonMatch {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
target := idp.RelyingPartyServer.URL + path
|
||||
resp := get(t, rpClient, target)
|
||||
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("request with authorization header set", func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
t.Run("should be preserved if no session found", func(t *testing.T) {
|
||||
up.requestCallback = func(r *http.Request) {
|
||||
authorization := r.Header.Get("Authorization")
|
||||
assert.Equal(t, "Bearer some-authorization", authorization)
|
||||
}
|
||||
|
||||
resp := getWithHeaders(t, rpClient, idp.RelyingPartyServer.URL, map[string]string{
|
||||
"Authorization": "Bearer some-authorization",
|
||||
})
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
assert.Equal(t, "not ok", resp.Body)
|
||||
})
|
||||
|
||||
t.Run("should be overwritten if session found", func(t *testing.T) {
|
||||
// acquire session
|
||||
login(t, rpClient, idp)
|
||||
|
||||
up.requestCallback = func(r *http.Request) {
|
||||
authorization := r.Header.Get("Authorization")
|
||||
assert.NotEqual(t, "Bearer some-authorization", authorization)
|
||||
}
|
||||
|
||||
resp := getWithHeaders(t, rpClient, idp.RelyingPartyServer.URL, map[string]string{
|
||||
"Authorization": "Bearer some-authorization",
|
||||
})
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, "ok", resp.Body)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/nais/wonderwall/pkg/mock"
|
||||
"github.com/nais/wonderwall/pkg/session"
|
||||
)
|
||||
|
||||
func TestSessionRefresh(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Second
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
// get initial session info
|
||||
resp := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var data session.MetadataVerboseWithRefresh
|
||||
err := json.Unmarshal([]byte(resp.Body), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// wait until refresh cooldown has reached zero before refresh
|
||||
waitForRefreshCooldownTimer(t, idp, rpClient)
|
||||
|
||||
resp = sessionRefresh(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var refreshedData session.MetadataVerboseWithRefresh
|
||||
err = json.Unmarshal([]byte(resp.Body), &refreshedData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// session create and end times should be unchanged
|
||||
assert.WithinDuration(t, data.Session.CreatedAt, refreshedData.Session.CreatedAt, 0)
|
||||
assert.WithinDuration(t, data.Session.EndsAt, refreshedData.Session.EndsAt, 0)
|
||||
|
||||
// token expiration and refresh times should be later than before
|
||||
assert.True(t, refreshedData.Tokens.ExpireAt.After(data.Tokens.ExpireAt))
|
||||
assert.True(t, refreshedData.Tokens.RefreshedAt.After(data.Tokens.RefreshedAt))
|
||||
|
||||
allowedSkew := 5 * time.Second
|
||||
assert.WithinDuration(t, time.Now().Add(idp.ProviderHandler.TokenDuration), refreshedData.Tokens.ExpireAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now(), refreshedData.Tokens.RefreshedAt, allowedSkew)
|
||||
|
||||
sessionEndDuration := time.Duration(refreshedData.Session.EndsInSeconds) * time.Second
|
||||
// 1 second < time until session ends <= configured max session lifetime
|
||||
assert.LessOrEqual(t, sessionEndDuration, cfg.Session.MaxLifetime)
|
||||
assert.Greater(t, sessionEndDuration, time.Second)
|
||||
|
||||
tokenExpiryDuration := time.Duration(refreshedData.Tokens.ExpireInSeconds) * time.Second
|
||||
// 1 second < time until token expires <= max duration for tokens from IDP
|
||||
assert.LessOrEqual(t, tokenExpiryDuration, idp.ProviderHandler.TokenDuration)
|
||||
assert.Greater(t, tokenExpiryDuration, time.Second)
|
||||
|
||||
// 1 second < next token refresh <= seconds until token expires
|
||||
assert.LessOrEqual(t, refreshedData.Tokens.NextAutoRefreshInSeconds, refreshedData.Tokens.ExpireInSeconds)
|
||||
assert.Greater(t, refreshedData.Tokens.NextAutoRefreshInSeconds, int64(1))
|
||||
|
||||
assert.True(t, refreshedData.Tokens.RefreshCooldown)
|
||||
// 1 second < refresh cooldown <= minimum refresh interval
|
||||
assert.LessOrEqual(t, refreshedData.Tokens.RefreshCooldownSeconds, session.RefreshMinInterval)
|
||||
assert.Greater(t, refreshedData.Tokens.RefreshCooldownSeconds, int64(1))
|
||||
|
||||
assert.True(t, data.Session.Active)
|
||||
assert.True(t, refreshedData.Session.Active)
|
||||
|
||||
assert.True(t, data.Session.TimeoutAt.IsZero())
|
||||
assert.True(t, refreshedData.Session.TimeoutAt.IsZero())
|
||||
|
||||
assert.Equal(t, int64(-1), data.Session.TimeoutInSeconds)
|
||||
assert.Equal(t, int64(-1), refreshedData.Session.TimeoutInSeconds)
|
||||
}
|
||||
|
||||
func TestSessionRefresh_Disabled(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = false
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Second
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := sessionRefresh(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestSessionRefresh_WithInactivity(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
cfg.Session.Inactivity = true
|
||||
cfg.Session.InactivityTimeout = 10 * time.Minute
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Second
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
// get initial session info
|
||||
resp := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var data session.MetadataVerboseWithRefresh
|
||||
err := json.Unmarshal([]byte(resp.Body), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// wait until refresh cooldown has reached zero before refresh
|
||||
waitForRefreshCooldownTimer(t, idp, rpClient)
|
||||
|
||||
resp = sessionRefresh(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var refreshedData session.MetadataVerboseWithRefresh
|
||||
err = json.Unmarshal([]byte(resp.Body), &refreshedData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
maxDelta := 5 * time.Second
|
||||
|
||||
assert.True(t, data.Session.Active)
|
||||
assert.True(t, refreshedData.Session.Active)
|
||||
|
||||
assert.False(t, data.Session.TimeoutAt.IsZero())
|
||||
assert.False(t, refreshedData.Session.TimeoutAt.IsZero())
|
||||
|
||||
expectedTimeoutAt := time.Now().Add(cfg.Session.InactivityTimeout)
|
||||
assert.WithinDuration(t, expectedTimeoutAt, data.Session.TimeoutAt, maxDelta)
|
||||
assert.WithinDuration(t, expectedTimeoutAt, refreshedData.Session.TimeoutAt, maxDelta)
|
||||
|
||||
assert.True(t, refreshedData.Session.TimeoutAt.After(data.Session.TimeoutAt))
|
||||
|
||||
previousTimeoutDuration := time.Duration(data.Session.TimeoutInSeconds) * time.Second
|
||||
assert.WithinDuration(t, expectedTimeoutAt, time.Now().Add(previousTimeoutDuration), maxDelta)
|
||||
|
||||
refreshedTimeoutDuration := time.Duration(refreshedData.Session.TimeoutInSeconds) * time.Second
|
||||
assert.WithinDuration(t, expectedTimeoutAt, time.Now().Add(refreshedTimeoutDuration), maxDelta)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/nais/wonderwall/pkg/mock"
|
||||
"github.com/nais/wonderwall/pkg/session"
|
||||
)
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Minute
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var data session.MetadataVerbose
|
||||
err := json.Unmarshal([]byte(resp.Body), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
allowedSkew := 5 * time.Second
|
||||
assert.WithinDuration(t, time.Now(), data.Session.CreatedAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now().Add(cfg.Session.MaxLifetime), data.Session.EndsAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now().Add(idp.ProviderHandler.TokenDuration), data.Tokens.ExpireAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now(), data.Tokens.RefreshedAt, allowedSkew)
|
||||
|
||||
sessionEndDuration := time.Duration(data.Session.EndsInSeconds) * time.Second
|
||||
// 1 second < time until session ends <= configured max session lifetime
|
||||
assert.LessOrEqual(t, sessionEndDuration, cfg.Session.MaxLifetime)
|
||||
assert.Greater(t, sessionEndDuration, time.Second)
|
||||
|
||||
tokenExpiryDuration := time.Duration(data.Tokens.ExpireInSeconds) * time.Second
|
||||
// 1 second < time until token expires <= max duration for tokens from IDP
|
||||
assert.LessOrEqual(t, tokenExpiryDuration, idp.ProviderHandler.TokenDuration)
|
||||
assert.Greater(t, tokenExpiryDuration, time.Second)
|
||||
|
||||
assert.True(t, data.Session.Active)
|
||||
assert.True(t, data.Session.TimeoutAt.IsZero())
|
||||
assert.Equal(t, int64(-1), data.Session.TimeoutInSeconds)
|
||||
}
|
||||
|
||||
func TestSession_WithInactivity(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
cfg.Session.Inactivity = true
|
||||
cfg.Session.InactivityTimeout = 10 * time.Minute
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var data session.MetadataVerbose
|
||||
err := json.Unmarshal([]byte(resp.Body), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
maxDelta := 5 * time.Second
|
||||
|
||||
assert.True(t, data.Session.Active)
|
||||
assert.False(t, data.Session.TimeoutAt.IsZero())
|
||||
|
||||
expectedTimeoutAt := time.Now().Add(cfg.Session.InactivityTimeout)
|
||||
assert.WithinDuration(t, expectedTimeoutAt, data.Session.TimeoutAt, maxDelta)
|
||||
|
||||
actualTimeoutDuration := time.Duration(data.Session.TimeoutInSeconds) * time.Second
|
||||
assert.WithinDuration(t, expectedTimeoutAt, time.Now().Add(actualTimeoutDuration), maxDelta)
|
||||
}
|
||||
|
||||
func TestSession_WithRefresh(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Minute
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
resp := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var data session.MetadataVerboseWithRefresh
|
||||
err := json.Unmarshal([]byte(resp.Body), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
allowedSkew := 5 * time.Second
|
||||
assert.WithinDuration(t, time.Now(), data.Session.CreatedAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now().Add(cfg.Session.MaxLifetime), data.Session.EndsAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now().Add(idp.ProviderHandler.TokenDuration), data.Tokens.ExpireAt, allowedSkew)
|
||||
assert.WithinDuration(t, time.Now(), data.Tokens.RefreshedAt, allowedSkew)
|
||||
|
||||
sessionEndDuration := time.Duration(data.Session.EndsInSeconds) * time.Second
|
||||
// 1 second < time until session ends <= configured max session lifetime
|
||||
assert.LessOrEqual(t, sessionEndDuration, cfg.Session.MaxLifetime)
|
||||
assert.Greater(t, sessionEndDuration, time.Second)
|
||||
|
||||
tokenExpiryDuration := time.Duration(data.Tokens.ExpireInSeconds) * time.Second
|
||||
// 1 second < time until token expires <= max duration for tokens from IDP
|
||||
assert.LessOrEqual(t, tokenExpiryDuration, idp.ProviderHandler.TokenDuration)
|
||||
assert.Greater(t, tokenExpiryDuration, time.Second)
|
||||
|
||||
// 1 second < next token refresh <= seconds until token expires
|
||||
assert.LessOrEqual(t, data.Tokens.NextAutoRefreshInSeconds, data.Tokens.ExpireInSeconds)
|
||||
assert.Greater(t, data.Tokens.NextAutoRefreshInSeconds, int64(1))
|
||||
|
||||
assert.True(t, data.Tokens.RefreshCooldown)
|
||||
// 1 second < refresh cooldown <= minimum refresh interval
|
||||
assert.LessOrEqual(t, data.Tokens.RefreshCooldownSeconds, session.RefreshMinInterval)
|
||||
assert.Greater(t, data.Tokens.RefreshCooldownSeconds, int64(1))
|
||||
|
||||
assert.True(t, data.Session.Active)
|
||||
assert.True(t, data.Session.TimeoutAt.IsZero())
|
||||
assert.Equal(t, int64(-1), data.Session.TimeoutInSeconds)
|
||||
}
|
||||
Reference in New Issue
Block a user