OpenID Connect (OIDC) identity and OAuth 2.0 provider with pluggable connectors
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

345 lines
9.8 KiB

package server
import (
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dexidp/dex/storage"
"github.com/dexidp/dex/storage/memory"
)
func newTestSessionServer(t *testing.T) *Server {
t.Helper()
now := time.Date(2026, 3, 16, 12, 0, 0, 0, time.UTC)
issuerURL, err := url.Parse("https://example.com/dex")
require.NoError(t, err)
return &Server{
storage: memory.New(nil),
logger: slog.Default(),
now: func() time.Time { return now },
sessionConfig: &SessionConfig{
CookieName: "dex_session",
AbsoluteLifetime: 24 * time.Hour,
ValidIfNotUsedFor: 1 * time.Hour,
},
issuerURL: *issuerURL,
}
}
func TestSetSessionCookie(t *testing.T) {
s := newTestSessionServer(t)
w := httptest.NewRecorder()
s.setSessionCookie(w, "user1", "conn1", "nonce123", false)
cookies := w.Result().Cookies()
require.Len(t, cookies, 1)
c := cookies[0]
assert.Equal(t, "dex_session", c.Name)
assert.Equal(t, sessionCookieValue("user1", "conn1", "nonce123"), c.Value)
assert.Equal(t, "/dex", c.Path)
assert.True(t, c.HttpOnly)
assert.True(t, c.Secure)
assert.Equal(t, http.SameSiteLaxMode, c.SameSite)
}
func TestSetSessionCookie_HTTP(t *testing.T) {
s := newTestSessionServer(t)
u, _ := url.Parse("http://localhost:5556/dex")
s.issuerURL = *u
w := httptest.NewRecorder()
s.setSessionCookie(w, "user1", "conn1", "nonce123", false)
cookies := w.Result().Cookies()
require.Len(t, cookies, 1)
assert.False(t, cookies[0].Secure)
}
func TestClearSessionCookie(t *testing.T) {
s := newTestSessionServer(t)
w := httptest.NewRecorder()
s.clearSessionCookie(w)
cookies := w.Result().Cookies()
require.Len(t, cookies, 1)
assert.Equal(t, -1, cookies[0].MaxAge)
assert.Equal(t, "", cookies[0].Value)
}
func TestSessionCookieValueRoundtrip(t *testing.T) {
tests := []struct {
name string
userID string
connectorID string
nonce string
}{
{"simple", "user1", "ldap", "abc123"},
{"with special chars", "user@example.com", "oidc-provider", "xyz789"},
{"unicode", "юзер", "коннектор", "nonce"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
value := sessionCookieValue(tt.userID, tt.connectorID, tt.nonce)
gotUser, gotConn, gotNonce, err := parseSessionCookie(value)
require.NoError(t, err)
assert.Equal(t, tt.userID, gotUser)
assert.Equal(t, tt.connectorID, gotConn)
assert.Equal(t, tt.nonce, gotNonce)
})
}
}
func TestParseSessionCookie_Invalid(t *testing.T) {
//nolint:dogsled // only for tests
_, _, _, err := parseSessionCookie("invalid")
assert.Error(t, err)
//nolint:dogsled // only for tests
_, _, _, err = parseSessionCookie("a.b")
assert.Error(t, err)
}
func TestGetValidAuthSession(t *testing.T) {
ctx := t.Context()
t.Run("no session config", func(t *testing.T) {
s := newTestSessionServer(t)
s.sessionConfig = nil
r := httptest.NewRequest(http.MethodGet, "/", nil)
assert.Nil(t, s.getValidAuthSession(ctx, r))
})
t.Run("no cookie", func(t *testing.T) {
s := newTestSessionServer(t)
r := httptest.NewRequest(http.MethodGet, "/", nil)
assert.Nil(t, s.getValidAuthSession(ctx, r))
})
t.Run("invalid cookie format", func(t *testing.T) {
s := newTestSessionServer(t)
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: "invalid-format"})
assert.Nil(t, s.getValidAuthSession(ctx, r))
})
t.Run("session not found", func(t *testing.T) {
s := newTestSessionServer(t)
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("nouser", "noconn", "nonce")})
assert.Nil(t, s.getValidAuthSession(ctx, r))
})
t.Run("valid session", func(t *testing.T) {
s := newTestSessionServer(t)
now := s.now()
nonce := "test-nonce"
session := storage.AuthSession{
UserID: "user1",
ConnectorID: "conn1",
Nonce: nonce,
ClientStates: map[string]*storage.ClientAuthState{},
CreatedAt: now.Add(-30 * time.Minute),
LastActivity: now.Add(-5 * time.Minute),
IPAddress: "127.0.0.1",
UserAgent: "test",
}
require.NoError(t, s.storage.CreateAuthSession(ctx, session))
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user1", "conn1", nonce)})
result := s.getValidAuthSession(ctx, r)
require.NotNil(t, result)
assert.Equal(t, "user1", result.UserID)
assert.Equal(t, "conn1", result.ConnectorID)
})
t.Run("nonce mismatch", func(t *testing.T) {
s := newTestSessionServer(t)
now := s.now()
session := storage.AuthSession{
UserID: "user2",
ConnectorID: "conn2",
Nonce: "correct-nonce",
ClientStates: map[string]*storage.ClientAuthState{},
CreatedAt: now.Add(-30 * time.Minute),
LastActivity: now.Add(-5 * time.Minute),
IPAddress: "127.0.0.1",
UserAgent: "test",
}
require.NoError(t, s.storage.CreateAuthSession(ctx, session))
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user2", "conn2", "wrong-nonce")})
assert.Nil(t, s.getValidAuthSession(ctx, r))
})
t.Run("expired absolute lifetime", func(t *testing.T) {
s := newTestSessionServer(t)
now := s.now()
nonce := "expired-nonce"
session := storage.AuthSession{
UserID: "user3",
ConnectorID: "conn3",
Nonce: nonce,
ClientStates: map[string]*storage.ClientAuthState{},
CreatedAt: now.Add(-25 * time.Hour),
LastActivity: now.Add(-1 * time.Minute),
IPAddress: "127.0.0.1",
UserAgent: "test",
}
require.NoError(t, s.storage.CreateAuthSession(ctx, session))
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user3", "conn3", nonce)})
assert.Nil(t, s.getValidAuthSession(ctx, r))
// Session should be deleted.
_, err := s.storage.GetAuthSession(ctx, "user3", "conn3")
assert.ErrorIs(t, err, storage.ErrNotFound)
})
t.Run("expired idle timeout", func(t *testing.T) {
s := newTestSessionServer(t)
now := s.now()
nonce := "idle-nonce"
session := storage.AuthSession{
UserID: "user4",
ConnectorID: "conn4",
Nonce: nonce,
ClientStates: map[string]*storage.ClientAuthState{},
CreatedAt: now.Add(-2 * time.Hour),
LastActivity: now.Add(-2 * time.Hour),
IPAddress: "127.0.0.1",
UserAgent: "test",
}
require.NoError(t, s.storage.CreateAuthSession(ctx, session))
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user4", "conn4", nonce)})
assert.Nil(t, s.getValidAuthSession(ctx, r))
// Session should be deleted.
_, err := s.storage.GetAuthSession(ctx, "user4", "conn4")
assert.ErrorIs(t, err, storage.ErrNotFound)
})
}
func TestCreateOrUpdateAuthSession(t *testing.T) {
ctx := t.Context()
t.Run("create new session", func(t *testing.T) {
s := newTestSessionServer(t)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", nil)
authReq := storage.AuthRequest{
ID: "auth-1",
ClientID: "client-1",
Claims: storage.Claims{UserID: "user-1"},
ConnectorID: "mock",
}
err := s.createOrUpdateAuthSession(ctx, r, w, authReq, false)
require.NoError(t, err)
// Cookie should be set.
cookies := w.Result().Cookies()
require.Len(t, cookies, 1)
userID, connectorID, nonce, err := parseSessionCookie(cookies[0].Value)
require.NoError(t, err)
assert.Equal(t, "user-1", userID)
assert.Equal(t, "mock", connectorID)
assert.NotEmpty(t, nonce)
// Session should exist in storage.
session, err := s.storage.GetAuthSession(ctx, "user-1", "mock")
require.NoError(t, err)
assert.Equal(t, "user-1", session.UserID)
assert.Equal(t, "mock", session.ConnectorID)
require.Contains(t, session.ClientStates, "client-1")
assert.True(t, session.ClientStates["client-1"].Active)
})
t.Run("update existing session", func(t *testing.T) {
s := newTestSessionServer(t)
now := s.now()
nonce := "existing-nonce"
existingSession := storage.AuthSession{
UserID: "user-1",
ConnectorID: "mock",
Nonce: nonce,
ClientStates: map[string]*storage.ClientAuthState{
"client-1": {
Active: true,
ExpiresAt: now.Add(24 * time.Hour),
LastActivity: now.Add(-10 * time.Minute),
},
},
CreatedAt: now.Add(-30 * time.Minute),
LastActivity: now.Add(-10 * time.Minute),
IPAddress: "127.0.0.1",
UserAgent: "test",
}
require.NoError(t, s.storage.CreateAuthSession(ctx, existingSession))
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", nil)
authReq := storage.AuthRequest{
ID: "auth-2",
ClientID: "client-2",
Claims: storage.Claims{UserID: "user-1"},
ConnectorID: "mock",
}
err := s.createOrUpdateAuthSession(ctx, r, w, authReq, false)
require.NoError(t, err)
// Cookie should be set with existing nonce.
cookies := w.Result().Cookies()
require.Len(t, cookies, 1)
_, _, gotNonce, err := parseSessionCookie(cookies[0].Value)
require.NoError(t, err)
assert.Equal(t, nonce, gotNonce)
// Session should have both clients.
session, err := s.storage.GetAuthSession(ctx, "user-1", "mock")
require.NoError(t, err)
assert.Len(t, session.ClientStates, 2)
assert.Contains(t, session.ClientStates, "client-1")
assert.Contains(t, session.ClientStates, "client-2")
})
t.Run("nil session config", func(t *testing.T) {
s := newTestSessionServer(t)
s.sessionConfig = nil
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", nil)
err := s.createOrUpdateAuthSession(ctx, r, w, storage.AuthRequest{}, false)
assert.NoError(t, err)
assert.Empty(t, w.Result().Cookies())
})
}