feat(oidc): sid claim for id_tokens issued through login V1 (#8525)

# Which Problems Are Solved

id_tokens issued for auth requests created through the login UI
currently do not provide a sid claim.
This is due to the fact that (SSO) sessions for the login UI do not have
one and are only computed by the userAgent(ID), the user(ID) and the
authentication checks of the latter.

This prevents client to track sessions and terminate specific session on
the end_session_endpoint.

# How the Problems Are Solved

- An `id` column is added to the `auth.user_sessions` table.
- The `id` (prefixed with `V1_`) is set whenever a session is added or
updated to active (from terminated)
- The id is passed to the `oidc session` (as v2 sessionIDs), to expose
it as `sid` claim

# Additional Changes

- refactored `getUpdateCols` to handle different column value types and
add arguments for query

# Additional Context

- closes #8499 
- relates to #8501
This commit is contained in:
Livio Spring
2024-09-03 15:19:00 +02:00
committed by GitHub
parent 41b7bf976d
commit 9ec9ad4314
28 changed files with 336 additions and 54 deletions

View File

@@ -146,18 +146,17 @@ func NewUpsertStatement(event eventstore.Event, conflictCols []Column, values []
conflictTarget[i] = col.Name
}
config := execConfig{
args: args,
}
config := execConfig{}
if len(values) == 0 {
config.err = ErrNoValues
}
updateCols, updateVals := getUpdateCols(values, conflictTarget)
updateCols, updateVals, args := getUpdateCols(values, conflictTarget, params, args)
if len(updateCols) == 0 || len(updateVals) == 0 {
config.err = ErrNoValues
}
config.args = args
q := func(config execConfig) string {
var updateStmt string
@@ -194,18 +193,78 @@ func OnlySetValueOnInsert(table string, value interface{}) *onlySetValueOnInsert
}
}
func getUpdateCols(cols []Column, conflictTarget []string) (updateCols, updateVals []string) {
type onlySetValueInCase struct {
Table string
Value interface{}
Condition Condition
}
func (c *onlySetValueInCase) GetValue() interface{} {
return c.Value
}
// ColumnChangedCondition checks the current value and if it changed to a specific new value
func ColumnChangedCondition(table, column string, currentValue, newValue interface{}) Condition {
return func(param string) (string, []any) {
index, _ := strconv.Atoi(param)
return fmt.Sprintf("%[1]s.%[2]s = $%[3]d AND EXCLUDED.%[2]s = $%[4]d", table, column, index, index+1), []any{currentValue, newValue}
}
}
// ColumnIsNullCondition checks if the current value is null
func ColumnIsNullCondition(table, column string) Condition {
return func(param string) (string, []any) {
return fmt.Sprintf("%[1]s.%[2]s IS NULL", table, column), nil
}
}
// ConditionOr links multiple Conditions by OR
func ConditionOr(conditions ...Condition) Condition {
return func(param string) (_ string, args []any) {
if len(conditions) == 0 {
return "", nil
}
b := strings.Builder{}
s, arg := conditions[0](param)
b.WriteString(s)
args = append(args, arg...)
for i := 1; i < len(conditions); i++ {
b.WriteString(" OR ")
s, condArgs := conditions[i](param)
b.WriteString(s)
args = append(args, condArgs...)
}
return b.String(), args
}
}
// OnlySetValueInCase will only update to the desired value if the condition applies
func OnlySetValueInCase(table string, value interface{}, condition Condition) *onlySetValueInCase {
return &onlySetValueInCase{
Table: table,
Value: value,
Condition: condition,
}
}
func getUpdateCols(cols []Column, conflictTarget, params []string, args []interface{}) (updateCols, updateVals []string, updatedArgs []interface{}) {
updateCols = make([]string, len(cols))
updateVals = make([]string, len(cols))
updatedArgs = args
for i := len(cols) - 1; i >= 0; i-- {
col := cols[i]
table := "EXCLUDED"
if onlyOnInsert, ok := col.Value.(*onlySetValueOnInsert); ok {
table = onlyOnInsert.Table
}
updateCols[i] = col.Name
updateVals[i] = table + "." + col.Name
switch v := col.Value.(type) {
case *onlySetValueOnInsert:
updateVals[i] = v.Table + "." + col.Name
case *onlySetValueInCase:
s, condArgs := v.Condition(strconv.Itoa(len(params) + 1))
updatedArgs = append(updatedArgs, condArgs...)
updateVals[i] = fmt.Sprintf("CASE WHEN %[1]s THEN EXCLUDED.%[2]s ELSE %[3]s.%[2]s END", s, col.Name, v.Table)
default:
updateVals[i] = "EXCLUDED" + "." + col.Name
}
for _, conflict := range conflictTarget {
if conflict == col.Name {
copy(updateCols[i:], updateCols[i+1:])
@@ -221,7 +280,7 @@ func getUpdateCols(cols []Column, conflictTarget []string) (updateCols, updateVa
}
}
return updateCols, updateVals
return updateCols, updateVals, updatedArgs
}
func NewUpdateStatement(event eventstore.Event, values []Column, conditions []Condition, opts ...execOption) *Statement {

View File

@@ -451,6 +451,55 @@ func TestNewUpsertStatement(t *testing.T) {
},
},
},
{
name: "correct all *onlySetValueInCase",
args: args{
table: "my_table",
event: &testEvent{
aggregateType: "agg",
sequence: 1,
previousSequence: 0,
},
conflictCols: []Column{
NewCol("col1", nil),
},
values: []Column{
{
Name: "col1",
Value: "val1",
},
{
Name: "col2",
Value: &onlySetValueInCase{
Table: "some.table",
Value: "val2",
Condition: ConditionOr(
ColumnChangedCondition("some.table", "val3", 0, 1),
ColumnIsNullCondition("some.table", "val3"),
),
},
},
},
},
want: want{
table: "my_table",
aggregateType: "agg",
sequence: 1,
previousSequence: 1,
executer: &wantExecuter{
params: []params{
{
query: "INSERT INTO my_table (col1, col2) VALUES ($1, $2) ON CONFLICT (col1) DO UPDATE SET col2 = CASE WHEN some.table.val3 = $3 AND EXCLUDED.val3 = $4 OR some.table.val3 IS NULL THEN EXCLUDED.col2 ELSE some.table.col2 END",
args: []interface{}{"val1", "val2", 0, 1},
},
},
shouldExecute: true,
},
isErr: func(err error) bool {
return err == nil
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {