Repository: ory/kratos
Pull request #1312 (2021-05-04 16:24:23)

feat(identities): add a state to identities
## Related issue

https://github.com/ory/kratos/issues/598

<!--
Please link the GitHub issue this pull request resolves in the format of `#1234`. If you discussed this change
with a maintainer, please mention her/him using the `@` syntax (e.g. `@aeneasr`).

If this change neither resolves an existing issue nor has sign-off from one of the maintainers, there is a
chance substantial changes will be requested or that the changes will be rejected.

You can discuss changes with maintainers either in the Github Discusssions in this repository or
join the [Ory Chat](https://www.ory.sh/chat).
-->

## Proposed changes

Add a `disabled_at` field in the identity model. If it is set, the account is disabled. Block session creation when the account is disabled.

## Checklist

<!--
Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of
them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.
-->

- [x] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md).
- [x] I have read the [security policy](../security/policy).
- [x] I confirm that this pull request does not address a security
      vulnerability. If this pull request addresses a security. vulnerability, I
      confirm that I got green light (please contact
      [security@ory.sh](mailto:security@ory.sh)) from the maintainers to push
      the changes.
- [x] I have added tests that prove my fix is effective or that my feature
      works.
- [x] I have added or changed [the documentation](docs/docs).

## Further comments

<!--
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution
you did and what alternatives you considered, etc...
-->

This is draft just to make sure the implementation is correct. Tests and documentation are very much needed, but before going further, I'd rather have a validation from the maintainers.


CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/ory/kratos?pullRequest=1312) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/ory/kratos?pullRequest=1312) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/ory/kratos?pullRequest=1312) it.</sub>
aeneasr: Thank you for the contribution! I think what I meant in the original issue was more along the lines of adding a `State` parameter that is an enum of the different states. That way, we don't have to rely on nil values but instead have a clear cut singular state object :)
thomasruiz: Would there be another state besides active and disabled? Having a date to know when the user was disabled is a good thing, it allows automatic cleanup and other things. We could have a `state` field and a `state_changed_at` but... I don't see any other state rather that active and disabled. :p 
zepatrik: `invited` might be a possible candidate later on (e.g. to implement #595). I am pretty sure there will be some more coming up :wink: 
thomasruiz: Alright, I see the point. This should be more what you guys had in mind. It still needs tests, and I'm thinking about adding a PATCH method to the identities controller. What do you think?
aeneasr: Sorry @thomasruiz for dropping the ball on this. Would you still be open to work on this or have you proceeded to other work?
aeneasr: To regenerate the SDK run `make sdk` in the root directory :)
aeneasr: I think this is ready for review right? :) If so can you please mark it as ready for review? Thank you! :)
thomasruiz: > I think this is ready for review right? :) If so can you please mark it as ready for review? Thank you! :)

There you go! I hope I didn't mess anything up :) 

