Merge pull request #3 from netbirdio/fix/implement-id-token-hint-rp-logout

Fix/implement id token hint rp logout
This commit is contained in:
Nicolas Frati
2026-04-15 16:58:16 +02:00
committed by GitHub
34 changed files with 1301 additions and 99 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
publish_results: true
- name: Upload results as artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: OpenSSF Scorecard results
path: results.sarif
+2 -2
View File
@@ -125,7 +125,7 @@ jobs:
- name: Build and push image
id: build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
@@ -256,7 +256,7 @@ jobs:
run: cat trivy-results.sarif
- name: Upload Trivy scan results as artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "[${{ github.job }}] Trivy scan results"
path: trivy-results.sarif
+76 -35
View File
@@ -63,26 +63,32 @@ type Config struct {
DomainHint string `json:"domainHint"`
Scopes []string `json:"scopes"` // defaults to scopeUser (user.read)
// PreferredUsernameField allows users to set the field to any of the
// following values: "name", "email", "mailNickname" or "onPremisesSamAccountName".
// If unset, the preferred_username field will remain empty.
PreferredUsernameField string `json:"preferredUsernameField"`
}
// Open returns a strategy for logging in through Microsoft.
func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
m := microsoftConnector{
apiURL: strings.TrimSuffix(c.APIURL, "/"),
graphURL: strings.TrimSuffix(c.GraphURL, "/"),
redirectURI: c.RedirectURI,
clientID: c.ClientID,
clientSecret: c.ClientSecret,
tenant: c.Tenant,
onlySecurityGroups: c.OnlySecurityGroups,
groups: c.Groups,
groupNameFormat: c.GroupNameFormat,
useGroupsAsWhitelist: c.UseGroupsAsWhitelist,
logger: logger.With(slog.Group("connector", "type", "microsoft", "id", id)),
emailToLowercase: c.EmailToLowercase,
promptType: c.PromptType,
domainHint: c.DomainHint,
scopes: c.Scopes,
apiURL: strings.TrimSuffix(c.APIURL, "/"),
graphURL: strings.TrimSuffix(c.GraphURL, "/"),
redirectURI: c.RedirectURI,
clientID: c.ClientID,
clientSecret: c.ClientSecret,
tenant: c.Tenant,
onlySecurityGroups: c.OnlySecurityGroups,
groups: c.Groups,
groupNameFormat: c.GroupNameFormat,
useGroupsAsWhitelist: c.UseGroupsAsWhitelist,
logger: logger.With(slog.Group("connector", "type", "microsoft", "id", id)),
emailToLowercase: c.EmailToLowercase,
promptType: c.PromptType,
domainHint: c.DomainHint,
scopes: c.Scopes,
preferredUsernameField: c.PreferredUsernameField,
}
if m.apiURL == "" {
@@ -123,21 +129,22 @@ var (
)
type microsoftConnector struct {
apiURL string
graphURL string
redirectURI string
clientID string
clientSecret string
tenant string
onlySecurityGroups bool
groupNameFormat GroupNameFormat
groups []string
useGroupsAsWhitelist bool
logger *slog.Logger
emailToLowercase bool
promptType string
domainHint string
scopes []string
apiURL string
graphURL string
redirectURI string
clientID string
clientSecret string
tenant string
onlySecurityGroups bool
groupNameFormat GroupNameFormat
groups []string
useGroupsAsWhitelist bool
logger *slog.Logger
emailToLowercase bool
promptType string
domainHint string
scopes []string
preferredUsernameField string
}
func (c *microsoftConnector) isOrgTenant() bool {
@@ -223,6 +230,7 @@ func (c *microsoftConnector) HandleCallback(s connector.Scopes, connData []byte,
Email: user.Email,
EmailVerified: true,
}
c.setPreferredUsername(&identity, user)
if c.groupsRequired(s.Groups) {
groups, err := c.getGroups(ctx, client, user.ID)
@@ -314,6 +322,7 @@ func (c *microsoftConnector) Refresh(ctx context.Context, s connector.Scopes, id
identity.Username = user.Name
identity.Email = user.Email
c.setPreferredUsername(&identity, user)
if c.groupsRequired(s.Groups) {
groups, err := c.getGroups(ctx, client, user.ID)
@@ -326,6 +335,23 @@ func (c *microsoftConnector) Refresh(ctx context.Context, s connector.Scopes, id
return identity, nil
}
func (c *microsoftConnector) setPreferredUsername(identity *connector.Identity, u user) {
switch c.preferredUsernameField {
case "name":
identity.PreferredUsername = u.Name
case "email":
identity.PreferredUsername = u.Email
case "mailNickname":
identity.PreferredUsername = u.MailNickname
case "onPremisesSamAccountName":
identity.PreferredUsername = u.OnPremisesSamAccountName
default:
if c.preferredUsernameField != "" {
c.logger.Warn("preferred_username left empty. Invalid microsoft field mapped to preferred_username", "field", c.preferredUsernameField)
}
}
}
// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/user
// id - The unique identifier for the user. Inherited from
//
@@ -342,22 +368,37 @@ func (c *microsoftConnector) Refresh(ctx context.Context, s connector.Scopes, id
//
// The UPN is an Internet-style login name for the user
// based on the Internet standard RFC 822. By convention,
// this should map to the user's email name. The general
// this should map to the users email name. The general
// format is alias@domain, where domain must be present in
// the tenants collection of verified domains. This
// property is required when a user is created. The
// verified domains for the tenant can be accessed from the
// verifiedDomains property of organization. Supports
// $filter and $orderby.
//
// mailNickname - The mail alias for the user.
//
// This property must be specified when a user is created.
// Maximum length is 64 characters. Supports $filter.
//
// onPremisesSamAccountName - Contains the on-premises SAM account name
//
// synchronized from the on-premises directory.
// This property is only populated for customers
// who are synchronizing their on-premises directory
// to Azure Active Directory via Azure AD Connect.
// Read-only.
type user struct {
ID string `json:"id"`
Name string `json:"displayName"`
Email string `json:"userPrincipalName"`
ID string `json:"id"`
Name string `json:"displayName"`
Email string `json:"userPrincipalName"`
MailNickname string `json:"mailNickname"`
OnPremisesSamAccountName string `json:"onPremisesSamAccountName"`
}
func (c *microsoftConnector) user(ctx context.Context, client *http.Client) (u user, err error) {
// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_get
req, err := http.NewRequest("GET", c.graphURL+"/v1.0/me?$select=id,displayName,userPrincipalName", nil)
req, err := http.NewRequest("GET", c.graphURL+"/v1.0/me?$select=id,displayName,userPrincipalName,mailNickname,onPremisesSamAccountName", nil)
if err != nil {
return u, fmt.Errorf("new req: %v", err)
}
+34 -3
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
@@ -82,7 +83,7 @@ func TestLoginURLWithOptions(t *testing.T) {
func TestUserIdentityFromGraphAPI(t *testing.T) {
s := newTestServer(map[string]testResponse{
"/v1.0/me?$select=id,displayName,userPrincipalName": {
"/v1.0/me?$select=id,displayName,userPrincipalName,mailNickname,onPremisesSamAccountName": {
data: user{ID: "S56767889", Name: "Jane Doe", Email: "jane.doe@example.com"},
},
"/" + tenant + "/oauth2/v2.0/token": dummyToken,
@@ -102,9 +103,39 @@ func TestUserIdentityFromGraphAPI(t *testing.T) {
expectEquals(t, len(identity.Groups), 0)
}
func TestPreferredUsernameField(t *testing.T) {
s := newTestServer(map[string]testResponse{
"/v1.0/me?$select=id,displayName,userPrincipalName,mailNickname,onPremisesSamAccountName": {
data: user{ID: "S56767889", Name: "Jane Doe", Email: "jane.doe@example.com", MailNickname: "janedoe", OnPremisesSamAccountName: "DOMAIN\\janedoe"},
},
"/" + tenant + "/oauth2/v2.0/token": dummyToken,
})
defer s.Close()
tests := []struct {
field string
expected string
}{
{"", ""},
{"name", "Jane Doe"},
{"email", "jane.doe@example.com"},
{"mailNickname", "janedoe"},
{"onPremisesSamAccountName", "DOMAIN\\janedoe"},
{"invalidstring", ""},
}
for _, tt := range tests {
req, _ := http.NewRequest("GET", s.URL, nil)
c := microsoftConnector{apiURL: s.URL, graphURL: s.URL, tenant: tenant, preferredUsernameField: tt.field, logger: slog.Default()}
identity, err := c.HandleCallback(connector.Scopes{Groups: false}, nil, req)
expectNil(t, err)
expectEquals(t, identity.PreferredUsername, tt.expected)
}
}
func TestUserGroupsFromGraphAPI(t *testing.T) {
s := newTestServer(map[string]testResponse{
"/v1.0/me?$select=id,displayName,userPrincipalName": {data: user{}},
"/v1.0/me?$select=id,displayName,userPrincipalName,mailNickname,onPremisesSamAccountName": {data: user{}},
"/v1.0/me/getMemberGroups": {data: map[string]interface{}{
"value": []string{"a", "b"},
}},
@@ -122,7 +153,7 @@ func TestUserGroupsFromGraphAPI(t *testing.T) {
func TestUserNotInRequiredGroupFromGraphAPI(t *testing.T) {
s := newTestServer(map[string]testResponse{
"/v1.0/me?$select=id,displayName,userPrincipalName": {
"/v1.0/me?$select=id,displayName,userPrincipalName,mailNickname,onPremisesSamAccountName": {
data: user{ID: "user-id-123", Name: "Jane Doe", Email: "jane.doe@example.com"},
},
// The user is a member of groups "c" and "d", but the connector only
+15 -1
View File
@@ -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 {
if len(cd.IDToken) > 0 {
q.Set("id_token_hint", string(cd.IDToken))
}
}
}
u.RawQuery = q.Encode()
return u.String(), nil
+40 -1
View File
@@ -979,10 +979,22 @@ func expectEquals(t *testing.T, a interface{}, b interface{}) {
}
func TestLogoutURL(t *testing.T) {
idTokenConnData, err := json.Marshal(connectorData{
RefreshToken: []byte("refresh"),
IDToken: []byte("id-token-jwt"),
})
require.NoError(t, err)
noIDTokenConnData, err := json.Marshal(connectorData{
RefreshToken: []byte("refresh"),
})
require.NoError(t, err)
tests := []struct {
name string
endSessionURL string
postLogoutRedirectURI string
connectorData []byte
wantURL string
wantEmpty bool
}{
@@ -1008,6 +1020,33 @@ func TestLogoutURL(t *testing.T) {
postLogoutRedirectURI: "https://dex.example.com/callback",
wantURL: "https://provider.example.com/logout?client_id=clientID&existing=param&post_logout_redirect_uri=https%3A%2F%2Fdex.example.com%2Fcallback",
},
{
name: "with id_token_hint from connector data",
endSessionURL: "https://provider.example.com/logout",
postLogoutRedirectURI: "https://dex.example.com/logout/callback",
connectorData: idTokenConnData,
wantURL: "https://provider.example.com/logout?client_id=clientID&id_token_hint=id-token-jwt&post_logout_redirect_uri=https%3A%2F%2Fdex.example.com%2Flogout%2Fcallback",
},
{
name: "id_token_hint included without post_logout_redirect_uri",
endSessionURL: "https://provider.example.com/logout",
connectorData: idTokenConnData,
wantURL: "https://provider.example.com/logout?id_token_hint=id-token-jwt",
},
{
name: "connector data without IDToken omits id_token_hint",
endSessionURL: "https://provider.example.com/logout",
postLogoutRedirectURI: "https://dex.example.com/logout/callback",
connectorData: noIDTokenConnData,
wantURL: "https://provider.example.com/logout?client_id=clientID&post_logout_redirect_uri=https%3A%2F%2Fdex.example.com%2Flogout%2Fcallback",
},
{
name: "malformed connector data is ignored",
endSessionURL: "https://provider.example.com/logout",
postLogoutRedirectURI: "https://dex.example.com/logout/callback",
connectorData: []byte("not-json"),
wantURL: "https://provider.example.com/logout?client_id=clientID&post_logout_redirect_uri=https%3A%2F%2Fdex.example.com%2Flogout%2Fcallback",
},
}
for _, tc := range tests {
@@ -1019,7 +1058,7 @@ func TestLogoutURL(t *testing.T) {
},
}
got, err := conn.LogoutURL(context.Background(), nil, tc.postLogoutRedirectURI)
got, err := conn.LogoutURL(context.Background(), tc.connectorData, tc.postLogoutRedirectURI)
require.NoError(t, err)
if tc.wantEmpty {
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/dexidp/dex/examples
go 1.25.0
require (
github.com/coreos/go-oidc/v3 v3.17.0
github.com/coreos/go-oidc/v3 v3.18.0
github.com/dexidp/dex/api/v2 v2.4.0
github.com/spf13/cobra v1.10.2
golang.org/x/oauth2 v0.36.0
+2 -2
View File
@@ -1,7 +1,7 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/dexidp/dex/api/v2 v2.4.0 h1:gNba7n6BKVp8X4Jp24cxYn5rIIGhM6kDOXcZoL6tr9A=
github.com/dexidp/dex/api/v2 v2.4.0/go.mod h1:/p550ADvFFh7K95VmhUD+jgm15VdaNnab9td8DHOpyI=
+10 -10
View File
@@ -16,8 +16,8 @@ require (
github.com/go-jose/go-jose/v4 v4.1.4
github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-sql-driver/mysql v1.9.3
github.com/go-webauthn/webauthn v0.16.3
github.com/google/cel-go v0.27.0
github.com/go-webauthn/webauthn v0.16.4
github.com/google/cel-go v0.28.0
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
@@ -25,7 +25,7 @@ require (
github.com/kylelemons/godebug v1.1.0
github.com/lib/pq v1.12.3
github.com/mattermost/xml-roundtrip-validator v0.1.0
github.com/mattn/go-sqlite3 v1.14.41
github.com/mattn/go-sqlite3 v1.14.42
github.com/oklog/run v1.2.0
github.com/openbao/openbao/api/v2 v2.5.1
github.com/pkg/errors v0.9.1
@@ -36,9 +36,9 @@ require (
github.com/stretchr/testify v1.11.1
go.etcd.io/etcd/client/pkg/v3 v3.6.10
go.etcd.io/etcd/client/v3 v3.6.10
golang.org/x/crypto v0.49.0
golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
golang.org/x/net v0.52.0
golang.org/x/net v0.53.0
golang.org/x/oauth2 v0.36.0
google.golang.org/api v0.275.0
google.golang.org/grpc v1.80.0
@@ -77,7 +77,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.2 // indirect
github.com/go-webauthn/x v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
@@ -133,12 +133,12 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.42.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
+20 -20
View File
@@ -98,10 +98,10 @@ github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.16.3 h1:RorP0c6VbaKP0i0Jxf/vAf7EFb2lmdLW8GLKITeaN5A=
github.com/go-webauthn/webauthn v0.16.3/go.mod h1:R2xjJxSPat5PYKg5r6cUmqXgbHtbv4GmF6uGkqFMLNI=
github.com/go-webauthn/x v0.2.2 h1:zIiipvMbr48CXi5RG0XdBJR94kd8I5LfzHPb/q+YYmk=
github.com/go-webauthn/x v0.2.2/go.mod h1:IpJ5qyWB9NRhLX3C7gIfjTU7RZLXEP6kzFkoVSE7Fz4=
github.com/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q=
github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4=
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@@ -109,8 +109,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -197,8 +197,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.41 h1:8p7Pwz5NHkEbWSqc/ygU4CBGubhFFkpgP9KwcdkAHNA=
github.com/mattn/go-sqlite3 v1.14.41/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
@@ -311,20 +311,20 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -336,20 +336,20 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY=
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
+2 -2
View File
@@ -449,8 +449,8 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
return
}
if redirectURL != "" {
// Session found but consent required — no UI allowed.
s.redirectWithError(w, r, authReq, errInteractionRequired, "Consent required")
// Session found but user interaction is needed (consent or MFA) — no UI allowed.
s.redirectWithError(w, r, authReq, errInteractionRequired, "User interaction required")
return
}
return
+55
View File
@@ -908,6 +908,61 @@ func TestScopesCoveredByConsent(t *testing.T) {
}
}
// TestConsentSurvivesSessionDeletion verifies that UserIdentity.Consents
// persists independently from AuthSession lifecycle (logout should not
// clear consent decisions).
func TestConsentSurvivesSessionDeletion(t *testing.T) {
ctx := t.Context()
httpServer, s := newTestServerWithSessions(t, nil)
defer httpServer.Close()
userID := "test-user"
connectorID := "mock"
clientID := "test-client"
// Create UserIdentity with existing consents.
require.NoError(t, s.storage.CreateUserIdentity(ctx, storage.UserIdentity{
UserID: userID,
ConnectorID: connectorID,
Claims: storage.Claims{UserID: userID, Username: "testuser"},
Consents: map[string][]string{clientID: {"openid", "email", "profile"}},
CreatedAt: time.Now(),
LastLogin: time.Now(),
}))
// Create and then delete the session (simulating logout).
require.NoError(t, s.storage.CreateAuthSession(ctx, storage.AuthSession{
UserID: userID, ConnectorID: connectorID, Nonce: "nonce",
CreatedAt: time.Now(), LastActivity: time.Now(),
}))
require.NoError(t, s.storage.DeleteAuthSession(ctx, userID, connectorID))
// Session is gone.
_, err := s.storage.GetAuthSession(ctx, userID, connectorID)
require.ErrorIs(t, err, storage.ErrNotFound)
// Consent survives.
ui, err := s.storage.GetUserIdentity(ctx, userID, connectorID)
require.NoError(t, err)
require.Equal(t, []string{"openid", "email", "profile"}, ui.Consents[clientID],
"consent should survive session deletion")
}
// TestConsentIsolatedBetweenClients verifies that consent given for
// client-A does not satisfy scope check for client-B.
func TestConsentIsolatedBetweenClients(t *testing.T) {
approvedForA := map[string][]string{"client-a": {"openid", "email"}}
// client-b should not have consent.
require.False(t, scopesCoveredByConsent(approvedForA["client-b"], []string{"openid", "email"}),
"consent for client-a should not cover client-b")
// client-a should have consent.
require.True(t, scopesCoveredByConsent(approvedForA["client-a"], []string{"openid", "email"}),
"consent for client-a should cover client-a's requested scopes")
}
func TestHandlePasswordLoginWithSkipApproval(t *testing.T) {
ctx := t.Context()
+2 -1
View File
@@ -2,6 +2,7 @@ package server
import (
"context"
"crypto/subtle"
"errors"
"fmt"
"net/http"
@@ -24,7 +25,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(s.sessionConfig.CookieName); err == nil && cookie.Value != "" {
if userID, connectorID, nonce, err := parseSessionCookie(cookie.Value, s.sessionConfig.CookieEncryptionKey); err == nil {
session, err := s.storage.GetAuthSession(ctx, userID, connectorID)
if err == nil && session.Nonce == nonce {
if err == nil && subtle.ConstantTimeCompare([]byte(session.Nonce), []byte(nonce)) == 1 {
data.LoggedIn = true
data.IPAddress = session.IPAddress
data.UserAgent = session.UserAgent
+12 -3
View File
@@ -2,6 +2,7 @@ package server
import (
"context"
"crypto/subtle"
"errors"
"net/http"
"net/url"
@@ -94,7 +95,7 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(s.sessionConfig.CookieName); err == nil && cookie.Value != "" {
if uid, cid, nonce, err := parseSessionCookie(cookie.Value, s.sessionConfig.CookieEncryptionKey); err == nil {
// Verify the session exists and nonce matches before trusting the cookie.
if session, err := s.storage.GetAuthSession(ctx, uid, cid); err == nil && session.Nonce == nonce {
if session, err := s.storage.GetAuthSession(ctx, uid, cid); err == nil && subtle.ConstantTimeCompare([]byte(session.Nonce), []byte(nonce)) == 1 {
userID = uid
connectorID = cid
s.logger.DebugContext(ctx, "logout: identified user from session cookie",
@@ -178,7 +179,7 @@ func (s *Server) handleLogoutCallback(w http.ResponseWriter, r *http.Request) {
return
}
if session.Nonce != nonce {
if subtle.ConstantTimeCompare([]byte(session.Nonce), []byte(nonce)) != 1 {
s.renderError(r, w, http.StatusBadRequest, "Invalid session.")
return
}
@@ -250,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 {
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{
+141
View File
@@ -12,9 +12,29 @@ import (
"github.com/stretchr/testify/require"
"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/storage"
)
// recordingLogoutConnector implements connector.LogoutCallbackConnector and
// records the connectorData it was invoked with so tests can assert what was
// passed down.
type recordingLogoutConnector struct {
gotConnectorData []byte
returnURL string
}
func (c *recordingLogoutConnector) LogoutURL(_ context.Context, connectorData []byte, _ string) (string, error) {
c.gotConnectorData = connectorData
return c.returnURL, nil
}
func (c *recordingLogoutConnector) HandleLogoutCallback(_ context.Context, _ *http.Request) error {
return nil
}
var _ connector.LogoutCallbackConnector = (*recordingLogoutConnector)(nil)
func TestHandleLogoutNoSessions(t *testing.T) {
httpServer, server := newTestServer(t, nil)
defer httpServer.Close()
@@ -293,6 +313,62 @@ func TestDiscoveryWithoutSessions(t *testing.T) {
require.Empty(t, d.EndSession)
}
// TestHandleLogoutFromCookie tests logout without id_token_hint,
// where the user is identified by their session cookie alone.
func TestHandleLogoutFromCookie(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
ctx := t.Context()
userID := "test-user"
connectorID := "mock"
nonce := "testnonce"
require.NoError(t, server.storage.CreateAuthSession(ctx, storage.AuthSession{
UserID: userID, ConnectorID: connectorID, Nonce: nonce,
CreatedAt: time.Now(), LastActivity: time.Now(),
}))
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/logout", nil)
req.AddCookie(&http.Cookie{
Name: "dex_session",
Value: sessionCookieValue(userID, connectorID, nonce, server.sessionConfig.CookieEncryptionKey),
})
server.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Contains(t, rr.Body.String(), "successfully logged out")
// Session should be deleted.
_, err := server.storage.GetAuthSession(ctx, userID, connectorID)
require.ErrorIs(t, err, storage.ErrNotFound)
// Cookie should be cleared.
for _, c := range rr.Result().Cookies() {
if c.Name == "dex_session" {
require.Equal(t, -1, c.MaxAge)
}
}
}
// TestLogoutCallbackWithExpiredSession tests that /logout/callback
// returns an error when the session has expired or been deleted.
func TestLogoutCallbackWithExpiredSession(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
// No session created — cookie points to nonexistent session.
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/logout/callback", nil)
req.AddCookie(&http.Cookie{
Name: "dex_session",
Value: sessionCookieValue("user-1", "mock", "nonce", server.sessionConfig.CookieEncryptionKey),
})
server.ServeHTTP(rr, req)
require.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestRevokeRefreshTokensReturnsConnectorData(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
@@ -324,3 +400,68 @@ func TestRevokeRefreshTokensReturnsConnectorData(t *testing.T) {
require.Empty(t, os.Refresh)
require.Equal(t, expectedConnData, os.ConnectorData)
}
// TestTryUpstreamLogoutPrefersSessionConnectorData verifies that when the auth
// session has ConnectorData stored (from login), it takes precedence over the
// connectorData the caller passes in (which originates from the offline session).
func TestTryUpstreamLogoutPrefersSessionConnectorData(t *testing.T) {
tests := []struct {
name string
sessionConnData []byte
callerConnData []byte
wantConnData []byte
}{
{
name: "session data wins over caller data",
sessionConnData: []byte(`{"IDToken":"session-token"}`),
callerConnData: []byte(`{"IDToken":"caller-token"}`),
wantConnData: []byte(`{"IDToken":"session-token"}`),
},
{
name: "caller data used when session data is empty",
sessionConnData: nil,
callerConnData: []byte(`{"IDToken":"caller-token"}`),
wantConnData: []byte(`{"IDToken":"caller-token"}`),
},
{
name: "empty when neither source has data",
sessionConnData: nil,
callerConnData: nil,
wantConnData: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
ctx := t.Context()
userID := "test-user"
connectorID := "mock"
// Inject a recording connector with matching ResourceVersion so
// getConnector returns our mock instead of re-opening from storage.
rec := &recordingLogoutConnector{returnURL: "https://upstream.example.com/logout"}
server.mu.Lock()
server.connectors[connectorID] = Connector{
Type: "mockCallback",
ResourceVersion: "1",
Connector: rec,
}
server.mu.Unlock()
require.NoError(t, server.storage.CreateAuthSession(ctx, storage.AuthSession{
UserID: userID, ConnectorID: connectorID, Nonce: "nonce",
CreatedAt: time.Now(), LastActivity: time.Now(),
ConnectorData: tc.sessionConnData,
}))
redirectURL, ok := server.tryUpstreamLogout(ctx, userID, connectorID, tc.callerConnData,
"https://dex.example.com/cb", "state-123", "client-123")
require.True(t, ok)
require.Equal(t, "https://upstream.example.com/logout", redirectURL)
require.Equal(t, tc.wantConnData, rec.gotConnectorData)
})
}
}
+5 -7
View File
@@ -336,13 +336,11 @@ func TestRefreshTokenAuthTime(t *testing.T) {
"access token should not have auth_time when sessions are disabled")
}
// TODO: newIDToken in handleRefreshToken is currently called with time.Time{},
// so the ID token does not include auth_time. Once fixed, uncomment:
// if tc.wantAuthTime {
// idClaims := decodeJWTClaims(t, resp.IDToken)
// assert.Equal(t, float64(loginTime.Unix()), idClaims["auth_time"],
// "id token auth_time should match UserIdentity.LastLogin")
// }
if tc.wantAuthTime {
idClaims := decodeJWTClaims(t, resp.IDToken)
assert.Equal(t, float64(loginTime.Unix()), idClaims["auth_time"],
"id token auth_time should match UserIdentity.LastLogin")
}
})
}
}
+6 -1
View File
@@ -5,6 +5,7 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
@@ -178,7 +179,9 @@ func (s *Server) getValidSession(ctx context.Context, w http.ResponseWriter, r *
}
// Verify nonce to prevent cookie forgery.
if session.Nonce != nonce {
// Use constant-time comparison to prevent timing attacks that could
// allow an attacker to recover the nonce byte-by-byte.
if subtle.ConstantTimeCompare([]byte(session.Nonce), []byte(nonce)) != 1 {
s.logger.DebugContext(ctx, "auth session nonce mismatch")
s.clearSessionCookie(w)
return nil
@@ -258,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
return old, nil
}); err != nil {
return fmt.Errorf("update auth session: %w", err)
@@ -286,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 {
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -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="}`),
}
// Create.
@@ -1418,8 +1420,9 @@ 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,
@@ -1427,6 +1430,7 @@ func testAuthSessionCRUD(t *testing.T, s storage.Storage) {
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)
+2
View File
@@ -31,6 +31,7 @@ func (d *Database) CreateAuthSession(ctx context.Context, session storage.AuthSe
SetUserAgent(session.UserAgent).
SetAbsoluteExpiry(session.AbsoluteExpiry.UTC()).
SetIdleExpiry(session.IdleExpiry.UTC()).
SetConnectorData(session.ConnectorData).
Save(ctx)
if err != nil {
return convertDBError("create auth session: %w", err)
@@ -106,6 +107,7 @@ func (d *Database) UpdateAuthSession(ctx context.Context, userID, connectorID st
SetUserAgent(newSession.UserAgent).
SetAbsoluteExpiry(newSession.AbsoluteExpiry.UTC()).
SetIdleExpiry(newSession.IdleExpiry.UTC()).
SetConnectorData(newSession.ConnectorData).
Save(ctx)
if err != nil {
return rollback(tx, "update auth session updating: %w", err)

Some files were not shown because too many files have changed in this diff Show More