Skip to content

Commit ab64ed7

Browse files
authored
feat(server): one session per signed-in browser (#4951)
Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
1 parent ba3915f commit ab64ed7

59 files changed

Lines changed: 1914 additions & 1468 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/v2/api.pb.go

Lines changed: 402 additions & 399 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v2/api.proto

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,19 @@ message VerifyPasswordResp {
304304
// The user_id and connector_id are on the parent AuthSession message.
305305
message ClientAuthState {
306306
string client_id = 1;
307-
bool active = 2;
308-
int64 expires_at = 3;
309-
int64 last_activity = 4;
310-
int64 last_token_issued_at = 5;
307+
int64 authenticated_at = 2;
308+
int64 last_activity = 3;
309+
int64 last_token_issued_at = 4;
310+
// Whether this client was reached through another client's SSO sharing rather
311+
// than by authenticating directly.
312+
bool via_sso = 5;
311313
}
312314

313315
// AuthSession represents a user's authentication session.
314316
message AuthSession {
317+
// Random identifier of the session, published to clients as the "sid" claim. One
318+
// signed-in browser is one session, so a user has as many as they have devices.
319+
string id = 10;
315320
string user_id = 1;
316321
string connector_id = 2;
317322
repeated ClientAuthState client_states = 3;
@@ -325,8 +330,7 @@ message AuthSession {
325330

326331
// GetAuthSessionReq is a request to retrieve an auth session.
327332
message GetAuthSessionReq {
328-
string user_id = 1;
329-
string connector_id = 2;
333+
string id = 1;
330334
}
331335

332336
// GetAuthSessionResp returns the auth session details.
@@ -338,6 +342,8 @@ message GetAuthSessionResp {
338342
message ListAuthSessionsReq {
339343
// Optional filter: if set, only sessions for this user are returned.
340344
string user_id = 1;
345+
// Optional filter: if set, only sessions from this connector are returned.
346+
string connector_id = 2;
341347
}
342348

343349
// ListAuthSessionsResp returns a list of auth sessions.
@@ -348,8 +354,7 @@ message ListAuthSessionsResp {
348354
// DeleteAuthSessionReq is a request to delete an auth session.
349355
// Deleting a session also revokes all associated refresh tokens (consistent with logout behavior).
350356
message DeleteAuthSessionReq {
351-
string user_id = 1;
352-
string connector_id = 2;
357+
string id = 1;
353358
}
354359

355360
// DeleteAuthSessionResp returns the result of deleting an auth session.
@@ -567,9 +572,9 @@ service Dex {
567572
rpc RevokeRefresh(RevokeRefreshReq) returns (RevokeRefreshResp) {};
568573
// VerifyPassword returns whether a password matches a hash for a specific email or not.
569574
rpc VerifyPassword(VerifyPasswordReq) returns (VerifyPasswordResp) {};
570-
// GetAuthSession returns an auth session by user and connector ID.
575+
// GetAuthSession returns an auth session by its ID.
571576
rpc GetAuthSession(GetAuthSessionReq) returns (GetAuthSessionResp) {};
572-
// ListAuthSessions lists auth sessions, optionally filtered by user_id.
577+
// ListAuthSessions lists auth sessions, optionally filtered by user and connector.
573578
rpc ListAuthSessions(ListAuthSessionsReq) returns (ListAuthSessionsResp) {};
574579
// DeleteAuthSession deletes an auth session and revokes associated refresh tokens.
575580
rpc DeleteAuthSession(DeleteAuthSessionReq) returns (DeleteAuthSessionResp) {};

api/v2/api_grpc.pb.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/example-app/server/admin.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,15 @@ func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
230230
// connector gave the user, refresh tokens under the encoded sub claim
231231
// that ends up in tokens.
232232
if data.UserID != "" {
233-
if resp, err := s.admin.api.ListAuthSessions(ctx, &api.ListAuthSessionsReq{UserId: data.UserID}); err == nil {
233+
req := &api.ListAuthSessionsReq{UserId: data.UserID, ConnectorId: data.ConnectorID}
234+
if resp, err := s.admin.api.ListAuthSessions(ctx, req); err == nil {
234235
for _, sess := range resp.Sessions {
235236
data.Sessions = append(data.Sessions, AdminSession{
237+
ID: sess.Id,
236238
UserID: sess.UserId,
237239
ConnectorID: sess.ConnectorId,
238240
IPAddress: sess.IpAddress,
241+
UserAgent: sess.UserAgent,
239242
Created: epochText(sess.CreatedAt),
240243
Expires: epochText(sess.AbsoluteExpiry),
241244
})
@@ -446,9 +449,11 @@ func (s *Server) handleAdminRevokeRefresh(w http.ResponseWriter, r *http.Request
446449
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
447450
defer cancel()
448451

452+
// The API keys refresh tokens by the sub claim, not by the user id the
453+
// connector gave — the same encoding the listing above uses.
449454
userID, clientID := r.FormValue("user_id"), r.FormValue("client_id")
450455
resp, err := s.admin.api.RevokeRefresh(ctx, &api.RevokeRefreshReq{
451-
UserId: userID,
456+
UserId: idTokenSubject(userID, r.FormValue("connector_id")),
452457
ClientId: clientID,
453458
})
454459
switch {
@@ -680,18 +685,16 @@ func (s *Server) handleAdminDeleteSession(w http.ResponseWriter, r *http.Request
680685
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
681686
defer cancel()
682687

683-
userID := r.FormValue("user_id")
684-
resp, err := s.admin.api.DeleteAuthSession(ctx, &api.DeleteAuthSessionReq{
685-
UserId: userID,
686-
ConnectorId: r.FormValue("connector_id"),
687-
})
688+
// One session is one signed-in browser, so this ends that device and no other.
689+
sessionID := r.FormValue("session_id")
690+
resp, err := s.admin.api.DeleteAuthSession(ctx, &api.DeleteAuthSessionReq{Id: sessionID})
688691
switch {
689692
case err != nil:
690693
s.adminRedirect(w, r, "", err.Error())
691694
case resp.NotFound:
692-
s.adminRedirect(w, r, "", fmt.Sprintf("no session for user %q", userID))
695+
s.adminRedirect(w, r, "", fmt.Sprintf("no session %q", sessionID))
693696
default:
694-
s.adminRedirect(w, r, fmt.Sprintf("deleted session for user %q", userID), "")
697+
s.adminRedirect(w, r, fmt.Sprintf("deleted session %q", sessionID), "")
695698
}
696699
}
697700

examples/example-app/server/render.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,11 @@ type DiscoveryEntry struct {
205205

206206
// AdminSession is one of dex's own sessions as the API reports it.
207207
type AdminSession struct {
208+
ID string
208209
UserID string
209210
ConnectorID string
210211
IPAddress string
212+
UserAgent string
211213
Created string
212214
Expires string
213215
}

examples/example-app/server/static/app.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,20 @@ document.querySelectorAll(".chip-remove").forEach(function (btn) {
314314
(function () {
315315
if (!window.EventSource) return;
316316

317+
// Only where the notice has something to say. A stream is a connection held
318+
// open for as long as the page lives, and a browser allows six of them per
319+
// host, so opening one from every page — the admin screens included — is how
320+
// you starve the rest of the site of connections.
321+
if (!document.getElementById("signed-in-card")) return;
322+
317323
const stream = new EventSource("/events");
318324

325+
// Release the connection as the page goes away rather than waiting for the
326+
// browser to notice, so a reload does not briefly hold two.
327+
window.addEventListener("pagehide", function () {
328+
stream.close();
329+
});
330+
319331
stream.addEventListener("backchannel-logout", function (event) {
320332
let notice = {};
321333
try {

examples/example-app/server/templates/admin.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -595,11 +595,13 @@
595595
<div class="subhead">Sessions</div>
596596
{{if .Sessions}}
597597
<table class="table">
598-
<tr><th>Connector</th><th>From</th><th>Started</th><th>Expires</th><th class="actions-col"></th></tr>
598+
<tr><th>Session</th><th>Connector</th><th>From</th><th>Device</th><th>Started</th><th>Expires</th><th class="actions-col"></th></tr>
599599
{{range .Sessions}}
600600
<tr>
601+
<td class="mono small">{{.ID}}</td>
601602
<td class="mono small">{{.ConnectorID}}</td>
602603
<td class="mono small">{{.IPAddress}}</td>
604+
<td class="mono small">{{.UserAgent}}</td>
603605
<td class="small">{{.Created}}</td>
604606
<td class="small">{{.Expires}}</td>
605607
<td class="actions-col">
@@ -608,8 +610,7 @@
608610
<input type="hidden" name="section" value="sessions">
609611
<input type="hidden" name="list_user_id" value="{{$.UserID}}">
610612
<input type="hidden" name="list_connector_id" value="{{$.ConnectorID}}">
611-
<input type="hidden" name="user_id" value="{{.UserID}}">
612-
<input type="hidden" name="connector_id" value="{{.ConnectorID}}">
613+
<input type="hidden" name="session_id" value="{{.ID}}">
613614
<button type="submit" class="button button-small">Delete</button>
614615
</form>
615616
</div>

0 commit comments

Comments
 (0)