Patch:
diff --git a/cmd/daemon/serve.go b/cmd/daemon/serve.go
index 43f607089..60a1cd345 100644
--- a/cmd/daemon/serve.go
+++ b/cmd/daemon/serve.go
@@ -13,7 +13,7 @@ import (
 
 	"github.com/rs/cors"
 
-	"github.com/ory/kratos/metrics/prometheus"
+	prometheus "github.com/ory/x/prometheusx"
 
 	"github.com/ory/analytics-go/v4"
 
@@ -82,6 +82,7 @@ func ServePublic(r driver.Registry, wg *sync.WaitGroup, cmd *cobra.Command, args
 	n.UseHandler(r.CSRFHandler())
 
 	r.RegisterPublicRoutes(ctx, router)
+	r.PrometheusManager().RegisterRouter(router.Router)
 	n.Use(reqlog.NewMiddlewareFromLogger(l, "public#"+c.SelfPublicURL(nil).String()))
 	n.Use(sqa(cmd, r))
 	n.Use(r.PrometheusManager())
@@ -127,6 +128,7 @@ func ServeAdmin(r driver.Registry, wg *sync.WaitGroup, cmd *cobra.Command, args
 
 	router := x.NewRouterAdmin()
 	r.RegisterAdminRoutes(ctx, router)
+	r.PrometheusManager().RegisterRouter(router.Router)
 	n.Use(reqlog.NewMiddlewareFromLogger(l, "admin#"+c.SelfPublicURL(nil).String()))
 	n.Use(sqa(cmd, r))
 	n.Use(r.PrometheusManager())
diff --git a/docs/docs/concepts/email-sms.md b/docs/docs/concepts/email-sms.md
index 637a9bb35..80b79ae79 100644
--- a/docs/docs/concepts/email-sms.md
+++ b/docs/docs/concepts/email-sms.md
@@ -81,21 +81,21 @@ courier:
   template_override_path: /conf/courier-templates
 ```
 
-`email.subject.gotmpl` and `email.body.gotmpl` are common template file names
-expected in remainder directories corresponding to respective methods for
-filling E-mail subject and body.
+`email.subject.gotmpl`, `email.body.gotmpl` and `email.body.plaintext.gotmpl`
+are common template file names expected in remainder directories corresponding
+to respective methods for filling E-mail subject and body.
 
 > Templates use the engine golang text template for text/html email rendering:
 > https://golang.org/pkg/text/template
 
 - recovery: recovery email templates root directory
   - valid: sub directory containing templates with variables `To` and
-    `VerificationURL` for validating a recovery
+    `RecoveryURL` for validating a recovery
   - invalid: sub directory containing templates with variables `To` for
     invalidating a recovery
 - verification: verification email templates root directory
   - valid: sub directory containing templates with variables `To` and
-    `RecoveryURL` for validating a verification
+    `VerificationURL` for validating a verification
   - invalid: sub directory containing templates with variables `To` for
     invalidating a verification
 
@@ -108,6 +108,10 @@ Hi, please verify your account by clicking the following link:
 <a href="{{ .VerificationURL }}">{{ .VerificationURL }}</a>
 ```
 
+```gotmp title="courier/template/templates/verification/valid/email.body.plaintext.gotmpl"
+Hi, please verify your account by clicking the following link: {{ .VerificationURL }}
+```
+
 ## Sending SMS
 
 The Sending SMS feature is not supported at present. It will be available in a
diff --git a/docs/docs/concepts/identity-data-model.mdx b/docs/docs/concepts/identity-data-model.mdx
index 5bb91263c..a5b1f3c57 100644
--- a/docs/docs/concepts/identity-data-model.mdx
+++ b/docs/docs/concepts/identity-data-model.mdx
@@ -28,6 +28,9 @@ payload is usually in JSON format. An `identity` has the following properties:
 # which cannot be changed or updated at a later stage.
 id: '9f425a8d-7efc-4768-8f23-7647a74fdf13'
 
+# Every identity has a state. Inactive identities will not be able to log into the system.
+state: active
+
 # This section represents all the credentials associated with this identity.
 # It is further explained in the "Credentials" section.
 credentials:
@@ -73,22 +76,24 @@ Identities are
 
 - `created` - via API or self-service registration;
 - `updated` - via API or self-service settings, account recovery, etc.;
-- `disabled` - not yet implemented, see
-  [#598](https://github.com/ory/kratos/issues/598);
+- `inactive` - via API only;
 - `deleted` - via API or with a self-service flow (not yet implemented see
   [#596](https://github.com/ory/kratos/issues/596)).
 
-The identity state is therefore `active` or `disabled` (not yet implemented see
-[#598](https://github.com/ory/kratos/issues/598))
+The identity state is therefore `active` or `inactive`.
+
+An inactive identity will not be able to log into the system. It will receive a
+401 with a reason indicating that the account has been disabled. More states
+will be added later on for specific features.
 
 <Mermaid
   chart={`
 stateDiagram-v2
 [*] --> Active: create
 Active --> Active: update
-Active --> Disabled: disable
-Disabled --> [*]: delete
-Disabled --> Active: enable
+Active --> Inactive: disable
+Inactive --> [*]: delete
+Inactive --> Active: enable
 `}
 />
 
diff --git a/driver/registry.go b/driver/registry.go
index dadf48241..248eaad23 100644
--- a/driver/registry.go
+++ b/driver/registry.go
@@ -3,7 +3,7 @@ package driver
 import (
 	"context"
 
-	"github.com/ory/kratos/metrics/prometheus"
+	prometheus "github.com/ory/x/prometheusx"
 	"github.com/ory/x/tracing"
 
 	"github.com/gorilla/sessions"
diff --git a/driver/registry_default.go b/driver/registry_default.go
index 40acaea26..ff22eb0e7 100644
--- a/driver/registry_default.go
+++ b/driver/registry_default.go
@@ -12,7 +12,7 @@ import (
 
 	"github.com/ory/kratos/corp"
 
-	"github.com/ory/kratos/metrics/prometheus"
+	prometheus "github.com/ory/x/prometheusx"
 
 	"github.com/gobuffalo/pop/v5"
 
@@ -617,7 +617,7 @@ func (m *RegistryDefault) PrometheusManager() *prometheus.MetricsManager {
 	m.rwl.Lock()
 	defer m.rwl.Unlock()
 	if m.pmm == nil {
-		m.pmm = prometheus.NewMetricsManager(m.buildVersion, m.buildHash, m.buildDate)
+		m.pmm = prometheus.NewMetricsManager("kratos", m.buildVersion, m.buildHash, m.buildDate)
 	}
 	return m.pmm
 }
diff --git a/identity/handler.go b/identity/handler.go
index 37549ee32..c49342102 100644
--- a/identity/handler.go
+++ b/identity/handler.go
@@ -3,16 +3,19 @@ package identity
 import (
 	"encoding/json"
 	"net/http"
+	"time"
 
-	"github.com/ory/kratos/driver/config"
+	"github.com/ory/herodot"
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/pkg/errors"
 
+	"github.com/ory/kratos/driver/config"
+	"github.com/ory/kratos/x"
+
 	"github.com/ory/x/jsonx"
+	"github.com/ory/x/sqlxx"
 	"github.com/ory/x/urlx"
-
-	"github.com/ory/kratos/x"
 )
 
 const RouteBase = "/identities"
@@ -214,7 +217,7 @@ func (h *Handler) create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa
 		return
 	}
 
-	i := &Identity{SchemaID: cr.SchemaID, Traits: []byte(cr.Traits)}
+	i := &Identity{SchemaID: cr.SchemaID, Traits: []byte(cr.Traits), State: StateActive, StateChangedAt: sqlxx.NullTime(time.Now())}
 	if err := h.r.IdentityManager().Create(r.Context(), i); err != nil {
 		h.r.Writer().WriteError(w, r, err)
 		return
@@ -255,6 +258,11 @@ type UpdateIdentity struct {
 	//
 	// required: true
 	Traits json.RawMessage `json:"traits"`
+
+	// State is the identity's state.
+	//
+	// required: true
+	State State `json:"state"`
 }
 
 // swagger:route PUT /identities/{id} admin updateIdentity
@@ -300,6 +308,15 @@ func (h *Handler) update(w http.ResponseWriter, r *http.Request, ps httprouter.P
 		identity.SchemaID = ur.SchemaID
 	}
 
+	if ur.State != "" && identity.State != ur.State {
+		if err := ur.State.IsValid(); err != nil {
+			h.r.Writer().WriteError(w, r, herodot.ErrBadRequest.WithReasonf("%s", err).WithWrap(err))
+		}
+
+		identity.State = ur.State
+		identity.StateChangedAt = sqlxx.NullTime(time.Now())
+	}
+
 	identity.Traits = []byte(ur.Traits)
 	if err := h.r.IdentityManager().Update(
 		r.Context(),
diff --git a/identity/identity.go b/identity/identity.go
index 6674aa933..df65b952c 100644
--- a/identity/identity.go
+++ b/identity/identity.go
@@ -20,6 +20,22 @@ import (
 	"github.com/ory/kratos/x"
 )
 
+// swagger:enum State
+type State string
+
+const (
+	StateActive   State = "active"
+	StateInactive State = "inactive"
+)
+
+func (lt State) IsValid() error {
+	switch lt {
+	case StateActive, StateInactive:
+		return nil
+	}
+	return errors.New("identity state is not valid")
+}
+
 // Identity represents an Ory Kratos identity
 //
 // An identity can be a real human, a service, an IoT device - everything that
@@ -51,6 +67,17 @@ type Identity struct {
 	// required: true
 	SchemaURL string `json:"schema_url" faker:"-" db:"-"`
 
+	// State is the identity's state.
+	//
+	// enum:
+	// - active
+	// - inactive
+	// required: true
+	State State `json:"state" faker:"-" db:"state"`
+
+	// StateChangedAt contains the last time when the identity's state changed.
+	StateChangedAt sqlxx.NullTime `json:"state_changed_at" faker:"-" db:"state_changed_at"`
+
 	// Traits represent an identity's traits. The identity is able to create, modify, and delete traits
 	// in a self-service manner. The input will always be validated against the JSON Schema defined
 	// in `schema_url`.
@@ -129,6 +156,10 @@ func (i *Identity) lock() *sync.RWMutex {
 	return i.l
 }
 
+func (i *Identity) IsActive() bool {
+	return i.State == StateActive
+}
+
 func (i *Identity) SetCredentials(t CredentialsType, c Credentials) {
 	i.lock().Lock()
 	defer i.lock().Unlock()
@@ -182,6 +213,8 @@ func NewIdentity(traitsSchemaID string) *Identity {
 		Traits:              Traits("{}"),
 		SchemaID:            traitsSchemaID,
 		VerifiableAddresses: []VerifiableAddress{},
+		State:               StateActive,
+		StateChangedAt:      sqlxx.NullTime(time.Now()),
 		l:                   new(sync.RWMutex),
 	}
 }
diff --git a/internal/httpclient/api/openapi.yaml b/internal/httpclient/api/openapi.yaml
index 4ee9a37bf..22db46a7c 100644
--- a/internal/httpclient/api/openapi.yaml
+++ b/internal/httpclient/api/openapi.yaml
@@ -2492,9 +2492,11 @@ components:
           status: status
           via: via
         schema_id: schema_id
+        state_changed_at: 2000-01-23T04:56:07.000+00:00
         created_at: 2000-01-23T04:56:07.000+00:00
         schema_url: schema_url
         id: id
+        state: active
       properties:
         created_at:
           description: CreatedAt is a helper struct field for gobuffalo.pop.
@@ -2526,6 +2528,16 @@ components:
 
             format: url
           type: string
+        state:
+          description: State is the identity's state.
+          enum:
+          - active
+          - inactive
+          type: string
+        state_changed_at:
+          format: date-time
+          title: NullTime implements sql.NullTime functionality.
+          type: string
         traits:
           description: |-
             Traits represent an identity's traits. The identity is able to create, modify, and delete traits
@@ -2546,6 +2558,7 @@ components:
       - id
       - schema_id
       - schema_url
+      - state
       - traits
       title: Identity represents an Ory Kratos identity
       type: object
@@ -2769,9 +2782,11 @@ components:
               status: status
               via: via
             schema_id: schema_id
+            state_changed_at: 2000-01-23T04:56:07.000+00:00
             created_at: 2000-01-23T04:56:07.000+00:00
             schema_url: schema_url
             id: id
+            state: active
           authenticated_at: 2000-01-23T04:56:07.000+00:00
           active: true
           id: id
@@ -3053,9 +3068,11 @@ components:
             status: status
             via: via
           schema_id: schema_id
+          state_changed_at: 2000-01-23T04:56:07.000+00:00
           created_at: 2000-01-23T04:56:07.000+00:00
           schema_url: schema_url
           id: id
+          state: active
         session:
           expires_at: 2000-01-23T04:56:07.000+00:00
           identity:
@@ -3099,9 +3116,11 @@ components:
               status: status
               via: via
             schema_id: schema_id
+            state_changed_at: 2000-01-23T04:56:07.000+00:00
             created_at: 2000-01-23T04:56:07.000+00:00
             schema_url: schema_url
             id: id
+            state: active
           authenticated_at: 2000-01-23T04:56:07.000+00:00
           active: true
           id: id
@@ -3212,9 +3231,11 @@ components:
             status: status
             via: via
           schema_id: schema_id
+          state_changed_at: 2000-01-23T04:56:07.000+00:00
           created_at: 2000-01-23T04:56:07.000+00:00
           schema_url: schema_url
           id: id
+          state: active
         authenticated_at: 2000-01-23T04:56:07.000+00:00
         active: true
         id: id
@@ -3339,9 +3360,11 @@ components:
             status: status
             via: via
           schema_id: schema_id
+          state_changed_at: 2000-01-23T04:56:07.000+00:00
           created_at: 2000-01-23T04:56:07.000+00:00
           schema_url: schema_url
           id: id
+          state: active
         active: active
         id: id
         state: state
@@ -3458,9 +3481,11 @@ components:
             status: status
             via: via
           schema_id: schema_id
+          state_changed_at: 2000-01-23T04:56:07.000+00:00
           created_at: 2000-01-23T04:56:07.000+00:00
           schema_url: schema_url
           id: id
+          state: active
         flow:
           expires_at: 2000-01-23T04:56:07.000+00:00
           ui:
@@ -3551,9 +3576,11 @@ components:
               status: status
               via: via
             schema_id: schema_id
+            state_changed_at: 2000-01-23T04:56:07.000+00:00
             created_at: 2000-01-23T04:56:07.000+00:00
             schema_url: schema_url
             id: id
+            state: active
           active: active
           id: id
           state: state
@@ -3970,6 +3997,12 @@ components:
             SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. If set
             will update the Identity's SchemaID.
           type: string
+        state:
+          description: State is the identity's state.
+          enum:
+          - active
+          - inactive
+          type: string
         traits:
           description: |-
             Traits represent an identity's traits. The identity is able to create, modify, and delete traits
@@ -3977,6 +4010,7 @@ components:
             in `schema_id`.
           type: object
       required:
+      - state
       - traits
       type: object
     verifiableIdentityAddress:
diff --git a/internal/httpclient/docs/AdminApi.md b/internal/httpclient/docs/AdminApi.md
index 8686c5d3d..7d9c64a19 100644
--- a/internal/httpclient/docs/AdminApi.md
+++ b/internal/httpclient/docs/AdminApi.md
@@ -1094,7 +1094,7 @@ import (
 
 func main() {
     id := "id_example" // string | ID must be set to the ID of identity you want to update
-    updateIdentity := *openapiclient.NewUpdateIdentity(map[string]interface{}(123)) // UpdateIdentity |  (optional)
+    updateIdentity := *openapiclient.NewUpdateIdentity("State_example", map[string]interface{}(123)) // UpdateIdentity |  (optional)
 
     configuration := openapiclient.NewConfiguration()
     apiClient := openapiclient.NewAPIClient(configuration)
diff --git a/internal/httpclient/docs/Identity.md b/internal/httpclient/docs/Identity.md
index 4b2c5bbcb..b8303ca1c 100644
--- a/internal/httpclient/docs/Identity.md
+++ b/internal/httpclient/docs/Identity.md
@@ -10,6 +10,8 @@ Name | Type | Description | Notes
 **RecoveryAddresses** | Pointer to [**[]RecoveryAddress**](RecoveryAddress.md) | RecoveryAddresses contains all the addresses that can be used to recover an identity. | [optional] 
 **SchemaId** | **string** | SchemaID is the ID of the JSON Schema to be used for validating the identity&#39;s traits. | 
 **SchemaUrl** | **string** | SchemaURL is the URL of the endpoint where the identity&#39;s traits schema can be fetched from.  format: url | 
+**State** | **string** | State is the identity&#39;s state. | 
+**StateChangedAt** | Pointer to **time.Time** |  | [optional] 
 **Traits** | **interface{}** | Traits represent an identity&#39;s traits. The identity is able to create, modify, and delete traits in a self-service manner. The input will always be validated against the JSON Schema defined in &#x60;schema_url&#x60;. | 
 **UpdatedAt** | Pointer to **time.Time** | UpdatedAt is a helper struct field for gobuffalo.pop. | [optional] 
 **VerifiableAddresses** | Pointer to [**[]VerifiableIdentityAddress**](VerifiableIdentityAddress.md) | VerifiableAddresses contains all the addresses that can be verified by the user. | [optional] 
@@ -18,7 +20,7 @@ Name | Type | Description | Notes
 
 ### NewIdentity
 
-`func NewIdentity(id string, schemaId string, schemaUrl string, traits interface{}, ) *Identity`
+`func NewIdentity(id string, schemaId string, schemaUrl string, state string, traits interface{}, ) *Identity`
 
 NewIdentity instantiates a new Identity object
 This constructor will assign default values to properties that have it defined,
@@ -168,6 +170,51 @@ and a boolean to check if the value has been set.
 SetSchemaUrl sets SchemaUrl field to given value.
 
 
+### GetState
+
+`func (o *Identity) GetState() string`
+
+GetState returns the State field if non-nil, zero value otherwise.
+
+### GetStateOk
+
+`func (o *Identity) GetStateOk() (*string, bool)`
+
+GetStateOk returns a tuple with the State field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetState
+
+`func (o *Identity) SetState(v string)`
+
+SetState sets State field to given value.
+
+
+### GetStateChangedAt
+
+`func (o *Identity) GetStateChangedAt() time.Time`
+
+GetStateChangedAt returns the StateChangedAt field if non-nil, zero value otherwise.
+
+### GetStateChangedAtOk
+
+`func (o *Identity) GetStateChangedAtOk() (*time.Time, bool)`
+
+GetStateChangedAtOk returns a tuple with the StateChangedAt field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetStateChangedAt
+
+`func (o *Identity) SetStateChangedAt(v time.Time)`
+
+SetStateChangedAt sets StateChangedAt field to given value.
+
+### HasStateChangedAt
+
+`func (o *Identity) HasStateChangedAt() bool`
+
+HasStateChangedAt returns a boolean if a field has been set.
+
 ### GetTraits
 
 `func (o *Identity) GetTraits() interface{}`
diff --git a/internal/httpclient/docs/UpdateIdentity.md b/internal/httpclient/docs/UpdateIdentity.md
index 8a78927c9..9dbf8d55c 100644
--- a/internal/httpclient/docs/UpdateIdentity.md
+++ b/internal/httpclient/docs/UpdateIdentity.md
@@ -5,13 +5,14 @@
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 **SchemaId** | Pointer to **string** | SchemaID is the ID of the JSON Schema to be used for validating the identity&#39;s traits. If set will update the Identity&#39;s SchemaID. | [optional] 
+**State** | **string** | State is the identity&#39;s state. | 
 **Traits** | **map[string]interface{}** | Traits represent an identity&#39;s traits. The identity is able to create, modify, and delete traits in a self-service manner. The input will always be validated against the JSON Schema defined in &#x60;schema_id&#x60;. | 
 
 ## Methods
 
 ### NewUpdateIdentity
 
-`func NewUpdateIdentity(traits map[string]interface{}, ) *UpdateIdentity`
+`func NewUpdateIdentity(state string, traits map[string]interface{}, ) *UpdateIdentity`
 
 NewUpdateIdentity instantiates a new UpdateIdentity object
 This constructor will assign default values to properties that have it defined,
@@ -51,6 +52,26 @@ SetSchemaId sets SchemaId field to given value.
 
 HasSchemaId returns a boolean if a field has been set.
 
+### GetState
+
+`func (o *UpdateIdentity) GetState() string`
+
+GetState returns the State field if non-nil, zero value otherwise.
+
+### GetStateOk
+
+`func (o *UpdateIdentity) GetStateOk() (*string, bool)`
+
+GetStateOk returns a tuple with the State field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetState
+
+`func (o *UpdateIdentity) SetState(v string)`
+
+SetState sets State field to given value.
+
+
 ### GetTraits
 
 `func (o *UpdateIdentity) GetTraits() map[string]interface{}`
diff --git a/internal/httpclient/model_identity.go b/internal/httpclient/model_identity.go
index af9a03857..6a49243b3 100644
--- a/internal/httpclient/model_identity.go
+++ b/internal/httpclient/model_identity.go
@@ -29,6 +29,9 @@ type Identity struct {
 	SchemaId string `json:"schema_id"`
 	// SchemaURL is the URL of the endpoint where the identity's traits schema can be fetched from.  format: url
 	SchemaUrl string `json:"schema_url"`
+	// State is the identity's state.
+	State          string     `json:"state"`
+	StateChangedAt *time.Time `json:"state_changed_at,omitempty"`
 	// Traits represent an identity's traits. The identity is able to create, modify, and delete traits in a self-service manner. The input will always be validated against the JSON Schema defined in `schema_url`.
 	Traits interface{} `json:"traits"`
 	// UpdatedAt is a helper struct field for gobuffalo.pop.
@@ -41,11 +44,12 @@ type Identity struct {
 // This constructor will assign default values to properties that have it defined,
 // and makes sure properties required by API are set, but the set of arguments
 // will change when the set of required properties is changed
-func NewIdentity(id string, schemaId string, schemaUrl string, traits interface{}) *Identity {
+func NewIdentity(id string, schemaId string, schemaUrl string, state string, traits interface{}) *Identity {
 	this := Identity{}
 	this.Id = id
 	this.SchemaId = schemaId
 	this.SchemaUrl = schemaUrl
+	this.State = state
 	this.Traits = traits
 	return &this
 }
@@ -226,6 +230,62 @@ func (o *Identity) SetSchemaUrl(v string) {
 	o.SchemaUrl = v
 }
 
+// GetState returns the State field value
+func (o *Identity) GetState() string {
+	if o == nil {
+		var ret string
+		return ret
+	}
+
+	return o.State
+}
+
+// GetStateOk returns a tuple with the State field value
+// and a boolean to check if the value has been set.
+func (o *Identity) GetStateOk() (*string, bool) {
+	if o == nil {
+		return nil, false
+	}
+	return &o.State, true
+}
+
+// SetState sets field value
+func (o *Identity) SetState(v string) {
+	o.State = v
+}
+
+// GetStateChangedAt returns the StateChangedAt field value if set, zero value otherwise.
+func (o *Identity) GetStateChangedAt() time.Time {
+	if o == nil || o.StateChangedAt == nil {
+		var ret time.Time
+		return ret
+	}
+	return *o.StateChangedAt
+}
+
+// GetStateChangedAtOk returns a tuple with the StateChangedAt field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *Identity) GetStateChangedAtOk() (*time.Time, bool) {
+	if o == nil || o.StateChangedAt == nil {
+		return nil, false
+	}
+	return o.StateChangedAt, true
+}
+
+// HasStateChangedAt returns a boolean if a field has been set.
+func (o *Identity) HasStateChangedAt() bool {
+	if o != nil && o.StateChangedAt != nil {
+		return true
+	}
+
+	return false
+}
+
+// SetStateChangedAt gets a reference to the given time.Time and assigns it to the StateChangedAt field.
+func (o *Identity) SetStateChangedAt(v time.Time) {
+	o.StateChangedAt = &v
+}
+
 // GetTraits returns the Traits field value
 // If the value is explicit nil, the zero value for interface{} will be returned
 func (o *Identity) GetTraits() interface{} {
@@ -336,6 +396,12 @@ func (o Identity) MarshalJSON() ([]byte, error) {
 	if true {
 		toSerialize["schema_url"] = o.SchemaUrl
 	}
+	if true {
+		toSerialize["state"] = o.State
+	}
+	if o.StateChangedAt != nil {
+		toSerialize["state_changed_at"] = o.StateChangedAt
+	}
 	if o.Traits != nil {
 		toSerialize["traits"] = o.Traits
 	}
diff --git a/internal/httpclient/model_update_identity.go b/internal/httpclient/model_update_identity.go
index 37574b654..2bab91954 100644
--- a/internal/httpclient/model_update_identity.go
+++ b/internal/httpclient/model_update_identity.go
@@ -19,6 +19,8 @@ import (
 type UpdateIdentity struct {
 	// SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. If set will update the Identity's SchemaID.
 	SchemaId *string `json:"schema_id,omitempty"`
+	// State is the identity's state.
+	State string `json:"state"`
 	// Traits represent an identity's traits. The identity is able to create, modify, and delete traits in a self-service manner. The input will always be validated against the JSON Schema defined in `schema_id`.
 	Traits map[string]interface{} `json:"traits"`
 }
@@ -27,8 +29,9 @@ type UpdateIdentity struct {
 // This constructor will assign default values to properties that have it defined,
 // and makes sure properties required by API are set, but the set of arguments
 // will change when the set of required properties is changed
-func NewUpdateIdentity(traits map[string]interface{}) *UpdateIdentity {
+func NewUpdateIdentity(state string, traits map[string]interface{}) *UpdateIdentity {
 	this := UpdateIdentity{}
+	this.State = state
 	this.Traits = traits
 	return &this
 }
@@ -73,6 +76,30 @@ func (o *UpdateIdentity) SetSchemaId(v string) {
 	o.SchemaId = &v
 }
 
+// GetState returns the State field value
+func (o *UpdateIdentity) GetState() string {
+	if o == nil {
+		var ret string
+		return ret
+	}
+
+	return o.State
+}
+
+// GetStateOk returns a tuple with the State field value
+// and a boolean to check if the value has been set.
+func (o *UpdateIdentity) GetStateOk() (*string, bool) {
+	if o == nil {
+		return nil, false
+	}
+	return &o.State, true
+}
+
+// SetState sets field value
+func (o *UpdateIdentity) SetState(v string) {
+	o.State = v
+}
+
 // GetTraits returns the Traits field value
 func (o *UpdateIdentity) GetTraits() map[string]interface{} {
 	if o == nil {
@@ -102,6 +129,9 @@ func (o UpdateIdentity) MarshalJSON() ([]byte, error) {
 	if o.SchemaId != nil {
 		toSerialize["schema_id"] = o.SchemaId
 	}
+	if true {
+		toSerialize["state"] = o.State
+	}
 	if true {
 		toSerialize["traits"] = o.Traits
 	}
diff --git a/metrics/prometheus/handler.go b/metrics/prometheus/handler.go
deleted file mode 100644
index 88c6f586a..000000000
--- a/metrics/prometheus/handler.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package prometheus
-
-import (
-	"net/http"
-
-	"github.com/julienschmidt/httprouter"
-	"github.com/prometheus/client_golang/prometheus/promhttp"
-
-	"github.com/ory/herodot"
-)
-
-const (
-	MetricsPrometheusPath = "/metrics/prometheus"
-)
-
-// Handler handles HTTP requests to health and version endpoints.
-type Handler struct {
-	H             herodot.Writer
-	VersionString string
-}
-
-// NewHandler instantiates a handler.
-func NewHandler(
-	h herodot.Writer,
-	version string,
-) *Handler {
-	return &Handler{
-		H:             h,
-		VersionString: version,
-	}
-}
-
-// SetRoutes registers this handler's routes.
-func (h *Handler) SetRoutes(r *httprouter.Router) {
-	r.GET(MetricsPrometheusPath, h.Metrics)
-}
-
-// Metrics outputs prometheus metrics
-//
-// swagger:route GET /metrics/prometheus admin prometheus
-//
-// Get snapshot metrics from the Hydra service. If you're using k8s, you can then add annotations to
-// your deployment like so:
-//
-// ```
-// metadata:
-//  annotations:
-//    prometheus.io/port: "4434"
-//    prometheus.io/path: "/metrics/prometheus"
-// ```
-//
-//     Produces:
-//     - plain/text
-//
-//     Responses:
-//       200: emptyResponse
-func (h *Handler) Metrics(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) {
-	promhttp.Handler().ServeHTTP(rw, r)
-}
diff --git a/metrics/prometheus/metrics.go b/metrics/prometheus/metrics.go
deleted file mode 100644
index 5666f078d..000000000
--- a/metrics/prometheus/metrics.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package prometheus
-
-import "github.com/prometheus/client_golang/prometheus"
-
-// Metrics prototypes
-type Metrics struct {
-	ResponseTime *prometheus.HistogramVec
-}
-
-// Method for creation new custom Prometheus  metrics
-func NewMetrics(version, hash, date string) *Metrics {
-	pm := &Metrics{
-		ResponseTime: prometheus.NewHistogramVec(
-			prometheus.HistogramOpts{
-				Name: "kratos_response_time_seconds",
-				Help: "Description",
-				ConstLabels: map[string]string{
-					"version":   version,
-					"hash":      hash,
-					"buildTime": date,
-				},
-			},
-			[]string{"endpoint"},
-		),
-	}
-	err := prometheus.Register(pm.ResponseTime)
-
-	if err != nil {
-		panic(err)
-	}
-	return pm
-}
diff --git a/metrics/prometheus/middleware.go b/metrics/prometheus/middleware.go
deleted file mode 100644
index 5c7f5cbae..000000000
--- a/metrics/prometheus/middleware.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package prometheus
-
-import (
-	"net/http"
-	"time"
-)
-
-type MetricsManager struct {
-	prometheusMetrics *Metrics
-}
-
-func NewMetricsManager(version, hash, buildTime string) *MetricsManager {
-	return &MetricsManager{
-		prometheusMetrics: NewMetrics(version, hash, buildTime),
-	}
-}
-
-// Main middleware method to collect metrics for Prometheus.
-func (pmm *MetricsManager) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
-	start := time.Now()
-	next(rw, r)
-
-	pmm.prometheusMetrics.ResponseTime.WithLabelValues(r.URL.Path).Observe(time.Since(start).Seconds())
-}
diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.down.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.down.sql
new file mode 100644
index 000000000..f75038c00
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "identities" DROP COLUMN "state_changed_at";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.up.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.up.sql
new file mode 100644
index 000000000..6915848da
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.cockroach.up.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "identities" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'active';
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.down.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.down.sql
new file mode 100644
index 000000000..d3e77b358
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE `identities` DROP COLUMN `state_changed_at`;
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.up.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.up.sql
new file mode 100644
index 000000000..931022d01
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.mysql.up.sql
@@ -0,0 +1,1 @@
+ALTER TABLE `identities` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'active';
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.down.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.down.sql
new file mode 100644
index 000000000..f75038c00
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "identities" DROP COLUMN "state_changed_at";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.up.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.up.sql
new file mode 100644
index 000000000..6915848da
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.postgres.up.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "identities" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'active';
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..09c5e5055
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "_identities_tmp" RENAME TO "identities";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..4eb6d512b
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000000_add_identity_states.sqlite3.up.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "identities" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'active';
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.down.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.down.sql
new file mode 100644
index 000000000..e49529c70
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "identities" DROP COLUMN "state";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.up.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.up.sql
new file mode 100644
index 000000000..31d1e71f3
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.cockroach.up.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "identities" ADD COLUMN "state_changed_at" timestamp;
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.down.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.down.sql
new file mode 100644
index 000000000..8e13fbe36
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE `identities` DROP COLUMN `state`;
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.up.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.up.sql
new file mode 100644
index 000000000..ba1d26c76
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.mysql.up.sql
@@ -0,0 +1,1 @@
+ALTER TABLE `identities` ADD COLUMN `state_changed_at` DATETIME;
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.down.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.down.sql
new file mode 100644
index 000000000..e49529c70
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "identities" DROP COLUMN "state";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.up.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.up.sql
new file mode 100644
index 000000000..31d1e71f3
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.postgres.up.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "identities" ADD COLUMN "state_changed_at" timestamp;
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..10597b3c9
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,2 @@
+
+DROP TABLE "identities";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..7b0b44093
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000001_add_identity_states.sqlite3.up.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "identities" ADD COLUMN "state_changed_at" DATETIME;
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000002_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000002_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..bd5056d04
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000002_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,1 @@
+INSERT INTO "_identities_tmp" (id, schema_id, traits, created_at, updated_at, nid) SELECT id, schema_id, traits, created_at, updated_at, nid FROM "identities";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000002_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000002_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/persistence/sql/migrations/sql/20210504121624000003_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000003_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..044042e4a
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000003_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,1 @@
+CREATE INDEX "identities_nid_idx" ON "_identities_tmp" (id, nid);
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000003_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000003_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/persistence/sql/migrations/sql/20210504121624000004_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000004_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..82c623667
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000004_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,8 @@
+CREATE TABLE "_identities_tmp" (
+"id" TEXT PRIMARY KEY,
+"schema_id" TEXT NOT NULL,
+"traits" TEXT NOT NULL,
+"created_at" DATETIME NOT NULL,
+"updated_at" DATETIME NOT NULL,
+"nid" char(36)
+);
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000004_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000004_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/persistence/sql/migrations/sql/20210504121624000005_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000005_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..295533e43
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000005_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,1 @@
+DROP INDEX IF EXISTS "identities_nid_idx";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000005_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000005_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/persistence/sql/migrations/sql/20210504121624000006_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000006_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..09c5e5055
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000006_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "_identities_tmp" RENAME TO "identities";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000006_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000006_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/persistence/sql/migrations/sql/20210504121624000007_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000007_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..10597b3c9
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000007_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,2 @@
+
+DROP TABLE "identities";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000007_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000007_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/persistence/sql/migrations/sql/20210504121624000008_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000008_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..171014525
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000008_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,1 @@
+INSERT INTO "_identities_tmp" (id, schema_id, traits, created_at, updated_at, nid, state_changed_at) SELECT id, schema_id, traits, created_at, updated_at, nid, state_changed_at FROM "identities";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000008_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000008_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/persistence/sql/migrations/sql/20210504121624000009_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000009_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..044042e4a
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000009_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,1 @@
+CREATE INDEX "identities_nid_idx" ON "_identities_tmp" (id, nid);
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000009_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000009_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/persistence/sql/migrations/sql/20210504121624000010_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000010_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..0a7fdbee6
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000010_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,9 @@
+CREATE TABLE "_identities_tmp" (
+"id" TEXT PRIMARY KEY,
+"schema_id" TEXT NOT NULL,
+"traits" TEXT NOT NULL,
+"created_at" DATETIME NOT NULL,
+"updated_at" DATETIME NOT NULL,
+"nid" char(36),
+"state_changed_at" DATETIME
+);
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000010_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000010_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/persistence/sql/migrations/sql/20210504121624000011_add_identity_states.sqlite3.down.sql b/persistence/sql/migrations/sql/20210504121624000011_add_identity_states.sqlite3.down.sql
new file mode 100644
index 000000000..295533e43
--- /dev/null
+++ b/persistence/sql/migrations/sql/20210504121624000011_add_identity_states.sqlite3.down.sql
@@ -0,0 +1,1 @@
+DROP INDEX IF EXISTS "identities_nid_idx";
\ No newline at end of file
diff --git a/persistence/sql/migrations/sql/20210504121624000011_add_identity_states.sqlite3.up.sql b/persistence/sql/migrations/sql/20210504121624000011_add_identity_states.sqlite3.up.sql
new file mode 100644
index 000000000..e69de29bb
diff --git a/persistence/sql/migrations/templates/20210504121624_add_identity_states.down.fizz b/persistence/sql/migrations/templates/20210504121624_add_identity_states.down.fizz
new file mode 100644
index 000000000..5c9633f9e
--- /dev/null
+++ b/persistence/sql/migrations/templates/20210504121624_add_identity_states.down.fizz
@@ -0,0 +1,2 @@
+drop_column("identities", "state")
+drop_column("identities", "state_changed_at")
diff --git a/persistence/sql/migrations/templates/20210504121624_add_identity_states.up.fizz b/persistence/sql/migrations/templates/20210504121624_add_identity_states.up.fizz
new file mode 100644
index 000000000..f7a234a0b
--- /dev/null
+++ b/persistence/sql/migrations/templates/20210504121624_add_identity_states.up.fizz
@@ -0,0 +1,2 @@
+add_column("identities", "state", "string", {"default": "active"})
+add_column("identities", "state_changed_at", "timestamp", {"null": true})
diff --git a/persistence/sql/persister_identity.go b/persistence/sql/persister_identity.go
index 0bec2c1d4..29ea5b7b5 100644
--- a/persistence/sql/persister_identity.go
+++ b/persistence/sql/persister_identity.go
@@ -213,6 +213,11 @@ func (p *Persister) CreateIdentity(ctx context.Context, i *identity.Identity) er
 		i.SchemaID = config.DefaultIdentityTraitsSchemaID
 	}
 
+	i.StateChangedAt = sqlxx.NullTime(time.Now())
+	if i.State == "" {
+		i.State = identity.StateActive
+	}
+
 	if len(i.Traits) == 0 {
 		i.Traits = identity.Traits("{}")
 	}
diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go
index 41b095275..a195eebd2 100644
--- a/selfservice/flow/login/hook.go
+++ b/selfservice/flow/login/hook.go
@@ -61,7 +61,11 @@ func NewHookExecutor(d executorDependencies) *HookExecutor {
 }
 
 func (e *HookExecutor) PostLoginHook(w http.ResponseWriter, r *http.Request, ct identity.CredentialsType, a *Flow, i *identity.Identity) error {
-	s := session.NewActiveSession(i, e.d.Config(r.Context()), time.Now().UTC()).Declassify()
+	s, err := session.NewActiveSession(i, e.d.Config(r.Context()), time.Now().UTC())
+	if err != nil {
+		return err
+	}
+	s = s.Declassify()
 
 	e.d.Logger().
 		WithRequest(r).
diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go
index 7e8e2cb79..626bf7d44 100644
--- a/selfservice/flow/registration/hook.go
+++ b/selfservice/flow/registration/hook.go
@@ -129,7 +129,11 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque
 		WithField("identity_id", i.ID).
 		Info("A new identity has registered using self-service registration.")
 
-	s := session.NewActiveSession(i, e.d.Config(r.Context()), time.Now().UTC())
+	s, err := session.NewActiveSession(i, e.d.Config(r.Context()), time.Now().UTC())
+	if err != nil {
+		return err
+	}
+
 	e.d.Logger().
 		WithRequest(r).
 		WithField("identity_id", i.ID).
diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go
index 24125627e..b7693c3f4 100644
--- a/selfservice/strategy/link/strategy_recovery.go
+++ b/selfservice/strategy/link/strategy_recovery.go
@@ -323,7 +323,7 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request,
 		return s.handleRecoveryError(w, r, f, nil, err)
 	}
 
-	sess := session.NewActiveSession(recovered, s.d.Config(r.Context()), time.Now().UTC())
+	sess, _ := session.NewActiveSession(recovered, s.d.Config(r.Context()), time.Now().UTC())
 	if err := s.d.SessionManager().CreateAndIssueCookie(r.Context(), w, r, sess); err != nil {
 		return s.handleRecoveryError(w, r, f, nil, err)
 	}
diff --git a/session/session.go b/session/session.go
index 7b69b0b63..cab3bb76a 100644
--- a/session/session.go
+++ b/session/session.go
@@ -4,16 +4,17 @@ import (
 	"context"
 	"time"
 
-	"github.com/ory/kratos/corp"
-
 	"github.com/gofrs/uuid"
 
-	"github.com/ory/x/randx"
-
+	"github.com/ory/herodot"
+	"github.com/ory/kratos/corp"
 	"github.com/ory/kratos/identity"
 	"github.com/ory/kratos/x"
+	"github.com/ory/x/randx"
 )
 
+var ErrIdentityDisabled = herodot.ErrUnauthorized.WithError("identity is disabled").WithReason("This account was disabled.")
+
 // swagger:model session
 type Session struct {
 	// required: true
@@ -49,7 +50,11 @@ func (s Session) TableName(ctx context.Context) string {
 
 func NewActiveSession(i *identity.Identity, c interface {
 	SessionLifespan() time.Duration
-}, authenticatedAt time.Time) *Session {
+}, authenticatedAt time.Time) (*Session, error) {
+	if i != nil && !i.IsActive() {
+		return nil, ErrIdentityDisabled
+	}
+
 	return &Session{
 		ID:              x.NewUUID(),
 		ExpiresAt:       authenticatedAt.Add(c.SessionLifespan()),
@@ -59,7 +64,7 @@ func NewActiveSession(i *identity.Identity, c interface {
 		IdentityID:      i.ID,
 		Token:           randx.MustString(32, randx.AlphaNum),
 		Active:          true,
-	}
+	}, nil
 }
 
 type Device struct {
@@ -73,5 +78,5 @@ func (s *Session) Declassify() *Session {
 }
 
 func (s *Session) IsActive() bool {
-	return s.Active && s.ExpiresAt.After(time.Now())
+	return s.Active && s.ExpiresAt.After(time.Now()) && (s.Identity == nil || s.Identity.IsActive())
 }
diff --git a/spec/openapi.json b/spec/openapi.json
index e4cb929ca..6cf23b00f 100755
--- a/spec/openapi.json
+++ b/spec/openapi.json
@@ -1048,6 +1048,17 @@
             "description": "SchemaURL is the URL of the endpoint where the identity's traits schema can be fetched from.\n\nformat: url",
             "type": "string"
           },
+          "state": {
+            "description": "State is the identity's state.",
+            "enum": [
+              "active",
+              "inactive"
+            ],
+            "type": "string"
+          },
+          "state_changed_at": {
+            "$ref": "#/components/schemas/NullTime"
+          },
           "traits": {
             "$ref": "#/components/schemas/identityTraits"
           },
@@ -1069,6 +1080,7 @@
           "id",
           "schema_id",
           "schema_url",
+          "state",
           "traits"
         ],
         "title": "Identity represents an Ory Kratos identity",
@@ -1864,13 +1876,22 @@
             "description": "SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. If set\nwill update the Identity's SchemaID.",
             "type": "string"
           },
+          "state": {
+            "description": "State is the identity's state.",
+            "enum": [
+              "active",
+              "inactive"
+            ],
+            "type": "string"
+          },
           "traits": {
             "description": "Traits represent an identity's traits. The identity is able to create, modify, and delete traits\nin a self-service manner. The input will always be validated against the JSON Schema defined\nin `schema_id`.",
             "type": "object"
           }
         },
         "required": [
-          "traits"
+          "traits",
+          "state"
         ],
         "type": "object"
       },
diff --git a/spec/swagger.json b/spec/swagger.json
index 011854f3b..2bf61870d 100755
--- a/spec/swagger.json
+++ b/spec/swagger.json
@@ -2615,6 +2615,7 @@
         "id",
         "schema_id",
         "schema_url",
+        "state",
         "traits"
       ],
       "properties": {
@@ -2649,6 +2650,17 @@
           "description": "SchemaURL is the URL of the endpoint where the identity's traits schema can be fetched from.\n\nformat: url",
           "type": "string"
         },
+        "state": {
+          "description": "State is the identity's state.",
+          "type": "string",
+          "enum": [
+            "active",
+            "inactive"
+          ]
+        },
+        "state_changed_at": {
+          "$ref": "#/definitions/NullTime"
+        },
         "traits": {
           "$ref": "#/definitions/identityTraits"
         },
@@ -3410,13 +3422,22 @@
     "updateIdentity": {
       "type": "object",
       "required": [
-        "traits"
+        "traits",
+        "state"
       ],
       "properties": {
         "schema_id": {
           "description": "SchemaID is the ID of the JSON Schema to be used for validating the identity's traits. If set\nwill update the Identity's SchemaID.",
           "type": "string"
         },
+        "state": {
+          "description": "State is the identity's state.",
+          "type": "string",
+          "enum": [
+            "active",
+            "inactive"
+          ]
+        },
         "traits": {
           "description": "Traits represent an identity's traits. The identity is able to create, modify, and delete traits\nin a self-service manner. The input will always be validated against the JSON Schema defined\nin `schema_id`.",
           "type": "object"


Test patch:
diff --git a/identity/handler_test.go b/identity/handler_test.go
index 77d2c6e6c..1933be096 100644
--- a/identity/handler_test.go
+++ b/identity/handler_test.go
@@ -4,13 +4,13 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"github.com/bxcodec/faker/v3"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
 	"github.com/ory/x/sqlxx"
-
 	"github.com/ory/x/urlx"
 
 	"github.com/ory/kratos/internal/testhelpers"
@@ -131,6 +131,7 @@ func TestHandler(t *testing.T) {
 
 			i.Traits = []byte(res.Get("traits").Raw)
 			i.ID = x.ParseUUID(res.Get("id").String())
+			i.StateChangedAt = sqlxx.NullTime(res.Get("state_changed_at").Time())
 			assert.NotEmpty(t, res.Get("id").String())
 
 			assert.EqualValues(t, "baz", res.Get("traits.bar").String(), "%s", res.Raw)
@@ -149,14 +150,23 @@ func TestHandler(t *testing.T) {
 		})
 
 		t.Run("case=should update an identity and persist the changes", func(t *testing.T) {
-			ur := identity.UpdateIdentity{Traits: []byte(`{"bar":"baz","foo":"baz"}`), SchemaID: i.SchemaID}
+			ur := identity.UpdateIdentity{
+				Traits:   []byte(`{"bar":"baz","foo":"baz"}`),
+				SchemaID: i.SchemaID,
+				State:    identity.StateInactive,
+			}
+
 			res := send(t, "PUT", "/identities/"+i.ID.String(), http.StatusOK, &ur)
 			assert.EqualValues(t, "baz", res.Get("traits.bar").String(), "%s", res.Raw)
 			assert.EqualValues(t, "baz", res.Get("traits.foo").String(), "%s", res.Raw)
+			assert.EqualValues(t, identity.StateInactive, res.Get("state").String(), "%s", res.Raw)
+			assert.NotEqualValues(t, i.StateChangedAt, sqlxx.NullTime(res.Get("state_changed_at").Time()), "%s", res.Raw)
 
 			res = get(t, "/identities/"+i.ID.String(), http.StatusOK)
 			assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw)
 			assert.EqualValues(t, "baz", res.Get("traits.bar").String(), "%s", res.Raw)
+			assert.EqualValues(t, identity.StateInactive, res.Get("state").String(), "%s", res.Raw)
+			assert.NotEqualValues(t, i.StateChangedAt, sqlxx.NullTime(res.Get("state_changed_at").Time()), "%s", res.Raw)
 		})
 
 		t.Run("case=should delete a client and no longer be able to retrieve it", func(t *testing.T) {
@@ -240,6 +250,20 @@ func TestHandler(t *testing.T) {
 		assert.Contains(t, res.Get("error.reason").String(), `additionalProperties "department" not allowed`, "%s", res.Raw)
 	})
 
+	t.Run("case=should fail to update identity if state is invalid", func(t *testing.T) {
+		var cr identity.CreateIdentity
+		cr.SchemaID = "employee"
+		cr.Traits = []byte(`{"email":"` + x.NewUUID().String() + `@ory.sh", "department": "ory"}`)
+		res := send(t, "POST", "/identities", http.StatusCreated, &cr)
+
+		id := res.Get("id").String()
+		res = send(t, "PUT", "/identities/"+id, http.StatusBadRequest, &identity.UpdateIdentity{
+			State:  "invalid-state",
+			Traits: []byte(`{"email":"` + faker.Email() + `", "department": "ory"}`),
+		})
+		assert.Contains(t, res.Get("error.reason").String(), `identity state is not valid`, "%s", res.Raw)
+	})
+
 	t.Run("case=should update the schema id", func(t *testing.T) {
 		var cr identity.CreateIdentity
 		cr.SchemaID = "employee"
diff --git a/identity/identity_test.go b/identity/identity_test.go
index a8917937c..14c671c50 100644
--- a/identity/identity_test.go
+++ b/identity/identity_test.go
@@ -23,6 +23,7 @@ func TestNewIdentity(t *testing.T) {
 	// assert.NotEmpty(t, i.Metadata)
 	assert.NotEmpty(t, i.Traits)
 	assert.NotNil(t, i.Credentials)
+	assert.True(t, i.IsActive())
 }
 
 func TestMarshalExcludesCredentials(t *testing.T) {
diff --git a/internal/testhelpers/handler_mock.go b/internal/testhelpers/handler_mock.go
index 745bdc086..79ee221c1 100644
--- a/internal/testhelpers/handler_mock.go
+++ b/internal/testhelpers/handler_mock.go
@@ -35,7 +35,8 @@ func MockSetSession(t *testing.T, reg mockDeps, conf *config.Config) httprouter.
 		i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
 		require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
 
-		require.NoError(t, reg.SessionManager().CreateAndIssueCookie(context.Background(), w, r, session.NewActiveSession(i, conf, time.Now().UTC())))
+		activeSession, _ := session.NewActiveSession(i, conf, time.Now().UTC())
+		require.NoError(t, reg.SessionManager().CreateAndIssueCookie(context.Background(), w, r, activeSession))
 
 		w.WriteHeader(http.StatusOK)
 	}
@@ -121,5 +122,5 @@ func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identit
 
 func MockSessionCreateHandler(t *testing.T, reg mockDeps) (httprouter.Handle, *session.Session) {
 	return MockSessionCreateHandlerWithIdentity(t, reg, &identity.Identity{
-		ID: x.NewUUID(), Traits: identity.Traits(`{"baz":"bar","foo":true,"bar":2.5}`)})
+		ID: x.NewUUID(), State: identity.StateActive, Traits: identity.Traits(`{"baz":"bar","foo":true,"bar":2.5}`)})
 }
diff --git a/internal/testhelpers/selfservice.go b/internal/testhelpers/selfservice.go
index cce5312c3..cdbaea6c2 100644
--- a/internal/testhelpers/selfservice.go
+++ b/internal/testhelpers/selfservice.go
@@ -84,6 +84,7 @@ func SelfServiceHookFakeIdentity(t *testing.T) *identity.Identity {
 	var i identity.Identity
 	require.NoError(t, faker.FakeData(&i))
 	i.Traits = identity.Traits(`{}`)
+	i.State = identity.StateActive
 	return &i
 }
 
diff --git a/internal/testhelpers/session.go b/internal/testhelpers/session.go
index aa73da892..31708c754 100644
--- a/internal/testhelpers/session.go
+++ b/internal/testhelpers/session.go
@@ -92,27 +92,37 @@ func NewHTTPClientWithSessionToken(t *testing.T, reg *driver.RegistryDefault, se
 }
 
 func NewHTTPClientWithArbitrarySessionToken(t *testing.T, reg *driver.RegistryDefault) *http.Client {
-	return NewHTTPClientWithSessionToken(t, reg, session.NewActiveSession(
-		&identity.Identity{ID: x.NewUUID()},
+	s, err := session.NewActiveSession(
+		&identity.Identity{ID: x.NewUUID(), State: identity.StateActive},
 		NewSessionLifespanProvider(time.Hour),
 		time.Now(),
-	))
+	)
+	require.NoError(t, err, "Could not initialize session from identity.")
+
+	return NewHTTPClientWithSessionToken(t, reg, s)
 }
 
 func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryDefault) *http.Client {
-	return NewHTTPClientWithSessionCookie(t, reg, session.NewActiveSession(
-		&identity.Identity{ID: x.NewUUID()},
+	s, err := session.NewActiveSession(
+		&identity.Identity{ID: x.NewUUID(), State: identity.StateActive},
 		NewSessionLifespanProvider(time.Hour),
 		time.Now(),
-	))
+	)
+	require.NoError(t, err, "Could not initialize session from identity.")
+
+	return NewHTTPClientWithSessionCookie(t, reg, s)
 }
 
 func NewHTTPClientWithIdentitySessionCookie(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
-	return NewHTTPClientWithSessionCookie(t, reg,
-		session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now()))
+	s, err := session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now())
+	require.NoError(t, err, "Could not initialize session from identity.")
+
+	return NewHTTPClientWithSessionCookie(t, reg, s)
 }
 
 func NewHTTPClientWithIdentitySessionToken(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
-	return NewHTTPClientWithSessionToken(t, reg,
-		session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now()))
+	s, err := session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now())
+	require.NoError(t, err, "Could not initialize session from identity.")
+
+	return NewHTTPClientWithSessionToken(t, reg, s)
 }
diff --git a/metrics/prometheus/handler_test.go b/metrics/prometheus/handler_test.go
deleted file mode 100644
index 8b8eef820..000000000
--- a/metrics/prometheus/handler_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package prometheus_test
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/prometheus/common/expfmt"
-	"github.com/stretchr/testify/require"
-
-	"github.com/ory/kratos/metrics/prometheus"
-
-	"github.com/ory/kratos/internal"
-	"github.com/ory/kratos/x"
-)
-
-func TestHandler(t *testing.T) {
-	_, reg := internal.NewFastRegistryWithMocks(t)
-	router := x.NewRouterAdmin()
-	reg.MetricsHandler().SetRoutes(router.Router)
-	ts := httptest.NewServer(router)
-	defer ts.Close()
-
-	c := http.DefaultClient
-
-	response, err := c.Get(ts.URL + prometheus.MetricsPrometheusPath)
-	require.NoError(t, err)
-	require.EqualValues(t, http.StatusOK, response.StatusCode)
-
-	textParser := expfmt.TextParser{}
-	text, err := textParser.TextToMetricFamilies(response.Body)
-	require.NoError(t, err)
-	require.EqualValues(t, "go_info", *text["go_info"].Name)
-}
diff --git a/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json b/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json
index 89d34bab4..5b2d49d1c 100644
--- a/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json
+++ b/persistence/sql/migratest/fixtures/identity/196d8c1e-4f04-40f0-94b3-5ec43996b28a.json
@@ -2,9 +2,11 @@
   "id": "196d8c1e-4f04-40f0-94b3-5ec43996b28a",
   "schema_id": "default",
   "schema_url": "https://www.ory.sh/schemas/default",
+  "state": "active",
+  "state_changed_at": null,
   "traits": {
     "email": "foobar@ory.sh"
   },
   "created_at": "2013-10-07T08:23:19Z",
   "updated_at": "2013-10-07T08:23:19Z"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json b/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json
index 8512ffa9f..44a0e6fc9 100644
--- a/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json
+++ b/persistence/sql/migratest/fixtures/identity/5ff66179-c240-4703-b0d8-494592cefff5.json
@@ -2,9 +2,11 @@
   "id": "5ff66179-c240-4703-b0d8-494592cefff5",
   "schema_id": "default",
   "schema_url": "https://www.ory.sh/schemas/default",
+  "state": "active",
+  "state_changed_at": null,
   "traits": {
     "email": "bazbar@ory.sh"
   },
   "created_at": "2013-10-07T08:23:19Z",
   "updated_at": "2013-10-07T08:23:19Z"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json b/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json
index 24a877487..ce5d26eb4 100644
--- a/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json
+++ b/persistence/sql/migratest/fixtures/identity/a251ebc2-880c-4f76-a8f3-38e6940eab0e.json
@@ -2,9 +2,11 @@
   "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e",
   "schema_id": "default",
   "schema_url": "https://www.ory.sh/schemas/default",
+  "state": "active",
+  "state_changed_at": null,
   "traits": {
     "email": "foobar@ory.sh"
   },
   "created_at": "2013-10-07T08:23:19Z",
   "updated_at": "2013-10-07T08:23:19Z"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json b/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json
index e04e9643a..7fa54af6d 100644
--- a/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json
+++ b/persistence/sql/migratest/fixtures/identity/d7b9addb-ac15-4bc2-9fa5-562e0bf48755.json
@@ -2,9 +2,11 @@
   "id": "d7b9addb-ac15-4bc2-9fa5-562e0bf48755",
   "schema_id": "default",
   "schema_url": "https://www.ory.sh/schemas/default",
+  "state": "active",
+  "state_changed_at": null,
   "traits": {
     "email": "d7b9@ory.sh"
   },
   "created_at": "2013-10-07T08:23:19Z",
   "updated_at": "2013-10-07T08:23:19Z"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json b/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json
index 68194de36..042ad96a3 100644
--- a/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json
+++ b/persistence/sql/migratest/fixtures/identity/ed253b2c-48ed-4c58-9b6f-1dc963c30a66.json
@@ -2,9 +2,11 @@
   "id": "ed253b2c-48ed-4c58-9b6f-1dc963c30a66",
   "schema_id": "default",
   "schema_url": "https://www.ory.sh/schemas/default",
+  "state": "active",
+  "state_changed_at": null,
   "traits": {
     "email": "bazbar@ory.sh"
   },
   "created_at": "2013-10-07T08:23:19Z",
   "updated_at": "2013-10-07T08:23:19Z"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json b/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json
index b0b05982f..9414301ec 100644
--- a/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json
+++ b/persistence/sql/migratest/fixtures/session/8571e374-38f2-4f46-8ad3-b9d914e174d3.json
@@ -8,6 +8,8 @@
     "id": "5ff66179-c240-4703-b0d8-494592cefff5",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "bazbar@ory.sh"
     },
@@ -26,4 +28,4 @@
     "created_at": "2013-10-07T08:23:19Z",
     "updated_at": "2013-10-07T08:23:19Z"
   }
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json b/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json
index e63d6d91e..f67e838aa 100644
--- a/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json
+++ b/persistence/sql/migratest/fixtures/session/f38cdebe-e567-42c9-a562-1bd4dee40998.json
@@ -8,6 +8,8 @@
     "id": "5ff66179-c240-4703-b0d8-494592cefff5",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "bazbar@ory.sh"
     },
@@ -26,4 +28,4 @@
     "created_at": "2013-10-07T08:23:19Z",
     "updated_at": "2013-10-07T08:23:19Z"
   }
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json b/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json
index 4aa715e26..14a50a0e2 100644
--- a/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json
+++ b/persistence/sql/migratest/fixtures/settings_flow/194c5b05-0487-4a11-bcbc-f301c9ff9678.json
@@ -13,6 +13,8 @@
     "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "foobar@ory.sh"
     },
@@ -61,4 +63,4 @@
     "updated_at": "2013-10-07T08:23:19Z"
   },
   "state": "show_form"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json b/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json
index 66bf86388..73ac98548 100644
--- a/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json
+++ b/persistence/sql/migratest/fixtures/settings_flow/19ede218-928c-4e02-ab49-b76e12b34f31.json
@@ -14,6 +14,8 @@
     "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "foobar@ory.sh"
     },
@@ -62,4 +64,4 @@
     "updated_at": "2013-10-07T08:23:19Z"
   },
   "state": "show_form"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json b/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json
index 1b9d59bcd..3f1e3167c 100644
--- a/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json
+++ b/persistence/sql/migratest/fixtures/settings_flow/21c5f714-3089-49d2-b387-f244d4dd9e00.json
@@ -14,6 +14,8 @@
     "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "foobar@ory.sh"
     },
@@ -62,4 +64,4 @@
     "updated_at": "2013-10-07T08:23:19Z"
   },
   "state": "show_form"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json b/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json
index 38295dca3..ffd9c5391 100644
--- a/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json
+++ b/persistence/sql/migratest/fixtures/settings_flow/74fd6c53-7651-453e-90b8-2c5adbf911bb.json
@@ -13,6 +13,8 @@
     "id": "5ff66179-c240-4703-b0d8-494592cefff5",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "bazbar@ory.sh"
     },
@@ -32,4 +34,4 @@
     "updated_at": "2013-10-07T08:23:19Z"
   },
   "state": "show_form"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json b/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json
index 6b91a6606..aa8b4f7ea 100644
--- a/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json
+++ b/persistence/sql/migratest/fixtures/settings_flow/77fe4fb3-2d4e-4532-b568-c44b0aece0aa.json
@@ -14,6 +14,8 @@
     "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "foobar@ory.sh"
     },
@@ -62,4 +64,4 @@
     "updated_at": "2013-10-07T08:23:19Z"
   },
   "state": "show_form"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json b/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json
index b11d5e9f5..5183bcb00 100644
--- a/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json
+++ b/persistence/sql/migratest/fixtures/settings_flow/90b4f970-b9ae-42bc-a0a7-73ec750e0aa1.json
@@ -14,6 +14,8 @@
     "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "foobar@ory.sh"
     },
@@ -62,4 +64,4 @@
     "updated_at": "2013-10-07T08:23:19Z"
   },
   "state": "show_form"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json b/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json
