Skip to content

Commit

Permalink
feat: allow marking OIDC provider-verified addresses as verified duri…
Browse files Browse the repository at this point in the history
…ng registration
  • Loading branch information
Saancreed committed Aug 18, 2023
1 parent 0827ae5 commit 32772cd
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 9 deletions.
65 changes: 56 additions & 9 deletions selfservice/strategy/oidc/strategy_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"encoding/json"
"net/http"
"strings"
"time"

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

type MetadataType string

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

const (
VerifiedAddressesKey = "verified_addresses"

PublicMetadata MetadataType = "identity.metadata_public"
AdminMetadata MetadataType = "identity.metadata_admin"
)
Expand Down Expand Up @@ -249,7 +257,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 +267,17 @@ 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
verifiable.VerifiedAt = i.StateChangedAt
}
}
}

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 +308,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 +350,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 +394,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.ErrInternalServerError.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.ErrInternalServerError.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 = "[email protected]"
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 = "[email protected]"
r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute)
action := assertFormValues(t, r.ID, "valid")
res, body := makeRequest(t, "valid", action, url.Values{"traits.subject": {"[email protected]"}})
subject = "[email protected]"
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
3 changes: 3 additions & 0 deletions selfservice/strategy/oidc/stub/oidc.hydra.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ else
[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
}

0 comments on commit 32772cd

Please sign in to comment.