Add OIDC token template transformation

pull/1673/head
Herman Slatman 5 months ago
parent 2c27e865cb
commit 0ad381b092
No known key found for this signature in database
GPG Key ID: F4D8A44EA0A75A4F

@ -74,6 +74,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
InsecureSkipSignatureCheck: true,
Now: time.Now,
},
TransformTemplate: "",
},
DPOP: &wire.DPOPOptions{
SigningKey: []byte(fakeKey),

@ -25,6 +25,7 @@ import (
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/fxamacker/cbor/v2"
"github.com/google/go-tpm/legacy/tpm2"
"github.com/smallstep/go-attestation/attest"
@ -36,6 +37,7 @@ import (
"github.com/smallstep/certificates/acme/wire"
"github.com/smallstep/certificates/authority/provisioner"
wireprovisioner "github.com/smallstep/certificates/authority/provisioner/wire"
)
type ChallengeType string
@ -408,8 +410,8 @@ func wireOIDC01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSO
"keyAuthorization does not match; expected %q, but got %q", expectedKeyAuth, oidcPayload.KeyAuth))
}
if wireID.Name != claims.Name || wireID.Handle != claims.Handle {
return storeError(ctx, db, ch, false, NewError(ErrorRejectedIdentifierType, "claims in OIDC ID token don't match"))
if err := validateWireOIDCClaims(oidcOptions, idToken, wireID); err != nil {
return storeError(ctx, db, ch, true, WrapError(ErrorRejectedIdentifierType, err, "claims in OIDC ID token don't match"))
}
// Update and store the challenge.
@ -446,6 +448,35 @@ func wireOIDC01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSO
return nil
}
func validateWireOIDCClaims(o *wireprovisioner.OIDCOptions, token *oidc.IDToken, wireID wire.ID) error {
var m map[string]any
if err := token.Claims(&m); err != nil {
return fmt.Errorf("failed extracting OIDC ID token claims: %w", err)
}
transformed, err := o.Transform(m)
if err != nil {
return fmt.Errorf("failed transforming OIDC ID token: %w", err)
}
name, ok := transformed["name"]
if !ok {
return fmt.Errorf("transformed OIDC ID token does not contain 'name'")
}
if wireID.Name != name {
return fmt.Errorf("invalid 'name' %q after transformation", name)
}
handle, ok := transformed["handle"]
if !ok {
return fmt.Errorf("transformed OIDC ID token does not contain 'handle'")
}
if wireID.Handle != handle {
return fmt.Errorf("invalid 'handle' %q after transformation", handle)
}
return nil
}
type wireDpopPayload struct {
// AccessToken is the token generated by wire-server
AccessToken string `json:"access_token"`
@ -496,7 +527,8 @@ func wireDPOP01Validate(ctx context.Context, ch *Challenge, db DB, accountJWK *j
}
_, dpop, err := parseAndVerifyWireAccessToken(params)
if err != nil {
return WrapErrorISE(err, "failed validating token")
return storeError(ctx, db, ch, true, WrapError(ErrorRejectedIdentifierType, err,
"failed validating Wire access token"))
}
// Update and store the challenge.

@ -40,6 +40,7 @@ import (
"github.com/smallstep/certificates/acme/wire"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
wireprovisioner "github.com/smallstep/certificates/authority/provisioner/wire"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -4371,3 +4372,85 @@ MCowBQYDK2VwAyEAB2IYqBWXAouDt3WcCZgCM3t9gumMEKMlgMsGenSu+fA=
assert.Equal(t, "wire", dt["team"].(string))
}
}
func createWireOptions(t *testing.T, transformTemplate string) *wireprovisioner.Options {
t.Helper()
fakeKey := `
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
-----END PUBLIC KEY-----`
opts := &wireprovisioner.Options{
OIDC: &wireprovisioner.OIDCOptions{
Provider: &wireprovisioner.Provider{
IssuerURL: "https://issuer.example.com",
AuthURL: "",
TokenURL: "",
JWKSURL: "",
UserInfoURL: "",
Algorithms: []string{},
},
Config: &wireprovisioner.Config{
ClientID: "unit test",
SignatureAlgorithms: []string{},
SkipClientIDCheck: true,
SkipExpiryCheck: true,
SkipIssuerCheck: true,
InsecureSkipSignatureCheck: true,
Now: time.Now,
},
TransformTemplate: transformTemplate,
},
DPOP: &wireprovisioner.DPOPOptions{
SigningKey: []byte(fakeKey),
},
}
err := opts.Validate()
require.NoError(t, err)
return opts
}
func Test_idTokenTransformation(t *testing.T) {
// {"name": "wireapp://%40alice_wire@wire.com", "handle": "Alice Smith", "iss": "http://dex:15818/dex"}
idTokenString := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjZhNDZlYzQ3YTQzYWI1ZTc4NzU3MzM5NWY1MGY4ZGQ5MWI2OTM5MzcifQ.eyJpc3MiOiJodHRwOi8vZGV4OjE1ODE4L2RleCIsInN1YiI6IkNqcDNhWEpsWVhCd09pOHZTMmh0VjBOTFpFTlRXakoyT1dWTWFHRk9XVlp6WnlFeU5UZzFNVEpoT0RRek5qTXhaV1V6UUhkcGNtVXVZMjl0RWdSc1pHRnciLCJhdWQiOiJ3aXJlYXBwIiwiZXhwIjoxNzA1MDkxNTYyLCJpYXQiOjE3MDUwMDUxNjIsIm5vbmNlIjoib0VjUzBRQUNXLVIyZWkxS09wUmZ2QSIsImF0X2hhc2giOiJoYzk0NmFwS25FeEV5TDVlSzJZMzdRIiwiY19oYXNoIjoidmRubFp2V1d1bVd1Z2NYR1JpOU5FUSIsIm5hbWUiOiJ3aXJlYXBwOi8vJTQwYWxpY2Vfd2lyZUB3aXJlLmNvbSIsInByZWZlcnJlZF91c2VybmFtZSI6IkFsaWNlIFNtaXRoIn0.aEBhWJugBJ9J_0L_4odUCg8SR8HMXVjd__X8uZRo42BSJQQO7-wdpy0jU3S4FOX9fQKr68wD61gS_QsnhfiT7w9U36mLpxaYlNVDCYfpa-gklVFit_0mjUOukXajTLK6H527TGiSss8z22utc40ckS1SbZa2BzKu3yOcqnFHUQwQc5sLYfpRABTB6WBoYFtnWDzdpyWJDaOzz7lfKYv2JBnf9vV8u8SYm-6gNKgtiQ3UUnjhIVUjdfHet2BMvmV2ooZ8V441RULCzKKG_sWZba-D_k_TOnSholGobtUOcKHlmVlmfUe8v7kuyBdhbPcembfgViaNldLQGKZjZfgvLg`
var claims struct {
Name string `json:"name,omitempty"`
Handle string `json:"preferred_username,omitempty"`
Issuer string `json:"iss,omitempty"`
}
idToken, err := jose.ParseSigned(idTokenString)
require.NoError(t, err)
err = idToken.UnsafeClaimsWithoutVerification(&claims)
require.NoError(t, err)
// original token contains "Alice Smith" as handle, and name as "wireapp://%40alice_wire@wire.com"
assert.Equal(t, "Alice Smith", claims.Handle)
assert.Equal(t, "wireapp://%40alice_wire@wire.com", claims.Name)
assert.Equal(t, "http://dex:15818/dex", claims.Issuer)
var m map[string]any
err = idToken.UnsafeClaimsWithoutVerification(&m)
require.NoError(t, err)
opts := createWireOptions(t, "") // uses default transformation template
result, err := opts.GetOIDCOptions().Transform(m)
require.NoError(t, err)
// default transformation sets preferred username to handle; name as name
assert.Equal(t, "Alice Smith", result["handle"].(string))
assert.Equal(t, "wireapp://%40alice_wire@wire.com", result["name"].(string))
assert.Equal(t, "http://dex:15818/dex", result["iss"].(string))
// swap the preferred_name and the name
swap := `{"name": "{{ .preferred_username }}", "handle": "{{ .name }}"}`
opts = createWireOptions(t, swap)
result, err = opts.GetOIDCOptions().Transform(m)
require.NoError(t, err)
// with the transformation, handle now contains wireapp://%40alice_wire@wire.com, name contains Alice Smith
assert.Equal(t, "wireapp://%40alice_wire@wire.com", result["handle"].(string))
assert.Equal(t, "Alice Smith", result["name"].(string))
assert.Equal(t, "http://dex:15818/dex", result["iss"].(string))
}

@ -3,6 +3,7 @@ package wire
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
@ -32,11 +33,13 @@ type Config struct {
}
type OIDCOptions struct {
Provider *Provider `json:"provider,omitempty"`
Config *Config `json:"config,omitempty"`
Provider *Provider `json:"provider,omitempty"`
Config *Config `json:"config,omitempty"`
TransformTemplate string `json:"transform,omitempty"`
oidcProviderConfig *oidc.ProviderConfig
target *template.Template
transform *template.Template
}
func (o *OIDCOptions) GetProvider(ctx context.Context) *oidc.Provider {
@ -62,6 +65,8 @@ func (o *OIDCOptions) GetConfig() *oidc.Config {
}
}
const defaultTemplate = `{"name": "{{ .name }}", "handle": "{{ .preferred_username }}"}`
func (o *OIDCOptions) validateAndInitialize() (err error) {
if o.Provider == nil {
return errors.New("provider not set")
@ -80,6 +85,15 @@ func (o *OIDCOptions) validateAndInitialize() (err error) {
return fmt.Errorf("failed parsing OIDC template: %w", err)
}
transformTemplate := defaultTemplate
if o.TransformTemplate != "" {
transformTemplate = o.TransformTemplate
}
o.transform, err = template.New("transform").Parse(transformTemplate)
if err != nil {
return fmt.Errorf("failed parsing OIDC transformation template: %w", err)
}
return nil
}
@ -91,6 +105,27 @@ func (o *OIDCOptions) EvaluateTarget(deviceID string) (string, error) {
return buf.String(), nil
}
func (o *OIDCOptions) Transform(v map[string]any) (map[string]any, error) {
if o.transform == nil || v == nil {
return v, nil
}
buf := new(bytes.Buffer)
if err := o.transform.Execute(buf, v); err != nil {
return nil, fmt.Errorf("failed executing OIDC transformation: %w", err)
}
var r map[string]any
if err := json.Unmarshal(buf.Bytes(), &r); err != nil {
return nil, fmt.Errorf("failed unmarshaling transformed OIDC token: %w", err)
}
// add original claims if not yet in the transformed result
for key, value := range v {
if _, ok := r[key]; !ok {
r[key] = value
}
}
return r, nil
}
func toOIDCProviderConfig(in *Provider) (*oidc.ProviderConfig, error) {
issuerURL, err := url.Parse(in.IssuerURL)
if err != nil {

@ -0,0 +1,104 @@
package wire
import (
"testing"
"text/template"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOIDCOptions_Transform(t *testing.T) {
defaultTransform, err := template.New("defaultTransform").Parse(`{"name": "{{ .name }}", "handle": "{{ .preferred_username }}"}`)
require.NoError(t, err)
swapTransform, err := template.New("swapTransform").Parse(`{"name": "{{ .preferred_username }}", "handle": "{{ .name }}"}`)
require.NoError(t, err)
type fields struct {
transform *template.Template
}
type args struct {
v map[string]any
}
tests := []struct {
name string
fields fields
args args
want map[string]any
expectedErr error
}{
{
name: "ok/no-transform",
fields: fields{
transform: nil,
},
args: args{
v: map[string]any{
"name": "Example",
"preferred_username": "Preferred",
},
},
want: map[string]any{
"name": "Example",
"preferred_username": "Preferred",
},
},
{
name: "ok/empty-data",
fields: fields{
transform: nil,
},
args: args{
v: map[string]any{},
},
want: map[string]any{},
},
{
name: "ok/default-transform",
fields: fields{
transform: defaultTransform,
},
args: args{
v: map[string]any{
"name": "Example",
"preferred_username": "Preferred",
},
},
want: map[string]any{
"name": "Example",
"handle": "Preferred",
"preferred_username": "Preferred",
},
},
{
name: "ok/swap-transform",
fields: fields{
transform: swapTransform,
},
args: args{
v: map[string]any{
"name": "Example",
"preferred_username": "Preferred",
},
},
want: map[string]any{
"name": "Preferred",
"handle": "Example",
"preferred_username": "Preferred",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &OIDCOptions{
transform: tt.fields.transform,
}
got, err := o.Transform(tt.args.v)
if tt.expectedErr != nil {
assert.Error(t, err)
return
}
assert.Equal(t, tt.want, got)
})
}
}

@ -83,6 +83,22 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
},
expectedErr: errors.New(`failed initializing OIDC options: failed parsing OIDC template: template: DeviceID:1: unexpected "}" in command`),
},
{
name: "fail/invalid-transform-template",
fields: fields{
OIDC: &OIDCOptions{
Provider: &Provider{
IssuerURL: "https://example.com",
},
Config: &Config{},
TransformTemplate: "{{}",
},
DPOP: &DPOPOptions{
SigningKey: key,
},
},
expectedErr: errors.New(`failed initializing OIDC options: failed parsing OIDC transformation template: template: transform:1: unexpected "}" in command`),
},
{
name: "fail/no-dpop-options",
fields: fields{

Loading…
Cancel
Save