index b9a1f51e0..91a8406e0 100644
--- a/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json
+++ b/persistence/sql/migratest/fixtures/settings_flow/a79bfcf1-68ae-49de-8b23-4f96921b8341.json
@@ -14,6 +14,8 @@
     "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "foobar@ory.sh"
     },
@@ -62,4 +64,4 @@
     "updated_at": "2013-10-07T08:23:19Z"
   },
   "state": "show_form"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json b/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json
index 4ee1a6b25..0e9a910a3 100644
--- a/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json
+++ b/persistence/sql/migratest/fixtures/settings_flow/aeba85bd-1a8c-44bf-8fc3-3be83c01a3dc.json
@@ -14,6 +14,8 @@
     "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "foobar@ory.sh"
     },
@@ -62,4 +64,4 @@
     "updated_at": "2013-10-07T08:23:19Z"
   },
   "state": "show_form"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json b/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json
index d039d1802..8b16d9ca0 100644
--- a/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json
+++ b/persistence/sql/migratest/fixtures/settings_flow/cdfd1eed-34a4-491d-ad0a-7579d3a0a7ba.json
@@ -14,6 +14,8 @@
     "id": "a251ebc2-880c-4f76-a8f3-38e6940eab0e",
     "schema_id": "default",
     "schema_url": "https://www.ory.sh/schemas/default",
