Skip to content

Commit

Permalink
feat: allow setting public and admin metadata with the jsonnet data m…
Browse files Browse the repository at this point in the history
…apper (#2569)

Closes #2552

Co-authored-by: aeneasr <3372410+aeneasr@users.noreply.github.com>
  • Loading branch information
JeffreyThijs and aeneasr authored Jul 13, 2022
1 parent 26f2618 commit aa6eb13
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 60 deletions.
21 changes: 17 additions & 4 deletions selfservice/strategy/oidc/strategy_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ import (
"github.com/ory/x/urlx"
)

type idTokenClaims struct {
traits struct {
website string
}
metadataPublic struct {
picture string
}
metadataAdmin struct {
phoneNumber string
}
}

func createClient(t *testing.T, remote string, redir, id string) {
require.NoError(t, resilience.Retry(logrusx.New("", ""), time.Second*10, time.Minute*2, func() error {
if req, err := http.NewRequest("DELETE", remote+"/clients/"+id, nil); err != nil {
Expand Down Expand Up @@ -72,7 +84,7 @@ func createClient(t *testing.T, remote string, redir, id string) {
}))
}

func newHydraIntegration(t *testing.T, remote *string, subject, website *string, scope *[]string, addr string) (*http.Server, string) {
func newHydraIntegration(t *testing.T, remote *string, subject *string, claims *idTokenClaims, scope *[]string, addr string) (*http.Server, string) {
router := httprouter.New()

type p struct {
Expand Down Expand Up @@ -125,7 +137,8 @@ func newHydraIntegration(t *testing.T, remote *string, subject, website *string,
require.NotEmpty(t, challenge)

var b bytes.Buffer
require.NoError(t, json.NewEncoder(&b).Encode(&p{GrantScope: *scope, Session: json.RawMessage(`{"id_token":{"website":"` + *website + `"}}`)}))
var msg = `{"id_token":{"website":"` + claims.traits.website + `","picture":"` + *&claims.metadataPublic.picture + `","phone_number":"` + *&claims.metadataAdmin.phoneNumber + `"}}`
require.NoError(t, json.NewEncoder(&b).Encode(&p{GrantScope: *scope, Session: json.RawMessage(msg)}))
href := urlx.MustJoin(*remote, "/oauth2/auth/requests/consent/accept") + "?consent_challenge=" + challenge
do(w, r, href, &b)
})
Expand Down Expand Up @@ -187,11 +200,11 @@ func newUI(t *testing.T, reg driver.Registry) *httptest.Server {
return ts
}

func newHydra(t *testing.T, subject, website *string, scope *[]string) (remoteAdmin, remotePublic, hydraIntegrationTSURL string) {
func newHydra(t *testing.T, subject *string, claims *idTokenClaims, scope *[]string) (remoteAdmin, remotePublic, hydraIntegrationTSURL string) {
remoteAdmin = os.Getenv("TEST_SELFSERVICE_OIDC_HYDRA_ADMIN")
remotePublic = os.Getenv("TEST_SELFSERVICE_OIDC_HYDRA_PUBLIC")

hydraIntegrationTS, hydraIntegrationTSURL := newHydraIntegration(t, &remoteAdmin, subject, website, scope, os.Getenv("TEST_SELFSERVICE_OIDC_HYDRA_INTEGRATION_ADDR"))
hydraIntegrationTS, hydraIntegrationTSURL := newHydraIntegration(t, &remoteAdmin, subject, claims, scope, os.Getenv("TEST_SELFSERVICE_OIDC_HYDRA_INTEGRATION_ADDR"))
t.Cleanup(func() {
require.NoError(t, hydraIntegrationTS.Close())
})
Expand Down
136 changes: 91 additions & 45 deletions selfservice/strategy/oidc/strategy_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import (
"net/http"
"time"

"github.com/ory/herodot"

"github.com/google/go-jsonnet"

"github.com/ory/x/fetcher"

"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

"github.com/ory/x/decoderx"
Expand All @@ -22,9 +27,6 @@ import (

"github.com/ory/kratos/continuity"

"github.com/google/go-jsonnet"
"github.com/tidwall/gjson"

"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/registration"
Expand All @@ -33,6 +35,13 @@ import (

var _ registration.Strategy = new(Strategy)

type MetadataType string

const (
PublicMetadata MetadataType = "identity.metadata_public"
AdminMetadata MetadataType = "identity.metadata_admin"
)

func (s *Strategy) RegisterRegistrationRoutes(r *x.RouterPublic) {
s.setRoutes(r)
}
Expand Down Expand Up @@ -186,29 +195,70 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a
return nil, s.handleError(w, r, a, provider.Config().ID, nil, err)
}

i, err := s.createIdentity(w, r, a, claims, provider, container, jn)
if err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

// Validate the identity itself
if err := s.d.IdentityValidator().Validate(r.Context(), i); err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

var it string
if idToken, ok := token.Extra("id_token").(string); ok {
if it, err = s.d.Cipher().Encrypt(r.Context(), []byte(idToken)); err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}
}

cat, err := s.d.Cipher().Encrypt(r.Context(), []byte(token.AccessToken))
if err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

crt, err := s.d.Cipher().Encrypt(r.Context(), []byte(token.RefreshToken))
if err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

creds, err := identity.NewCredentialsOIDC(it, cat, crt, provider.Config().ID, claims.Subject)
if err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

i.SetCredentials(s.ID(), *creds)
if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, identity.CredentialsTypeOIDC, a, i); err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

return nil, nil
}

func (s *Strategy) createIdentity(w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, container *authCodeContainer, jn *bytes.Buffer) (*identity.Identity, error) {
var jsonClaims bytes.Buffer
if err := json.NewEncoder(&jsonClaims).Encode(claims); err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, nil, err)
}

i := identity.NewIdentity(s.d.Config(r.Context()).DefaultIdentityTraitsSchemaID())

vm := jsonnet.MakeVM()
vm.ExtCode("claims", jsonClaims.String())
evaluated, err := vm.EvaluateAnonymousSnippet(provider.Config().Mapper, jn.String())
if err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, nil, err)
} else if traits := gjson.Get(evaluated, "identity.traits"); !traits.IsObject() {
i.Traits = []byte{'{', '}'}
s.d.Logger().
WithRequest(r).
WithField("oidc_provider", provider.Config().ID).
WithSensitiveField("oidc_claims", claims).
WithField("mapper_jsonnet_output", evaluated).
WithField("mapper_jsonnet_url", provider.Config().Mapper).
Error("OpenID Connect Jsonnet mapper did not return an object for key identity.traits. Please check your Jsonnet code!")
} else {
i.Traits = []byte(traits.Raw)
}

i := identity.NewIdentity(s.d.Config(r.Context()).DefaultIdentityTraitsSchemaID())
if err := s.setTraits(w, r, a, claims, provider, container, evaluated, i); err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

if err := s.setMetadata(evaluated, i, PublicMetadata); err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

if err := s.setMetadata(evaluated, i, AdminMetadata); err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

s.d.Logger().
Expand All @@ -218,51 +268,47 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a
WithSensitiveField("mapper_jsonnet_output", evaluated).
WithField("mapper_jsonnet_url", provider.Config().Mapper).
Debug("OpenID Connect Jsonnet mapper completed.")
return i, nil
}

i.Traits, err = merge(container.Traits, json.RawMessage(i.Traits))
func (s *Strategy) setTraits(w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, container *authCodeContainer, evaluated string, i *identity.Identity) error {
jsonTraits := gjson.Get(evaluated, "identity.traits")
if !jsonTraits.IsObject() {
return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("OpenID Connect Jsonnet mapper did not return an object for key identity.traits. Please check your Jsonnet code!"))
}

traits, err := merge(container.Traits, json.RawMessage(jsonTraits.Raw))
if err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, nil, err)
return s.handleError(w, r, a, provider.Config().ID, nil, err)
}

