Skip to content
Draft
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
16 changes: 16 additions & 0 deletions identity/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,22 @@ func (h *Handler) deleteIdentityCredentials(w http.ResponseWriter, r *http.Reque
return
}
}
case CredentialsTypeCodeAuth:
// The code credential acts as a first factor only when passwordless login
// with code is enabled. As a pure second factor (MFA) it is freely
// removable, like TOTP or lookup secrets.
if h.r.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled {
firstFactor, err := h.r.IdentityManager().CountActiveFirstFactorCredentials(ctx, identity)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
if firstFactor < 2 {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReason("You cannot remove the last first factor credential.")))
return
}
}
identity.DeleteCredentialsType(cred.Type)
default:
// A bunch of credential type deletions are not yet implemented, e.g. passkeys, etc.
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Credentials type %s cannot be deleted.", cred.Type)))
Expand Down
40 changes: 40 additions & 0 deletions identity/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2856,6 +2856,46 @@ func TestHandler(t *testing.T) {
assert.Equal(t, "{}", string(creds.Config))
assert.Equal(t, i.ID, actual.ID)
})
t.Run("type=delete a code credential/"+name, func(t *testing.T) {
i := createIdentity(M{
identity.CredentialsTypePassword: {
Config: []byte(`{"hashed_password":"some_valid_hash"}`),
Identifiers: []string{x.NewUUID().String()},
},
identity.CredentialsTypeCodeAuth: {
Config: []byte(`{"addresses":[{"channel":"email","address":"delete-code@ory.sh"}]}`),
Identifiers: []string{"delete-code@ory.sh"},
},
})(t)
remove(t, ts, "/identities/"+i.ID.String()+"/credentials/code", http.StatusNoContent)
res := get(t, ts, "/identities/"+i.ID.String(), http.StatusOK)
assert.False(t, res.Get("credentials.code").Exists(), "code credential should be removed: %s", res.Raw)
assert.True(t, res.Get("credentials.password").Exists(), "password credential should remain: %s", res.Raw)
})
t.Run("type=deny to remove code as last first factor when passwordless enabled/"+name, func(t *testing.T) {
ctx := context.Background()
reg.Config().MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true)
t.Cleanup(func() {
reg.Config().MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", false)
})
codeAddress := x.NewUUID().String() + "@ory.sh"
i := createIdentity(M{
identity.CredentialsTypeCodeAuth: {
Config: []byte(fmt.Sprintf(`{"addresses":[{"channel":"email","address":%q}]}`, codeAddress)),
Identifiers: []string{codeAddress},
},
})(t)
req, err := http.NewRequest("DELETE", ts.URL+"/identities/"+i.ID.String()+"/credentials/code", nil)
require.NoError(t, err)
httpRes, err := ts.Client().Do(req)
require.NoError(t, err)
body := ioutilx.MustReadAll(httpRes.Body)
require.NoError(t, httpRes.Body.Close())
require.EqualValuesf(t, http.StatusBadRequest, httpRes.StatusCode, "%s", body)
assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), "You cannot remove the last first factor credential.")
res := get(t, ts, "/identities/"+i.ID.String(), http.StatusOK)
assert.True(t, res.Get("credentials.code").Exists(), "code credential should remain after blocked delete: %s", res.Raw)
})
t.Run("type=remove oidc type/"+name, func(t *testing.T) {
// force ordering among github identifiers
githubSubject := "0" + randx.MustString(7, randx.Numeric)
Expand Down
Loading