+    "state": "active",
+    "state_changed_at": null,
     "traits": {
       "email": "foobar@ory.sh"
     },
@@ -62,4 +64,4 @@
     "updated_at": "2013-10-07T08:23:19Z"
   },
   "state": "show_form"
-}
\ No newline at end of file
+}
diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go
index e1c1e3e57..672fe9913 100644
--- a/persistence/sql/persister_test.go
+++ b/persistence/sql/persister_test.go
@@ -217,6 +217,7 @@ func TestPersister_Transaction(t *testing.T) {
 	t.Run("case=should not create identity because callback returned error", func(t *testing.T) {
 		i := &ri.Identity{
 			ID:     x.NewUUID(),
+			State:  ri.StateActive,
 			Traits: ri.Traits(`{}`),
 		}
 		errMessage := "failing because why not"
diff --git a/selfservice/flow/recovery/hook_test.go b/selfservice/flow/recovery/hook_test.go
index 3b217b400..857c73c0b 100644
--- a/selfservice/flow/recovery/hook_test.go
+++ b/selfservice/flow/recovery/hook_test.go
@@ -33,7 +33,7 @@ func TestRecoveryExecutor(t *testing.T) {
 		router.GET("/recovery/post", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 			a, err := recovery.NewFlow(conf, time.Minute, x.FakeCSRFToken, r, reg.RecoveryStrategies(context.Background()), ft)
 			require.NoError(t, err)
-			s := session.NewActiveSession(i, conf, time.Now().UTC())
+			s, _ := session.NewActiveSession(i, conf, time.Now().UTC())
 			a.RequestURL = x.RequestURL(r).String()
 			if testhelpers.SelfServiceHookErrorHandler(t, w, r, recovery.ErrHookAbortFlow, reg.RecoveryExecutor().PostRecoveryHook(w, r, a, s)) {
 				_, _ = w.Write([]byte("ok"))
diff --git a/selfservice/flow/settings/error_test.go b/selfservice/flow/settings/error_test.go
index a4656653a..13d49fddb 100644
--- a/selfservice/flow/settings/error_test.go
+++ b/selfservice/flow/settings/error_test.go
@@ -58,6 +58,7 @@ func TestHandleError(t *testing.T) {
 	var id identity.Identity
 	require.NoError(t, faker.FakeData(&id))
 	id.SchemaID = "default"
+	id.State = identity.StateActive
 
 	router.GET("/error", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 		h.WriteFlowError(w, r, flowMethod, settingsFlow, &id, flowError)
@@ -127,8 +128,9 @@ func TestHandleError(t *testing.T) {
 			t.Cleanup(reset)
 
 			// This needs an authenticated client in order to call the RouteGetFlow endpoint
-			c := testhelpers.NewHTTPClientWithSessionToken(t, reg, session.NewActiveSession(&id,
-				testhelpers.NewSessionLifespanProvider(time.Hour), time.Now()))
+			s, err := session.NewActiveSession(&id, testhelpers.NewSessionLifespanProvider(time.Hour), time.Now())
+			require.NoError(t, err)
+			c := testhelpers.NewHTTPClientWithSessionToken(t, reg, s)
 
 			settingsFlow = newFlow(t, time.Minute, flow.TypeAPI)
 			flowError = settings.NewFlowExpiredError(expiredAnHourAgo)
diff --git a/selfservice/flow/settings/hook_test.go b/selfservice/flow/settings/hook_test.go
index 0c23d86b3..a35574420 100644
--- a/selfservice/flow/settings/hook_test.go
+++ b/selfservice/flow/settings/hook_test.go
@@ -46,7 +46,7 @@ func TestSettingsExecutor(t *testing.T) {
 				handleErr := testhelpers.SelfServiceHookSettingsErrorHandler
 				router.GET("/settings/post", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 					i := testhelpers.SelfServiceHookCreateFakeIdentity(t, reg)
-					sess := session.NewActiveSession(i, conf, time.Now().UTC())
+					sess, _ := session.NewActiveSession(i, conf, time.Now().UTC())
 
 					a := settings.NewFlow(conf, time.Minute, r, sess.Identity, ft)
 					a.RequestURL = x.RequestURL(r).String()
diff --git a/selfservice/strategy/password/settings_test.go b/selfservice/strategy/password/settings_test.go
index 401b7ec67..4235488c0 100644
--- a/selfservice/strategy/password/settings_test.go
+++ b/selfservice/strategy/password/settings_test.go
@@ -44,6 +44,7 @@ func newIdentityWithPassword(email string) *identity.Identity {
 				Config:      []byte(`{"hashed_password":"foo"}`),
 			},
 		},
+		State:    identity.StateActive,
 		Traits:   identity.Traits(`{"email":"` + email + `"}`),
 		SchemaID: config.DefaultIdentityTraitsSchemaID,
 	}
@@ -52,6 +53,7 @@ func newIdentityWithPassword(email string) *identity.Identity {
 func newEmptyIdentity() *identity.Identity {
 	return &identity.Identity{
 		ID:       x.NewUUID(),
+		State:    identity.StateActive,
 		Traits:   identity.Traits(`{}`),
 		SchemaID: config.DefaultIdentityTraitsSchemaID,
 	}
diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go
index dcea6e0e6..4fc04af33 100644
--- a/selfservice/strategy/profile/strategy_test.go
+++ b/selfservice/strategy/profile/strategy_test.go
@@ -48,6 +48,7 @@ func newIdentityWithPassword(email string) *identity.Identity {
 		},
 		Traits:              identity.Traits(`{"email":"` + email + `","stringy":"foobar","booly":false,"numby":2.5,"should_long_string":"asdfasdfasdfasdfasfdasdfasdfasdf","should_big_number":2048}`),
 		SchemaID:            config.DefaultIdentityTraitsSchemaID,
+		State:               identity.StateActive,
 		VerifiableAddresses: []identity.VerifiableAddress{{Value: email, Via: identity.VerifiableAddressTypeEmail}},
 		// TO ADD - RECOVERY EMAIL,
 	}
@@ -82,8 +83,8 @@ func TestStrategyTraits(t *testing.T) {
 
 	browserIdentity1 := newIdentityWithPassword("john-browser@doe.com")
 	apiIdentity1 := newIdentityWithPassword("john-api@doe.com")
-	browserIdentity2 := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`)}
-	apiIdentity2 := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`)}
+	browserIdentity2 := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`), State: identity.StateActive}
+	apiIdentity2 := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`), State: identity.StateActive}
 
 	browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity1)
 	browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity2)
diff --git a/session/handler_test.go b/session/handler_test.go
index 6ff0db451..e0ef3cfed 100644
--- a/session/handler_test.go
+++ b/session/handler_test.go
@@ -85,7 +85,7 @@ func TestSessionRevoke(t *testing.T) {
 	conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://stub/identity.schema.json")
 	i := &identity.Identity{Traits: identity.Traits(`{"baz":"bar"}`)}
 	require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
-	sess := NewActiveSession(i, conf, time.Now())
+	sess, _ := NewActiveSession(i, conf, time.Now())
 	require.NoError(t, reg.SessionPersister().CreateSession(context.Background(), sess))
 
 	sdk := testhelpers.NewSDKClient(publicTS)
diff --git a/session/manager_http_test.go b/session/manager_http_test.go
index 34127bef9..db8c4b2c8 100644
--- a/session/manager_http_test.go
+++ b/session/manager_http_test.go
@@ -87,7 +87,7 @@ func TestManagerHTTP(t *testing.T) {
 
 			i := identity.Identity{Traits: []byte("{}")}
 			require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i))
-			s = session.NewActiveSession(&i, conf, time.Now())
+			s, _ = session.NewActiveSession(&i, conf, time.Now())
 
 			c := testhelpers.NewClientWithCookies(t)
 			testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set")
@@ -100,9 +100,10 @@ func TestManagerHTTP(t *testing.T) {
 		t.Run("case=valid bearer auth as fallback", func(t *testing.T) {
 			conf.MustSet(config.ViperKeySessionLifespan, "1m")
 
-			i := identity.Identity{Traits: []byte("{}")}
+			i := identity.Identity{Traits: []byte("{}"), State: identity.StateActive}
 			require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i))
-			s = session.NewActiveSession(&i, conf, time.Now())
+			s, err := session.NewActiveSession(&i, conf, time.Now())
+			require.NoError(t, err)
 			require.NoError(t, reg.SessionPersister().CreateSession(context.Background(), s))
 			require.NotEmpty(t, s.Token)
 
@@ -119,9 +120,10 @@ func TestManagerHTTP(t *testing.T) {
 		t.Run("case=valid x-session-token auth even if bearer is set", func(t *testing.T) {
 			conf.MustSet(config.ViperKeySessionLifespan, "1m")
 
-			i := identity.Identity{Traits: []byte("{}")}
+			i := identity.Identity{Traits: []byte("{}"), State: identity.StateActive}
 			require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i))
-			s = session.NewActiveSession(&i, conf, time.Now())
+			s, err := session.NewActiveSession(&i, conf, time.Now())
+			require.NoError(t, err)
 			require.NoError(t, reg.SessionPersister().CreateSession(context.Background(), s))
 
 			req, err := http.NewRequest("GET", pts.URL+"/session/get", nil)
@@ -143,7 +145,7 @@ func TestManagerHTTP(t *testing.T) {
 
 			i := identity.Identity{Traits: []byte("{}")}
 			require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i))
-			s = session.NewActiveSession(&i, conf, time.Now())
+			s, _ = session.NewActiveSession(&i, conf, time.Now())
 
 			c := testhelpers.NewClientWithCookies(t)
 			testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set")
@@ -158,9 +160,9 @@ func TestManagerHTTP(t *testing.T) {
 		t.Run("case=revoked", func(t *testing.T) {
 			i := identity.Identity{Traits: []byte("{}")}
 			require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i))
-			s = session.NewActiveSession(&i, conf, time.Now())
+			s, _ = session.NewActiveSession(&i, conf, time.Now())
 
-			s = session.NewActiveSession(&i, conf, time.Now())
+			s, _ = session.NewActiveSession(&i, conf, time.Now())
 
 			c := testhelpers.NewClientWithCookies(t)
 			testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set")
diff --git a/session/session_test.go b/session/session_test.go
index 5ca6a0d32..c9ec10157 100644
--- a/session/session_test.go
+++ b/session/session_test.go
@@ -15,9 +15,16 @@ func TestSession(t *testing.T) {
 	conf, _ := internal.NewFastRegistryWithMocks(t)
 	authAt := time.Now()
 
-	s := session.NewActiveSession(new(identity.Identity), conf, authAt)
+	i := new(identity.Identity)
+	i.State = identity.StateActive
+	s, _ := session.NewActiveSession(i, conf, authAt)
 	assert.True(t, s.IsActive())
 
+	i = new(identity.Identity)
+	s, err := session.NewActiveSession(i, conf, authAt)
+	assert.Nil(t, s)
+	assert.Equal(t, session.ErrIdentityDisabled, err)
+
 	assert.False(t, (&session.Session{ExpiresAt: time.Now().Add(time.Hour)}).IsActive())
 	assert.False(t, (&session.Session{Active: true}).IsActive())
 }