i.Traits = traits
s.d.Logger().
WithRequest(r).
WithField("oidc_provider", provider.Config().ID).
WithSensitiveField("identity_traits", i.Traits).
WithSensitiveField("mapper_jsonnet_output", evaluated).
WithField("mapper_jsonnet_url", provider.Config().Mapper).
Debug("Merged form values and OpenID Connect Jsonnet output.")
return nil
}

// Validate the identity itself
if err := s.d.IdentityValidator().Validate(r.Context(), i); err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

var it string
if idToken, ok := token.Extra("id_token").(string); ok {
if it, err = s.d.Cipher().Encrypt(r.Context(), []byte(idToken)); err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}
}

cat, err := s.d.Cipher().Encrypt(r.Context(), []byte(token.AccessToken))
if err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

crt, err := s.d.Cipher().Encrypt(r.Context(), []byte(token.RefreshToken))
if err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
func (s *Strategy) setMetadata(evaluated string, i *identity.Identity, m MetadataType) error {
if m != PublicMetadata && m != AdminMetadata {
return errors.Errorf("undefined metadata type: %s", m)
}

creds, err := identity.NewCredentialsOIDC(it, cat, crt, provider.Config().ID, claims.Subject)
if err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
metadata := gjson.Get(evaluated, string(m))
if !metadata.IsObject() {
return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("OpenID Connect Jsonnet mapper did not return an object for key %s. Please check your Jsonnet code!", m))
}

