feat: allow arbitrary claims from the IDToken and IdentityProvider UserInfo endpoint to be added to the session state (#2685)

* feat: support additional claims

Signed-off-by: afsu <suaf2020@163.com>
Signed-off-by: af su <saf@zjuici.com>

* docs: clarify that AdditionalClaims may come from id_token or userinfo endpoint

Signed-off-by: afsu <suaf2020@163.com>
Signed-off-by: af su <saf@zjuici.com>

* feat: include AdditionalClaims in /oauth2/userinfo response (#834)

Signed-off-by: afsu <suaf2020@163.com>
Signed-off-by: af su <saf@zjuici.com>

* refactor: extract coerceClaim logic into util

Signed-off-by: afsu <suaf2020@163.com>
Signed-off-by: af su <saf@zjuici.com>

* doc: add changelog entry for #2685

Signed-off-by: Jan Larwig <jan@larwig.com>

* refactor: added more verbose comments to some struct fields and minor code cleanup

Signed-off-by: Jan Larwig <jan@larwig.com>

---------

Signed-off-by: afsu <suaf2020@163.com>
Signed-off-by: af su <saf@zjuici.com>
Signed-off-by: Jan Larwig <jan@larwig.com>
Co-authored-by: af su <saf@zjuici.com>
Co-authored-by: Jan Larwig <jan@larwig.com>
This commit is contained in:
af su
2026-03-14 12:04:33 +08:00
committed by GitHub
co-authored by af su Jan Larwig
parent c6355ee402
commit e59f7c1549
14 changed files with 314 additions and 137 deletions
+64
View File
@@ -5,6 +5,7 @@ import (
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"fmt"
"math/big"
"net"
@@ -12,6 +13,8 @@ import (
"os"
"strings"
"time"
"github.com/spf13/cast"
)
func GetCertPool(paths []string, useSystemPool bool) (*x509.CertPool, error) {
@@ -191,3 +194,64 @@ func RemoveDuplicateStr(strSlice []string) []string {
}
return list
}
// CoerceClaim tries to convert the value into the destination interface type.
// If it can convert the value, it will then store the value in the destination
// interface.
func CoerceClaim(value, dst any) error {
switch d := dst.(type) {
case *string:
str, err := toString(value)
if err != nil {
return fmt.Errorf("could not convert value to string: %v", err)
}
*d = str
case *[]string:
strSlice, err := toStringSlice(value)
if err != nil {
return fmt.Errorf("could not convert value to string slice: %v", err)
}
*d = strSlice
case *bool:
*d = cast.ToBool(value)
default:
return fmt.Errorf("unknown type for destination: %T", dst)
}
return nil
}
// toStringSlice converts an interface (either a slice or single value) into
// a slice of strings.
func toStringSlice(value any) ([]string, error) {
var sliceValues []any
switch v := value.(type) {
case []any:
sliceValues = v
default:
sliceValues = []any{v}
}
out := []string{}
for _, v := range sliceValues {
str, err := toString(v)
if err != nil {
return nil, fmt.Errorf("could not convert slice entry to string %v: %v", v, err)
}
out = append(out, str)
}
return out, nil
}
// toString coerces a value into a string.
// If it is non-string, marshal it into JSON.
func toString(value any) (string, error) {
if str, err := cast.ToStringE(value); err == nil {
return str, nil
}
jsonStr, err := json.Marshal(value)
if err != nil {
return "", err
}
return string(jsonStr), nil
}
+70
View File
@@ -2,10 +2,13 @@ package util
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"os"
"reflect"
"testing"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
"github.com/stretchr/testify/assert"
)
@@ -253,3 +256,70 @@ func TestGetCertPool(t *testing.T) {
assert.Error(t, err3)
}
}
type coerceClaimTableInput struct {
name string
value any
dst any
expectedDst any
expectedError error
}
func TestCoerceClaim(t *testing.T) {
tests := []coerceClaimTableInput{
{
name: "coerces a string to a string",
value: "some_string",
dst: ptr.To(""),
expectedDst: ptr.To("some_string"),
},
{
name: "coerces a slice to a string slice",
value: []any{"a", "b"},
dst: ptr.To([]string{}),
expectedDst: ptr.To([]string{"a", "b"}),
},
{
name: "coerces a bool to a bool",
value: true,
dst: ptr.To(false),
expectedDst: ptr.To(true),
},
{
name: "coerces a string to a bool",
value: "true",
dst: ptr.To(false),
expectedDst: ptr.To(true),
},
{
name: "coerces a map to a string",
value: map[string]any{
"foo": []any{"bar", "baz"},
},
dst: ptr.To(""),
expectedDst: ptr.To("{\"foo\":[\"bar\",\"baz\"]}"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CoerceClaim(tt.value, tt.dst)
if tt.expectedError != nil {
if err == nil || err.Error() != tt.expectedError.Error() {
t.Errorf("expected error %v, got %v", tt.expectedError, err)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(tt.dst, tt.expectedDst) {
gotJSON, _ := json.Marshal(tt.dst)
wantJSON, _ := json.Marshal(tt.expectedDst)
t.Errorf("expected dst to be %+v, got %+v", string(wantJSON), string(gotJSON))
}
})
}
}