Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow marking OIDC provider-verified addresses as verified during registration #3448

Merged
merged 3 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 59 additions & 9 deletions selfservice/strategy/oidc/strategy_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import (
"bytes"
"encoding/json"
"net/http"
"strings"
"time"

"github.com/ory/x/sqlxx"

"github.com/ory/herodot"

"github.com/ory/x/fetcher"
Expand Down Expand Up @@ -38,7 +41,14 @@ var _ registration.Strategy = new(Strategy)

type MetadataType string

type VerifiedAddress struct {
Value string `json:"value"`
Via identity.VerifiableAddressType `json:"via"`
}

const (
VerifiedAddressesKey = "identity.verified_addresses"

PublicMetadata MetadataType = "identity.metadata_public"
AdminMetadata MetadataType = "identity.metadata_admin"
)
Expand Down Expand Up @@ -249,7 +259,7 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, r
return nil, s.handleError(w, r, rf, provider.Config().ID, nil, err)
}

i, err := s.createIdentity(w, r, rf, claims, provider, container, jn)
i, va, err := s.createIdentity(w, r, rf, claims, provider, container, jn)
if err != nil {
return nil, s.handleError(w, r, rf, provider.Config().ID, nil, err)
}
Expand All @@ -259,6 +269,18 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, r
return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err)
}

for n := range i.VerifiableAddresses {
verifiable := &i.VerifiableAddresses[n]
for _, verified := range va {
if verifiable.Via == verified.Via && verifiable.Value == verified.Value {
verifiable.Status = identity.VerifiableAddressStatusCompleted
verifiable.Verified = true
t := sqlxx.NullTime(time.Now().UTC().Round(time.Second))
verifiable.VerifiedAt = &t
}
}
}

var it string
if idToken, ok := token.Extra("id_token").(string); ok {
if it, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(idToken)); err != nil {
Expand Down Expand Up @@ -289,34 +311,39 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, r
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) {
func (s *Strategy) createIdentity(w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, container *authCodeContainer, jn *bytes.Buffer) (*identity.Identity, []VerifiedAddress, 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)
return nil, nil, s.handleError(w, r, a, provider.Config().ID, nil, err)
}

vm, err := s.d.JsonnetVM(r.Context())
if err != nil {
return nil, s.handleError(w, r, a, provider.Config().ID, nil, err)
return nil, nil, s.handleError(w, r, a, provider.Config().ID, nil, err)
}

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)
return nil, nil, s.handleError(w, r, a, provider.Config().ID, nil, err)
}

i := identity.NewIdentity(s.d.Config().DefaultIdentityTraitsSchemaID(r.Context()))
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)
return nil, 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)
return nil, 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)
return nil, nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err)
}

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

s.d.Logger().
Expand All @@ -326,7 +353,7 @@ func (s *Strategy) createIdentity(w http.ResponseWriter, r *http.Request, a *reg
WithSensitiveField("mapper_jsonnet_output", evaluated).
WithField("mapper_jsonnet_url", provider.Config().Mapper).
Debug("OpenID Connect Jsonnet mapper completed.")
return i, nil
return i, va, nil
}

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 {
Expand Down Expand Up @@ -370,3 +397,26 @@ func (s *Strategy) setMetadata(evaluated string, i *identity.Identity, m Metadat

return nil
}

func (s *Strategy) extractVerifiedAddresses(evaluated string) ([]VerifiedAddress, error) {
if verifiedAddresses := gjson.Get(evaluated, VerifiedAddressesKey); verifiedAddresses.Exists() {
if !verifiedAddresses.IsArray() {
return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("OpenID Connect Jsonnet mapper did not return an array for key %s. Please check your Jsonnet code!", VerifiedAddressesKey))
}

var va []VerifiedAddress
if err := json.Unmarshal([]byte(verifiedAddresses.Raw), &va); err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Failed to unmarshal value for key %s. Please check your Jsonnet code!", VerifiedAddressesKey).WithDebugf("%s", err))
}

for _, va := range va {
if va.Via == identity.VerifiableAddressTypeEmail {
va.Value = strings.ToLower(strings.TrimSpace(va.Value))
}
}

return va, nil
}

return nil, nil
}
32 changes: 32 additions & 0 deletions selfservice/strategy/oidc/strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,38 @@ func TestStrategy(t *testing.T) {
})
})

t.Run("case=verified addresses should be respected", func(t *testing.T) {
scope = []string{"openid"}

testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration-verifiable-email.schema.json")

var assertVerifiedEmail = func(t *testing.T, body []byte, verified bool) {
assert.Len(t, gjson.GetBytes(body, "identity.verifiable_addresses").Array(), 1, "%s", body)
assert.Equal(t, "email", gjson.GetBytes(body, "identity.verifiable_addresses.0.via").String(), "%s", body)
assert.Equal(t, subject, gjson.GetBytes(body, "identity.verifiable_addresses.0.value").String(), "%s", body)
assert.Equal(t, verified, gjson.GetBytes(body, "identity.verifiable_addresses.0.verified").Bool(), "%s", body)
}

t.Run("case=should have verified address when subject matches", func(t *testing.T) {
subject = "verified-email@ory.sh"
r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute)
action := assertFormValues(t, r.ID, "valid")
res, body := makeRequest(t, "valid", action, url.Values{})
assertIdentity(t, res, body)
assertVerifiedEmail(t, body, true)
})

t.Run("case=should have unverified address when subject does not match", func(t *testing.T) {
subject = "changed-verified-email@ory.sh"
r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute)
action := assertFormValues(t, r.ID, "valid")
res, body := makeRequest(t, "valid", action, url.Values{"traits.subject": {"unverified-email@ory.sh"}})
subject = "unverified-email@ory.sh"
assertIdentity(t, res, body)
assertVerifiedEmail(t, body, false)
})
})

t.Run("method=TestPopulateSignUpMethod", func(t *testing.T) {
conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://foo/")

Expand Down
5 changes: 4 additions & 1 deletion selfservice/strategy/oidc/stub/oidc.hydra.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ else
},
metadata_admin: {
[if "phone_number" in claims then "phone_number" else null]: claims.phone_number,
}
},
verified_addresses: [
{ via: "email", value: claims.sub },
],
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"$id": "https://example.com/person.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"subject": {
"format": "email",
"type": "string",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
}
}
},
"name": {
"type": "string",
"minLength": 2
},
"website": {
"type": "string",
"format": "uri"
},
"groups": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"subject"
]
},
"metadata_public": {
"type": "object",
"properties": {
"picture": {
"type": "string"
}
}
},
"metadata_admin": {
"type": "object",
"properties": {
"phone_number": {
"type": "string"
}
}
}
},
"additionalProperties": false
}
Loading