-
Notifications
You must be signed in to change notification settings - Fork 1.9k
fix: implement id token hint on RP-Initiated logout #4743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -228,6 +228,7 @@ var brokenAuthHeaderDomains = []string{ | |
| // connectorData stores information for sessions authenticated by this connector | ||
| type connectorData struct { | ||
| RefreshToken []byte | ||
| IDToken []byte // raw upstream id_token JWT for RP-Initiated logout | ||
| } | ||
|
|
||
| // Detect auth header provider issues for known providers. This lets users | ||
|
|
@@ -736,6 +737,9 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I | |
| cd := connectorData{ | ||
| RefreshToken: []byte(token.RefreshToken), | ||
| } | ||
| if rawIDToken, ok := token.Extra("id_token").(string); ok { | ||
| cd.IDToken = []byte(rawIDToken) | ||
| } | ||
|
|
||
| connData, err := json.Marshal(&cd) | ||
| if err != nil { | ||
|
|
@@ -766,7 +770,7 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I | |
| // LogoutURL returns the upstream OIDC provider's end_session_endpoint URL. | ||
| // Per the OIDC RP-Initiated Logout spec, the post_logout_redirect_uri parameter | ||
| // tells the upstream where to redirect after logout. | ||
| func (c *oidcConnector) LogoutURL(_ context.Context, _ []byte, postLogoutRedirectURI string) (string, error) { | ||
| func (c *oidcConnector) LogoutURL(_ context.Context, rawConnectorData []byte, postLogoutRedirectURI string) (string, error) { | ||
| if c.endSessionURL == "" { | ||
| return "", nil | ||
| } | ||
|
|
@@ -781,6 +785,16 @@ func (c *oidcConnector) LogoutURL(_ context.Context, _ []byte, postLogoutRedirec | |
| q.Set("post_logout_redirect_uri", postLogoutRedirectURI) | ||
| q.Set("client_id", c.oauth2Config.ClientID) | ||
| } | ||
| // Per the RP-Initiated Logout spec, id_token_hint is independently valid | ||
| // of post_logout_redirect_uri — include it whenever we have one. | ||
| if len(rawConnectorData) > 0 { | ||
| var cd connectorData | ||
| if err := json.Unmarshal(rawConnectorData, &cd); err == nil { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: Let's log the error here for debugging |
||
| if len(cd.IDToken) > 0 { | ||
| q.Set("id_token_hint", string(cd.IDToken)) | ||
| } | ||
| } | ||
| } | ||
| u.RawQuery = q.Encode() | ||
|
|
||
| return u.String(), nil | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -251,13 +251,21 @@ func (s *Server) tryUpstreamLogout(ctx context.Context, userID, connectorID stri | |
| } | ||
|
|
||
| // Check that the session exists — we need it to store logout state. | ||
| _, err = s.storage.GetAuthSession(ctx, userID, connectorID) | ||
| session, err := s.storage.GetAuthSession(ctx, userID, connectorID) | ||
| if err != nil { | ||
| s.logger.DebugContext(ctx, "logout: no auth session for upstream logout, skipping", | ||
| "user_id", userID, "connector_id", connectorID) | ||
| return "", false | ||
| } | ||
|
|
||
| // The auth session connector data should keep an id_token that will be used as hint for RP-Initiated logout | ||
| if len(session.ConnectorData) > 0 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The precedence logic (auth session over offline session) makes sense, but the comment says "should keep an id_token" — that's OIDC-specific language in generic server code. With if len(session.SessionData) > 0 {
connectorData = session.SessionData
}No need to explain what's inside — the connector decides. |
||
| connectorData = session.ConnectorData | ||
| s.logger.DebugContext(ctx, "logout: using auth_session.ConnectorData", "connector_id", connectorID) | ||
| } else if len(connectorData) == 0 { | ||
| s.logger.DebugContext(ctx, "logout: no connector data available", "connector_id", connectorID) | ||
| } | ||
|
|
||
| // Store logout parameters in the session. | ||
| if err := s.storage.UpdateAuthSession(ctx, userID, connectorID, func(old storage.AuthSession) (storage.AuthSession, error) { | ||
| old.LogoutState = &storage.LogoutState{ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -261,6 +261,7 @@ func (s *Server) createOrUpdateAuthSession(ctx context.Context, r *http.Request, | |
| old.ClientStates = make(map[string]*storage.ClientAuthState) | ||
| } | ||
| old.ClientStates[authReq.ClientID] = clientState | ||
| old.ConnectorData = authReq.ConnectorData | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This overwrites Same on line 293 (create path). |
||
| return old, nil | ||
| }); err != nil { | ||
| return fmt.Errorf("update auth session: %w", err) | ||
|
|
@@ -289,6 +290,7 @@ func (s *Server) createOrUpdateAuthSession(ctx context.Context, r *http.Request, | |
| UserAgent: r.UserAgent(), | ||
| AbsoluteExpiry: now.Add(s.sessionConfig.AbsoluteLifetime), | ||
| IdleExpiry: now.Add(s.sessionConfig.ValidIfNotUsedFor), | ||
| ConnectorData: authReq.ConnectorData, | ||
| } | ||
|
|
||
| if err := s.storage.CreateAuthSession(ctx, newSession); err != nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| package conformance | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "crypto/ecdsa" | ||
| "reflect" | ||
| "sort" | ||
|
|
@@ -1388,6 +1389,7 @@ func testAuthSessionCRUD(t *testing.T, s storage.Storage) { | |
| UserAgent: "TestBrowser/1.0", | ||
| AbsoluteExpiry: now.Add(24 * time.Hour), | ||
| IdleExpiry: now.Add(1 * time.Hour), | ||
| ConnectorData: []byte(`{"RefreshToken":"dGVzdA==","IDToken":"ZXlKaGJHY21PaUpTVXpJMU5pSjk="}`), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test data includes |
||
| } | ||
|
|
||
| // Create. | ||
|
|
@@ -1418,15 +1420,17 @@ func testAuthSessionCRUD(t *testing.T, s storage.Storage) { | |
| t.Errorf("auth session retrieved from storage did not match: %s", diff) | ||
| } | ||
|
|
||
| // Update: add a new client state. | ||
| // Update: add a new client state and rotate connector data. | ||
| newNow := now.Add(time.Minute) | ||
| updatedConnectorData := []byte(`{"RefreshToken":"bmV3","IDToken":"bmV3LWlk"}`) | ||
| if err := s.UpdateAuthSession(ctx, session.UserID, session.ConnectorID, func(old storage.AuthSession) (storage.AuthSession, error) { | ||
| old.ClientStates["client2"] = &storage.ClientAuthState{ | ||
| Active: true, | ||
| ExpiresAt: newNow.Add(24 * time.Hour), | ||
| LastActivity: newNow, | ||
| } | ||
| old.LastActivity = newNow | ||
| old.ConnectorData = updatedConnectorData | ||
| return old, nil | ||
| }); err != nil { | ||
| t.Fatalf("update auth session: %v", err) | ||
|
|
@@ -1443,6 +1447,9 @@ func testAuthSessionCRUD(t *testing.T, s storage.Storage) { | |
| if got.ClientStates["client2"] == nil { | ||
| t.Fatal("expected client2 state to exist") | ||
| } | ||
| if !bytes.Equal(got.ConnectorData, updatedConnectorData) { | ||
| t.Fatalf("expected updated connector data %q, got %q", updatedConnectorData, got.ConnectorData) | ||
| } | ||
|
|
||
| // List and verify. | ||
| sessions, err := s.ListAuthSessions(ctx) | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two issues here:
rawIDTokenis already extracted and verified at the top ofcreateIdentity(line 563). You can reuse it instead of extracting twice.IDTokengets packed into the sameconnectorDatastruct asRefreshToken, and this whole blob ends up inAuthSession.ConnectorData. That leaks the upstream refresh token into the auth session table.I think we should separate session-scoped data from refresh-scoped data at the
connector.Identitylevel. Something like:This is a struct, not an interface — adding a field is backward compatible. Connectors that don't set
SessionDatawill havenil, no behavior change.Then here you'd do:
This way auth session never sees the refresh token. SAML connectors can store
SessionIndexinSessionDatalater without touching the model.