i.SetCredentials(s.ID(), *creds)
if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, identity.CredentialsTypeOIDC, a, i); err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
switch m {
case PublicMetadata:
i.MetadataPublic = []byte(metadata.Raw)
case AdminMetadata:
i.MetadataAdmin = []byte(metadata.Raw)
}

return nil, nil
return nil
}
4 changes: 2 additions & 2 deletions selfservice/strategy/oidc/strategy_settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ func TestSettingsStrategy(t *testing.T) {
var (
conf, reg = internal.NewFastRegistryWithMocks(t)
subject string
website string
claims idTokenClaims
scope []string
)

remoteAdmin, remotePublic, _ := newHydra(t, &subject, &website, &scope)
remoteAdmin, remotePublic, _ := newHydra(t, &subject, &claims, &scope)
uiTS := newUI(t, reg)
errTS := testhelpers.NewErrorTestServer(t, reg)
publicTS, adminTS := testhelpers.NewKratosServers(t)
Expand Down
25 changes: 16 additions & 9 deletions selfservice/strategy/oidc/strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ func TestStrategy(t *testing.T) {
}

var (
conf, reg = internal.NewFastRegistryWithMocks(t)
subject, website string
scope []string
conf, reg = internal.NewFastRegistryWithMocks(t)
subject string
claims idTokenClaims
scope []string
)

remoteAdmin, remotePublic, hydraIntegrationTSURL := newHydra(t, &subject, &website, &scope)
remoteAdmin, remotePublic, hydraIntegrationTSURL := newHydra(t, &subject, &claims, &scope)
returnTS := newReturnTs(t, reg)
uiTS := newUI(t, reg)
errTS := testhelpers.NewErrorTestServer(t, reg)
Expand Down Expand Up @@ -176,6 +177,8 @@ func TestStrategy(t *testing.T) {
var ai = func(t *testing.T, res *http.Response, body []byte) {
assert.Contains(t, res.Request.URL.String(), returnTS.URL)
assert.Equal(t, subject, gjson.GetBytes(body, "identity.traits.subject").String(), "%s", body)
assert.Equal(t, claims.traits.website, gjson.GetBytes(body, "identity.traits.website").String(), "%s", body)
assert.Equal(t, claims.metadataPublic.picture, gjson.GetBytes(body, "identity.metadata_public.picture").String(), "%s", body)
}

var newLoginFlow = func(t *testing.T, redirectTo string, exp time.Duration) (req *login.Flow) {
Expand Down Expand Up @@ -386,7 +389,10 @@ func TestStrategy(t *testing.T) {
t.Run("case=register, merge, and complete data", func(t *testing.T) {
subject = "incomplete-data@ory.sh"
scope = []string{"openid"}
website = "https://www.ory.sh/kratos"
claims = idTokenClaims{}
claims.traits.website = "https://www.ory.sh/kratos"
claims.metadataPublic.picture = "picture.png"
claims.metadataAdmin.phoneNumber = "911"

t.Run("case=should fail registration on first attempt", func(t *testing.T) {
r := newRegistrationFlow(t, returnTS.URL, time.Minute)
Expand Down Expand Up @@ -649,13 +655,14 @@ func TestDisabledEndpoint(t *testing.T) {

func TestPostEndpointRedirect(t *testing.T) {
var (
conf, reg = internal.NewFastRegistryWithMocks(t)
subject, website string
scope []string
conf, reg = internal.NewFastRegistryWithMocks(t)
subject string
claims idTokenClaims
scope []string
)
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), true)

remoteAdmin, remotePublic, _ := newHydra(t, &subject, &website, &scope)
remoteAdmin, remotePublic, _ := newHydra(t, &subject, &claims, &scope)

publicTS, adminTS := testhelpers.NewKratosServers(t)

Expand Down
6 changes: 6 additions & 0 deletions selfservice/strategy/oidc/stub/oidc.hydra.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@ else
subject: claims.sub,
[if "website" in claims then "website" else null]: claims.website,
},
metadata_public: {
[if "picture" in claims then "picture" else null]: claims.picture,
},
metadata_admin: {
[if "phone_number" in claims then "phone_number" else null]: claims.phone_number,
}
},
}
16 changes: 16 additions & 0 deletions selfservice/strategy/oidc/stub/registration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@
"required": [
"subject"
]
},
"metadata_public": {
"type": "object",
"properties": {
"picture": {
"type": "string"
}
}
},
"metadata_admin": {
"type": "object",
"properties": {
"phone_number": {
"type": "string"
}
}
}
},
"additionalProperties": false
Expand Down

0 comments on commit aa6eb13

Please sign in to comment.