mirror of https://github.com/dexidp/dex.git
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.
751 lines
22 KiB
751 lines
22 KiB
package server |
|
|
|
import ( |
|
"crypto" |
|
"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() |
|
authReq := &storage.AuthRequest{ConnectorID: "conn1"} |
|
|
|
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, httptest.NewRecorder(), r, authReq)) |
|
}) |
|
|
|
t.Run("no cookie", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
r := httptest.NewRequest(http.MethodGet, "/", nil) |
|
assert.Nil(t, s.getValidAuthSession(ctx, httptest.NewRecorder(), r, authReq)) |
|
}) |
|
|
|
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"}) |
|
w := httptest.NewRecorder() |
|
assert.Nil(t, s.getValidAuthSession(ctx, w, r, authReq)) |
|
// Cookie should be cleared. |
|
assert.Equal(t, -1, w.Result().Cookies()[0].MaxAge) |
|
}) |
|
|
|
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")}) |
|
w := httptest.NewRecorder() |
|
assert.Nil(t, s.getValidAuthSession(ctx, w, r, authReq)) |
|
// Cookie should be cleared. |
|
assert.Equal(t, -1, w.Result().Cookies()[0].MaxAge) |
|
}) |
|
|
|
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, httptest.NewRecorder(), r, authReq) |
|
require.NotNil(t, result) |
|
assert.Equal(t, "user1", result.UserID) |
|
assert.Equal(t, "conn1", result.ConnectorID) |
|
}) |
|
|
|
t.Run("connector mismatch", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
now := s.now() |
|
nonce := "test-nonce-conn" |
|
|
|
session := storage.AuthSession{ |
|
UserID: "user1", |
|
ConnectorID: "ldap", |
|
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", "ldap", nonce)}) |
|
|
|
githubReq := &storage.AuthRequest{ConnectorID: "github"} |
|
assert.Nil(t, s.getValidAuthSession(ctx, httptest.NewRecorder(), r, githubReq)) |
|
}) |
|
|
|
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")}) |
|
|
|
conn2Req := &storage.AuthRequest{ConnectorID: "conn2"} |
|
w := httptest.NewRecorder() |
|
assert.Nil(t, s.getValidAuthSession(ctx, w, r, conn2Req)) |
|
assert.Equal(t, -1, w.Result().Cookies()[0].MaxAge) |
|
}) |
|
|
|
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)}) |
|
|
|
conn3Req := &storage.AuthRequest{ConnectorID: "conn3"} |
|
w := httptest.NewRecorder() |
|
assert.Nil(t, s.getValidAuthSession(ctx, w, r, conn3Req)) |
|
assert.Equal(t, -1, w.Result().Cookies()[0].MaxAge) |
|
|
|
// 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)}) |
|
|
|
conn4Req := &storage.AuthRequest{ConnectorID: "conn4"} |
|
w := httptest.NewRecorder() |
|
assert.Nil(t, s.getValidAuthSession(ctx, w, r, conn4Req)) |
|
assert.Equal(t, -1, w.Result().Cookies()[0].MaxAge) |
|
|
|
// 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()) |
|
}) |
|
} |
|
|
|
// setupSessionLoginFixture creates the necessary storage objects for trySessionLogin tests. |
|
func setupSessionLoginFixture(t *testing.T, s *Server) storage.AuthRequest { |
|
t.Helper() |
|
ctx := t.Context() |
|
now := s.now() |
|
|
|
require.NoError(t, s.storage.CreateAuthSession(ctx, storage.AuthSession{ |
|
UserID: "user-1", |
|
ConnectorID: "mock", |
|
Nonce: "test-nonce", |
|
ClientStates: map[string]*storage.ClientAuthState{ |
|
"client-1": { |
|
Active: true, |
|
ExpiresAt: now.Add(24 * time.Hour), |
|
LastActivity: now.Add(-1 * time.Minute), |
|
}, |
|
}, |
|
CreatedAt: now.Add(-30 * time.Minute), |
|
LastActivity: now.Add(-1 * time.Minute), |
|
IPAddress: "127.0.0.1", |
|
UserAgent: "test", |
|
})) |
|
|
|
require.NoError(t, s.storage.CreateUserIdentity(ctx, storage.UserIdentity{ |
|
UserID: "user-1", |
|
ConnectorID: "mock", |
|
Claims: storage.Claims{ |
|
UserID: "user-1", |
|
Username: "testuser", |
|
Email: "test@example.com", |
|
}, |
|
Consents: map[string][]string{"client-1": {"openid", "email"}}, |
|
CreatedAt: now.Add(-1 * time.Hour), |
|
LastLogin: now.Add(-30 * time.Minute), |
|
})) |
|
|
|
authReq := storage.AuthRequest{ |
|
ID: storage.NewID(), |
|
ClientID: "client-1", |
|
ConnectorID: "mock", |
|
Scopes: []string{"openid", "email"}, |
|
RedirectURI: "http://localhost/callback", |
|
MaxAge: -1, |
|
HMACKey: storage.NewHMACKey(crypto.SHA256), |
|
Expiry: now.Add(10 * time.Minute), |
|
} |
|
require.NoError(t, s.storage.CreateAuthRequest(ctx, authReq)) |
|
return authReq |
|
} |
|
|
|
func sessionCookieRequest(userID, connectorID, nonce string) *http.Request { |
|
r := httptest.NewRequest(http.MethodGet, "/", nil) |
|
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue(userID, connectorID, nonce)}) |
|
return r |
|
} |
|
|
|
func TestTrySessionLogin(t *testing.T) { |
|
ctx := t.Context() |
|
|
|
t.Run("no session", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
authReq := storage.AuthRequest{ConnectorID: "mock"} |
|
r := httptest.NewRequest(http.MethodGet, "/", nil) |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.False(t, ok) |
|
}) |
|
|
|
t.Run("successful login with skipApproval", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
s.skipApproval = true |
|
authReq := setupSessionLoginFixture(t, s) |
|
|
|
r := sessionCookieRequest("user-1", "mock", "test-nonce") |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.True(t, ok) |
|
}) |
|
|
|
t.Run("successful login redirects to approval", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
s.skipApproval = false |
|
authReq := setupSessionLoginFixture(t, s) |
|
authReq.ForceApprovalPrompt = true |
|
|
|
require.NoError(t, s.storage.UpdateAuthRequest(ctx, authReq.ID, func(a storage.AuthRequest) (storage.AuthRequest, error) { |
|
a.ForceApprovalPrompt = true |
|
return a, nil |
|
})) |
|
|
|
r := sessionCookieRequest("user-1", "mock", "test-nonce") |
|
w := httptest.NewRecorder() |
|
|
|
redirectURL, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.True(t, ok) |
|
assert.Contains(t, redirectURL, "/approval") |
|
assert.Contains(t, redirectURL, "req="+authReq.ID) |
|
}) |
|
|
|
t.Run("skips approval when consent already given", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
s.skipApproval = false |
|
authReq := setupSessionLoginFixture(t, s) |
|
|
|
r := sessionCookieRequest("user-1", "mock", "test-nonce") |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.True(t, ok) |
|
}) |
|
|
|
t.Run("connector mismatch returns false", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
authReq := setupSessionLoginFixture(t, s) |
|
authReq.ConnectorID = "github" |
|
|
|
r := sessionCookieRequest("user-1", "mock", "test-nonce") |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.False(t, ok) |
|
}) |
|
|
|
t.Run("no client state for requested client", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
authReq := setupSessionLoginFixture(t, s) |
|
authReq.ClientID = "unknown-client" |
|
|
|
r := sessionCookieRequest("user-1", "mock", "test-nonce") |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.False(t, ok) |
|
}) |
|
|
|
t.Run("expired client state returns false", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
now := s.now() |
|
|
|
require.NoError(t, s.storage.CreateAuthSession(t.Context(), storage.AuthSession{ |
|
UserID: "user-exp", |
|
ConnectorID: "mock", |
|
Nonce: "nonce-exp", |
|
ClientStates: map[string]*storage.ClientAuthState{ |
|
"client-1": { |
|
Active: true, |
|
ExpiresAt: now.Add(-1 * time.Hour), |
|
}, |
|
}, |
|
CreatedAt: now.Add(-2 * time.Hour), |
|
LastActivity: now.Add(-1 * time.Minute), |
|
})) |
|
|
|
require.NoError(t, s.storage.CreateUserIdentity(t.Context(), storage.UserIdentity{ |
|
UserID: "user-exp", |
|
ConnectorID: "mock", |
|
Claims: storage.Claims{UserID: "user-exp"}, |
|
Consents: make(map[string][]string), |
|
CreatedAt: now, |
|
LastLogin: now, |
|
})) |
|
|
|
authReq := storage.AuthRequest{ |
|
ID: storage.NewID(), |
|
ClientID: "client-1", |
|
ConnectorID: "mock", |
|
MaxAge: -1, |
|
HMACKey: storage.NewHMACKey(crypto.SHA256), |
|
Expiry: now.Add(10 * time.Minute), |
|
} |
|
require.NoError(t, s.storage.CreateAuthRequest(t.Context(), authReq)) |
|
|
|
r := sessionCookieRequest("user-exp", "mock", "nonce-exp") |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.False(t, ok) |
|
}) |
|
|
|
t.Run("updates session activity", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
s.skipApproval = true |
|
authReq := setupSessionLoginFixture(t, s) |
|
|
|
r := sessionCookieRequest("user-1", "mock", "test-nonce") |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
require.True(t, ok) |
|
|
|
session, err := s.storage.GetAuthSession(ctx, "user-1", "mock") |
|
require.NoError(t, err) |
|
assert.Equal(t, s.now(), session.LastActivity) |
|
}) |
|
} |
|
|
|
// setupSessionWithIdentity creates an AuthSession, UserIdentity, and AuthRequest in storage |
|
// for use in trySessionLogin tests. Returns the authReq. |
|
func setupSessionWithIdentity(t *testing.T, s *Server, now time.Time, lastLogin time.Time) storage.AuthRequest { |
|
t.Helper() |
|
ctx := t.Context() |
|
nonce := "test-nonce" |
|
|
|
session := 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(-1 * time.Minute), |
|
}, |
|
}, |
|
CreatedAt: now.Add(-30 * time.Minute), |
|
LastActivity: now.Add(-1 * time.Minute), |
|
IPAddress: "127.0.0.1", |
|
UserAgent: "test", |
|
} |
|
require.NoError(t, s.storage.CreateAuthSession(ctx, session)) |
|
|
|
ui := storage.UserIdentity{ |
|
UserID: "user-1", |
|
ConnectorID: "mock", |
|
Claims: storage.Claims{ |
|
UserID: "user-1", |
|
Username: "testuser", |
|
Email: "test@example.com", |
|
}, |
|
Consents: make(map[string][]string), |
|
CreatedAt: now.Add(-1 * time.Hour), |
|
LastLogin: lastLogin, |
|
} |
|
require.NoError(t, s.storage.CreateUserIdentity(ctx, ui)) |
|
|
|
authReq := storage.AuthRequest{ |
|
ID: storage.NewID(), |
|
ClientID: "client-1", |
|
ConnectorID: "mock", |
|
Scopes: []string{"openid"}, |
|
RedirectURI: "http://localhost/callback", |
|
MaxAge: -1, |
|
HMACKey: storage.NewHMACKey(crypto.SHA256), |
|
Expiry: now.Add(10 * time.Minute), |
|
} |
|
require.NoError(t, s.storage.CreateAuthRequest(ctx, authReq)) |
|
|
|
return authReq |
|
} |
|
|
|
func TestTrySessionLogin_MaxAge(t *testing.T) { |
|
ctx := t.Context() |
|
|
|
t.Run("max_age not specified, session reused", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
now := s.now() |
|
|
|
authReq := setupSessionWithIdentity(t, s, now, now.Add(-2*time.Hour)) |
|
authReq.MaxAge = -1 // not specified |
|
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil) |
|
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user-1", "mock", "test-nonce")}) |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.True(t, ok, "session should be reused when max_age is not specified") |
|
}) |
|
|
|
t.Run("max_age satisfied, session reused", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
now := s.now() |
|
|
|
// User logged in 10 minutes ago, max_age=3600 (1 hour) |
|
authReq := setupSessionWithIdentity(t, s, now, now.Add(-10*time.Minute)) |
|
authReq.MaxAge = 3600 |
|
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil) |
|
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user-1", "mock", "test-nonce")}) |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.True(t, ok, "session should be reused when max_age is satisfied") |
|
}) |
|
|
|
t.Run("max_age exceeded, force re-auth", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
now := s.now() |
|
|
|
// User logged in 2 hours ago, max_age=3600 (1 hour) |
|
authReq := setupSessionWithIdentity(t, s, now, now.Add(-2*time.Hour)) |
|
authReq.MaxAge = 3600 |
|
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil) |
|
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user-1", "mock", "test-nonce")}) |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.False(t, ok, "session should NOT be reused when max_age is exceeded") |
|
}) |
|
|
|
t.Run("max_age=0, always force re-auth", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
now := s.now() |
|
|
|
// User logged in 1 second ago, max_age=0 |
|
authReq := setupSessionWithIdentity(t, s, now, now.Add(-1*time.Second)) |
|
authReq.MaxAge = 0 |
|
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil) |
|
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user-1", "mock", "test-nonce")}) |
|
w := httptest.NewRecorder() |
|
|
|
_, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
assert.False(t, ok, "max_age=0 should always force re-authentication") |
|
}) |
|
|
|
t.Run("auth_time is set from UserIdentity.LastLogin", func(t *testing.T) { |
|
s := newTestSessionServer(t) |
|
s.skipApproval = false |
|
now := s.now() |
|
lastLogin := now.Add(-10 * time.Minute) |
|
|
|
authReq := setupSessionWithIdentity(t, s, now, lastLogin) |
|
authReq.ForceApprovalPrompt = true // force approval so AuthRequest is not deleted |
|
|
|
require.NoError(t, s.storage.UpdateAuthRequest(ctx, authReq.ID, func(a storage.AuthRequest) (storage.AuthRequest, error) { |
|
a.ForceApprovalPrompt = true |
|
return a, nil |
|
})) |
|
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil) |
|
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user-1", "mock", "test-nonce")}) |
|
w := httptest.NewRecorder() |
|
|
|
redirectURL, ok := s.trySessionLogin(ctx, r, w, &authReq) |
|
require.True(t, ok) |
|
assert.Contains(t, redirectURL, "/approval") |
|
|
|
// Verify AuthTime was set on the auth request. |
|
updated, err := s.storage.GetAuthRequest(ctx, authReq.ID) |
|
require.NoError(t, err) |
|
assert.Equal(t, lastLogin.Unix(), updated.AuthTime.Unix()) |
|
}) |
|
} |
|
|
|
func TestParseAuthRequest_PromptAndMaxAge(t *testing.T) { |
|
t.Run("prompt=consent sets ForceApprovalPrompt", func(t *testing.T) { |
|
authReq := storage.AuthRequest{ |
|
Prompt: "consent", |
|
ForceApprovalPrompt: true, |
|
} |
|
assert.True(t, authReq.ForceApprovalPrompt) |
|
assert.Equal(t, "consent", authReq.Prompt) |
|
}) |
|
|
|
t.Run("max_age default is -1", func(t *testing.T) { |
|
authReq := storage.AuthRequest{ |
|
MaxAge: -1, |
|
} |
|
assert.Equal(t, -1, authReq.MaxAge) |
|
}) |
|
}
|
|
|